Skip to content

Commit

Permalink
fix(fast-usdc): recover from bad lp proposal
Browse files Browse the repository at this point in the history
  • Loading branch information
samsiegart committed Jan 2, 2025
1 parent c833874 commit c5d30f6
Show file tree
Hide file tree
Showing 6 changed files with 178 additions and 42 deletions.
153 changes: 124 additions & 29 deletions packages/boot/test/fast-usdc/fast-usdc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,47 +200,49 @@ test.serial('writes account addresses to vstorage', async t => {
await documentStorageSchema(t, storage, doc);
});

test.serial('makes usdc advance', async t => {
const {
walletFactoryDriver: wd,
storage,
agoricNamesRemotes,
harness,
} = t.context;
const oracles = await Promise.all([
wd.provideSmartWallet('agoric19uscwxdac6cf6z7d5e26e0jm0lgwstc47cpll8'),
wd.provideSmartWallet('agoric1krunjcqfrf7la48zrvdfeeqtls5r00ep68mzkr'),
wd.provideSmartWallet('agoric1n4fcxsnkxe4gj6e24naec99hzmc4pjfdccy5nj'),
]);
await Promise.all(
oracles.map(wallet =>
wallet.sendOffer({
id: 'claim-oracle-invitation',
invitationSpec: {
source: 'purse',
instance: agoricNamesRemotes.instance.fastUsdc,
description: 'oracle operator invitation',
},
proposal: {},
}),
),
test.serial('LP deposits', async t => {
const { walletFactoryDriver: wd, agoricNamesRemotes } = t.context;
const lp = await wd.provideSmartWallet(
'agoric19uscwxdac6cf6z7d5e26e0jm0lgwstc47cpll8',
);

const lp = oracles[0]; // somewhat arbitrary

// @ts-expect-error it doesnt recognize usdc as a Brand type
// @ts-expect-error it doesnt recognize USDC as a Brand type
const usdc = agoricNamesRemotes.vbankAsset.USDC.brand as Brand<'nat'>;
// @ts-expect-error it doesnt recognize FastLP as a Brand type
const fastLP = agoricNamesRemotes.vbankAsset.FastLP.brand as Brand<'nat'>;

// Send a bad proposal first to make sure it's recoverable.
await lp.sendOffer({
id: 'deposit-lp-0',
invitationSpec: {
source: 'agoricContract',
instancePath: ['fastUsdc'],
callPipe: [['makeDepositInvitation', []]],
},
proposal: {
give: {
USDC: { brand: usdc, value: 98_000_000n },
},
want: {
BADPROPOSAL: { brand: fastLP, value: 567_000_000n },
},
},
});

await lp.sendOffer({
id: 'deposit-lp-1',
invitationSpec: {
source: 'agoricContract',
instancePath: ['fastUsdc'],
callPipe: [['makeDepositInvitation', []]],
},
proposal: {
give: {
USDC: { brand: usdc, value: 150_000_000n },
},
want: {
PoolShare: { brand: fastLP, value: 150_000_000n },
},
},
});
await eventLoopIteration();
Expand All @@ -252,15 +254,45 @@ test.serial('makes usdc advance', async t => {
obj.denom === 'ufastlp' &&
obj.recipient === lp.getAddress(),
);
t.log('LP vbank deposit', lpBankDeposit);
t.true(BigInt(lpBankDeposit.amount) > 1_000_000n, 'vbank GIVEs shares to LP');
t.log('LP vbank deposits', lpBankDeposit);
t.true(
BigInt(lpBankDeposit.amount) === 150_000_000n,
'vbank GIVEs shares to LP',
);

const { purses } = lp.getCurrentWalletRecord();
// XXX #10491 should not need to resort to string match on brand
t.falsy(
purses.find(p => `${p.brand}`.match(/FastLP/)),
'FastLP balance not in wallet record',
);
});

test.serial('makes usdc advance', async t => {
const {
walletFactoryDriver: wd,
storage,
agoricNamesRemotes,
harness,
} = t.context;
const oracles = await Promise.all([
wd.provideSmartWallet('agoric19uscwxdac6cf6z7d5e26e0jm0lgwstc47cpll8'),
wd.provideSmartWallet('agoric1krunjcqfrf7la48zrvdfeeqtls5r00ep68mzkr'),
wd.provideSmartWallet('agoric1n4fcxsnkxe4gj6e24naec99hzmc4pjfdccy5nj'),
]);
await Promise.all(
oracles.map(wallet =>
wallet.sendOffer({
id: 'claim-oracle-invitation',
invitationSpec: {
source: 'purse',
instance: agoricNamesRemotes.instance.fastUsdc,
description: 'oracle operator invitation',
},
proposal: {},
}),
),
);

const EUD = 'dydx1anything';
const lastNodeValue = storage.getValues('published.fastUsdc').at(-1);
Expand Down Expand Up @@ -361,6 +393,69 @@ test.serial('skips usdc advance when risks identified', async t => {
await documentStorageSchema(t, storage, doc);
});

test.serial('LP withdraws', async t => {
const { walletFactoryDriver: wd, agoricNamesRemotes } = t.context;
const lp = await wd.provideSmartWallet(
'agoric19uscwxdac6cf6z7d5e26e0jm0lgwstc47cpll8',
);

// @ts-expect-error it doesnt recognize USDC as a Brand type
const usdc = agoricNamesRemotes.vbankAsset.USDC.brand as Brand<'nat'>;
// @ts-expect-error it doesnt recognize FastLP as a Brand type
const fastLP = agoricNamesRemotes.vbankAsset.FastLP.brand as Brand<'nat'>;

// Send a bad proposal first to make sure it's recoverable.
await lp.sendOffer({
id: 'withdraw-lp-bad-shape',
invitationSpec: {
source: 'agoricContract',
instancePath: ['fastUsdc'],
callPipe: [['makeWithdrawInvitation', []]],
},
proposal: {
give: {
PoolShare: { brand: fastLP, value: 777_000n },
},
want: {
BADPROPOSALSHAPE: { brand: usdc, value: 777_000n },
},
},
});

await lp.sendOffer({
id: 'withdraw-lp-1',
invitationSpec: {
source: 'agoricContract',
instancePath: ['fastUsdc'],
callPipe: [['makeWithdrawInvitation', []]],
},
proposal: {
give: {
PoolShare: { brand: fastLP, value: 369_000n },
},
want: {
USDC: { brand: usdc, value: 369_000n },
},
},
});
await eventLoopIteration();

const { denom: usdcDenom } = agoricNamesRemotes.vbankAsset.USDC;
const { getOutboundMessages } = t.context.bridgeUtils;
const lpBankDeposits = getOutboundMessages(BridgeId.BANK).filter(
obj =>
obj.type === 'VBANK_GIVE' &&
obj.denom === usdcDenom &&
obj.recipient === lp.getAddress(),
);
t.log('LP vbank deposits', lpBankDeposits);
// Check index 2. Indexes 0 and 1 would be from the deposit offers in prior testcase.
t.true(
BigInt(lpBankDeposits[2].amount) >= 369_000n,
'vbank GIVEs USDC back to LP',
);
});

test.serial('restart contract', async t => {
const { EV } = t.context.runUtils;
await null;
Expand Down
16 changes: 15 additions & 1 deletion packages/fast-usdc/src/cli/lp-commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { AmountMath } from '@agoric/ertp';
import {
assertParsableNumber,
ceilDivideBy,
floorDivideBy,
multiplyBy,
parseRatio,
} from '@agoric/zoe/src/contractSupport/ratio.js';
Expand Down Expand Up @@ -81,18 +82,32 @@ export const addLPCommands = (
.action(async opts => {
swkP ||= loadSwk();
const swk = await swkP;

/** @type {Brand<'nat'>} */
// @ts-expect-error it doesnt recognize usdc as a Brand type
const usdc = swk.agoricNames.brand.USDC;
assert(usdc, 'USDC brand not in agoricNames');

/** @type {Brand<'nat'>} */
// @ts-expect-error it doesnt recognize FastLP as a Brand type
const poolShare = swk.agoricNames.brand.FastLP;
assert(poolShare, 'FastLP brand not in agoricNames');

const usdcAmount = parseUSDCAmount(opts.amount, usdc);

/** @type {import('../types.js').PoolMetrics} */
// @ts-expect-error it treats this as "unknown"
const metrics = await swk.readPublished('fastUsdc.poolMetrics');
const fastLPAmount = floorDivideBy(usdcAmount, metrics.shareWorth);

/** @type {USDCProposalShapes['deposit']} */
const proposal = {
give: {
USDC: usdcAmount,
},
want: {
PoolShare: fastLPAmount,
},
};

/** @type {OfferSpec} */
Expand Down Expand Up @@ -125,7 +140,6 @@ export const addLPCommands = (
.requiredOption('--amount <number>', 'USDC amount', parseDecimal)
.option('--offerId <string>', 'Offer id', String, `lpWithdraw-${now()}`)
.action(async opts => {
swkP ||= loadSwk();
swkP ||= loadSwk();
const swk = await swkP;

Expand Down
2 changes: 1 addition & 1 deletion packages/fast-usdc/src/pool-share-math.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const makeParity = (numerator, denominatorBrand) => {
* @typedef {{
* deposit: {
* give: { USDC: Amount<'nat'> },
* want?: { PoolShare: Amount<'nat'> }
* want: { PoolShare: Amount<'nat'> }
* },
* withdraw: {
* give: { PoolShare: Amount<'nat'> }
Expand Down
8 changes: 4 additions & 4 deletions packages/fast-usdc/src/type-guards.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ export const makeNatAmountShape = (brand, min) =>
/** @param {Record<'PoolShares' | 'USDC', Brand<'nat'>>} brands */
export const makeProposalShapes = ({ PoolShares, USDC }) => {
/** @type {TypedPattern<USDCProposalShapes['deposit']>} */
const deposit = M.splitRecord(
{ give: { USDC: makeNatAmountShape(USDC, 1n) } },
{ want: M.splitRecord({}, { PoolShare: makeNatAmountShape(PoolShares) }) },
);
const deposit = M.splitRecord({
give: { USDC: makeNatAmountShape(USDC, 1n) },
want: { PoolShare: makeNatAmountShape(PoolShares) },
});
/** @type {TypedPattern<USDCProposalShapes['withdraw']>} */
const withdraw = M.splitRecord({
give: { PoolShare: makeNatAmountShape(PoolShares, 1n) },
Expand Down
5 changes: 4 additions & 1 deletion packages/fast-usdc/test/cli/lp-commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const test = anyTest as TestFn<Awaited<ReturnType<typeof makeTestContext>>>;
test.beforeEach(async t => (t.context = await makeTestContext()));

test('fast-usdc deposit command', async t => {
const { program, marshaller, out, err, USDC } = t.context;
const { program, marshaller, out, err, USDC, FastLP } = t.context;
const amount = 100.05;
const argv = [...`node fast-usdc deposit`.split(' '), ...flags({ amount })];
t.log(...argv);
Expand All @@ -78,6 +78,9 @@ test('fast-usdc deposit command', async t => {
give: {
USDC: { brand: USDC, value: 100_050_000n },
},
want: {
PoolShare: { brand: FastLP, value: 90_954_545n },
},
},
},
});
Expand Down
36 changes: 30 additions & 6 deletions packages/fast-usdc/test/pool-share-math.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,12 @@ test('initial withdrawal fails', t => {
test('withdrawal after deposit OK', t => {
const { PoolShares, USDC } = brands;
const state0 = makeParity(make(USDC, 1n), PoolShares);
const emptyShares = makeEmpty(PoolShares);

const pDep = { give: { USDC: make(USDC, 100n) } };
const pDep = {
give: { USDC: make(USDC, 100n) },
want: { PoolShare: emptyShares },
};
const { shareWorth: state1 } = depositCalc(state0, pDep);

const proposal = harden({
Expand All @@ -81,8 +85,12 @@ test('withdrawal after deposit OK', t => {

test('deposit offer underestimates value of share', t => {
const { PoolShares, USDC } = brands;
const emptyShares = makeEmpty(PoolShares);

const pDep = { give: { USDC: make(USDC, 100n) } };
const pDep = {
give: { USDC: make(USDC, 100n) },
want: { PoolShare: emptyShares },
};
const { shareWorth: state1 } = depositCalc(parity, pDep);
const state2 = withFees(state1, make(USDC, 20n));

Expand Down Expand Up @@ -119,9 +127,13 @@ test('deposit offer overestimates value of share', t => {

test('withdrawal offer underestimates value of share', t => {
const { PoolShares, USDC } = brands;
const emptyShares = makeEmpty(PoolShares);
const state0 = makeParity(make(USDC, 1n), PoolShares);

const proposal1 = harden({ give: { USDC: make(USDC, 100n) } });
const proposal1 = harden({
give: { USDC: make(USDC, 100n) },
want: { PoolShare: emptyShares },
});
const { shareWorth: state1 } = depositCalc(state0, proposal1);

const proposal = harden({
Expand All @@ -143,9 +155,13 @@ test('withdrawal offer underestimates value of share', t => {

test('withdrawal offer overestimates value of share', t => {
const { PoolShares, USDC } = brands;
const emptyShares = makeEmpty(PoolShares);
const state0 = makeParity(make(USDC, 1n), PoolShares);

const d100 = { give: { USDC: make(USDC, 100n) } };
const d100 = {
give: { USDC: make(USDC, 100n) },
want: { PoolShare: emptyShares },
};
const { shareWorth: state1 } = depositCalc(state0, d100);

const proposal = harden({
Expand Down Expand Up @@ -196,7 +212,12 @@ testProp(
'deposit properties',
[arbShareWorth, arbUSDC],
(t, shareWorth, In) => {
const actual = depositCalc(shareWorth, { give: { USDC: In } });
const { PoolShares } = brands;
const emptyShares = makeEmpty(PoolShares);
const actual = depositCalc(shareWorth, {
give: { USDC: In },
want: { PoolShare: emptyShares },
});
const {
payouts: { PoolShare },
shareWorth: post,
Expand Down Expand Up @@ -234,7 +255,10 @@ testProp(

for (const { party, action } of actions) {
if ('In' in action) {
const d = depositCalc(shareWorth, { give: { USDC: action.In } });
const d = depositCalc(shareWorth, {
give: { USDC: action.In },
want: { PoolShare: emptyShares },
});
myShares[party] = add(
myShares[party] || emptyShares,
d.payouts.PoolShare,
Expand Down

0 comments on commit c5d30f6

Please sign in to comment.