Skip to content

Commit 5ae543d

Browse files
committed
feat(fast-usdc): deposit, withdraw liquidity in exchange for shares
- proposal shapes for deposit, withdraw - pool math with property testing
1 parent 9649c0a commit 5ae543d

9 files changed

+1006
-5
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
import {
2+
AmountMath,
3+
AmountShape,
4+
PaymentShape,
5+
RatioShape,
6+
} from '@agoric/ertp';
7+
import {
8+
makeRecorderTopic,
9+
TopicsRecordShape,
10+
} from '@agoric/zoe/src/contractSupport/topics.js';
11+
import { depositToSeat } from '@agoric/zoe/src/contractSupport/zoeHelpers.js';
12+
import { SeatShape } from '@agoric/zoe/src/typeGuards.js';
13+
import { M } from '@endo/patterns';
14+
import { Fail, q } from '@endo/errors';
15+
import {
16+
depositCalc,
17+
makeParity,
18+
withdrawCalc,
19+
withFees,
20+
} from '../pool-share-math.js';
21+
import { makeProposalShapes } from '../type-guards.js';
22+
23+
/**
24+
* @import {Zone} from '@agoric/zone';
25+
* @import {Remote, TypedPattern} from '@agoric/internal'
26+
* @import {StorageNode} from '@agoric/internal/src/lib-chainStorage.js'
27+
* @import {MakeRecorderKit, RecorderKit} from '@agoric/zoe/src/contractSupport/recorder.js'
28+
* @import {USDCProposalShapes, ShareWorth} from '../pool-share-math.js'
29+
*/
30+
31+
const { add, isEqual } = AmountMath;
32+
33+
/** @param {Brand} brand */
34+
const makeDust = brand => AmountMath.make(brand, 1n);
35+
36+
/**
37+
* Use of pool-share-math in offer handlers below assumes that
38+
* the pool balance represented by the USDC allocation in poolSeat
39+
* is the same as the pool balance represented by the numerator
40+
* of shareWorth.
41+
*
42+
* Well, almost: they're the same modulo the dust used
43+
* to initialize shareWorth with a non-zero denominator.
44+
*
45+
* @param {ZCFSeat} poolSeat
46+
* @param {ShareWorth} shareWorth
47+
* @param {Brand} USDC
48+
*/
49+
const checkPoolBalance = (poolSeat, shareWorth, USDC) => {
50+
const available = poolSeat.getAmountAllocated('USDC', USDC);
51+
const dust = makeDust(USDC);
52+
isEqual(add(available, dust), shareWorth.numerator) ||
53+
Fail`🚨 pool balance ${q(available)} inconsistent with shareWorth ${q(shareWorth)}`;
54+
};
55+
56+
/**
57+
* @param {Zone} zone
58+
* @param {ZCF} zcf
59+
* @param {Brand<'nat'>} USDC
60+
* @param {{
61+
* makeRecorderKit: MakeRecorderKit;
62+
* }} tools
63+
*/
64+
export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => {
65+
return zone.exoClassKit(
66+
'Liquidity Pool',
67+
{
68+
feeSink: M.interface('feeSink', {
69+
receive: M.call(AmountShape, PaymentShape).returns(M.promise()),
70+
}),
71+
external: M.interface('external', {
72+
publishShareWorth: M.call().returns(),
73+
}),
74+
depositHandler: M.interface('depositHandler', {
75+
handle: M.call(SeatShape, M.any()).returns(M.promise()),
76+
}),
77+
withdrawHandler: M.interface('withdrawHandler', {
78+
handle: M.call(SeatShape, M.any()).returns(M.promise()),
79+
}),
80+
public: M.interface('public', {
81+
makeDepositInvitation: M.call().returns(M.promise()),
82+
makeWithdrawInvitation: M.call().returns(M.promise()),
83+
getPublicTopics: M.call().returns(TopicsRecordShape),
84+
}),
85+
},
86+
/**
87+
* @param {ZCFMint<'nat'>} shareMint
88+
* @param {Remote<StorageNode>} node
89+
*/
90+
(shareMint, node) => {
91+
const { brand: PoolShares } = shareMint.getIssuerRecord();
92+
const proposalShapes = makeProposalShapes({ USDC, PoolShares });
93+
const shareWorth = makeParity(makeDust(USDC), PoolShares);
94+
const { zcfSeat: poolSeat } = zcf.makeEmptySeatKit();
95+
const shareWorthRecorderKit = tools.makeRecorderKit(node, RatioShape);
96+
return {
97+
shareMint,
98+
shareWorth,
99+
poolSeat,
100+
PoolShares,
101+
proposalShapes,
102+
shareWorthRecorderKit,
103+
};
104+
},
105+
{
106+
feeSink: {
107+
/**
108+
* @param {Amount<'nat'>} amount
109+
* @param {Payment<'nat'>} payment
110+
*/
111+
async receive(amount, payment) {
112+
const { poolSeat, shareWorth } = this.state;
113+
const { external } = this.facets;
114+
await depositToSeat(
115+
zcf,
116+
poolSeat,
117+
harden({ USDC: amount }),
118+
harden({ USDC: payment }),
119+
);
120+
this.state.shareWorth = withFees(shareWorth, amount);
121+
external.publishShareWorth();
122+
},
123+
},
124+
125+
external: {
126+
publishShareWorth() {
127+
const { shareWorth } = this.state;
128+
const { recorder } = this.state.shareWorthRecorderKit;
129+
// Consumers of this .write() are off-chain / outside the VM.
130+
// And there's no way to recover from a failed write.
131+
// So don't await.
132+
void recorder.write(shareWorth);
133+
},
134+
},
135+
136+
depositHandler: {
137+
/** @param {ZCFSeat} lp */
138+
async handle(lp) {
139+
const { shareWorth, shareMint, poolSeat } = this.state;
140+
const { external } = this.facets;
141+
142+
/** @type {USDCProposalShapes['deposit']} */
143+
// @ts-expect-error ensured by proposalShape
144+
const proposal = lp.getProposal();
145+
checkPoolBalance(poolSeat, shareWorth, USDC);
146+
const post = depositCalc(shareWorth, proposal);
147+
148+
// COMMIT POINT
149+
150+
try {
151+
const mint = shareMint.mintGains(post.payouts);
152+
this.state.shareWorth = post.shareWorth;
153+
zcf.atomicRearrange(
154+
harden([
155+
// zoe guarantees lp has proposal.give allocated
156+
[lp, poolSeat, proposal.give],
157+
// mintGains() above establishes that mint has post.payouts
158+
[mint, lp, post.payouts],
159+
]),
160+
);
161+
lp.exit();
162+
mint.exit();
163+
} catch (cause) {
164+
const reason = Error('🚨 cannot commit deposit', { cause });
165+
console.error(reason.message, cause);
166+
zcf.shutdownWithFailure(reason);
167+
}
168+
external.publishShareWorth();
169+
},
170+
},
171+
withdrawHandler: {
172+
/** @param {ZCFSeat} lp */
173+
async handle(lp) {
174+
const { shareWorth, shareMint, poolSeat } = this.state;
175+
const { external } = this.facets;
176+
177+
/** @type {USDCProposalShapes['withdraw']} */
178+
// @ts-expect-error ensured by proposalShape
179+
const proposal = lp.getProposal();
180+
const { zcfSeat: burn } = zcf.makeEmptySeatKit();
181+
checkPoolBalance(poolSeat, shareWorth, USDC);
182+
const post = withdrawCalc(shareWorth, proposal);
183+
184+
// COMMIT POINT
185+
186+
try {
187+
this.state.shareWorth = post.shareWorth;
188+
zcf.atomicRearrange(
189+
harden([
190+
// zoe guarantees lp has proposal.give allocated
191+
[lp, burn, proposal.give],
192+
// checkPoolBalance() + withdrawCalc() guarantee poolSeat has enough
193+
[poolSeat, lp, post.payouts],
194+
]),
195+
);
196+
shareMint.burnLosses(proposal.give, burn);
197+
lp.exit();
198+
burn.exit();
199+
} catch (cause) {
200+
const reason = Error('🚨 cannot commit withdraw', { cause });
201+
console.error(reason.message, cause);
202+
zcf.shutdownWithFailure(reason);
203+
}
204+
external.publishShareWorth();
205+
},
206+
},
207+
public: {
208+
makeDepositInvitation() {
209+
return zcf.makeInvitation(
210+
this.facets.depositHandler,
211+
'Deposit',
212+
undefined,
213+
this.state.proposalShapes.deposit,
214+
);
215+
},
216+
makeWithdrawInvitation() {
217+
return zcf.makeInvitation(
218+
this.facets.withdrawHandler,
219+
'Withdraw',
220+
undefined,
221+
this.state.proposalShapes.withdraw,
222+
);
223+
},
224+
getPublicTopics() {
225+
const { shareWorthRecorderKit } = this.state;
226+
return {
227+
shareWorth: makeRecorderTopic('shareWorth', shareWorthRecorderKit),
228+
};
229+
},
230+
},
231+
},
232+
{
233+
finish: ({ facets: { external } }) => {
234+
void external.publishShareWorth();
235+
},
236+
},
237+
);
238+
};
239+
harden(prepareLiquidityPoolKit);

packages/fast-usdc/src/fast-usdc.contract.js

+60-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1+
import { AssetKind } from '@agoric/ertp';
12
import { BrandShape } from '@agoric/ertp/src/typeGuards.js';
23
import { assertAllDefined, makeTracer } from '@agoric/internal';
34
import { observeIteration, subscribeEach } from '@agoric/notifier';
45
import { withOrchestration } from '@agoric/orchestration';
6+
import { provideSingleton } from '@agoric/zoe/src/contractSupport/durability.js';
7+
import { prepareRecorderKitMakers } from '@agoric/zoe/src/contractSupport/recorder.js';
58
import { M } from '@endo/patterns';
69
import { prepareAdvancer } from './exos/advancer.js';
10+
import { prepareLiquidityPoolKit } from './exos/liquidity-pool.js';
711
import { prepareSettler } from './exos/settler.js';
812
import { prepareStatusManager } from './exos/status-manager.js';
913
import { prepareTransactionFeedKit } from './exos/transaction-feed.js';
@@ -43,7 +47,11 @@ export const contract = async (zcf, privateArgs, zone, tools) => {
4347
assert(tools, 'no tools');
4448
const terms = zcf.getTerms();
4549
assert('USDC' in terms.brands, 'no USDC brand');
46-
assert('PoolShares' in terms.brands, 'no PoolShares brand');
50+
51+
const { makeRecorderKit } = prepareRecorderKitMakers(
52+
zone.mapStore('vstorage'),
53+
privateArgs.marshaller,
54+
);
4755

4856
const statusManager = prepareStatusManager(zone);
4957
const makeSettler = prepareSettler(zone, { statusManager });
@@ -71,8 +79,20 @@ export const contract = async (zcf, privateArgs, zone, tools) => {
7179
}
7280
},
7381
});
82+
const makeLiquidityPoolKit = prepareLiquidityPoolKit(
83+
zone,
84+
zcf,
85+
terms.brands.USDC,
86+
{ makeRecorderKit },
87+
);
7488

75-
const creatorFacet = zone.exo('Fast USDC Creator', undefined, {});
89+
const creatorFacet = zone.exo('Fast USDC Creator', undefined, {
90+
simulateFeesFromAdvance(amount, payment) {
91+
console.log('🚧🚧 UNTIL: advance fees are implemented 🚧🚧');
92+
// eslint-disable-next-line no-use-before-define
93+
return poolKit.feeSink.receive(amount, payment);
94+
},
95+
});
7696

7797
const publicFacet = zone.exo('Fast USDC Public', undefined, {
7898
// XXX to be removed before production
@@ -95,8 +115,46 @@ export const contract = async (zcf, privateArgs, zone, tools) => {
95115
return 'noop; evidence was pushed in the invitation maker call';
96116
}, 'noop invitation');
97117
},
118+
makeDepositInvitation() {
119+
// eslint-disable-next-line no-use-before-define
120+
return poolKit.public.makeDepositInvitation();
121+
},
122+
makeWithdrawInvitation() {
123+
// eslint-disable-next-line no-use-before-define
124+
return poolKit.public.makeWithdrawInvitation();
125+
},
126+
getPublicTopics() {
127+
// eslint-disable-next-line no-use-before-define
128+
return poolKit.public.getPublicTopics();
129+
},
98130
});
99131

132+
// ^^^ Define all kinds above this line. Keep remote calls below. vvv
133+
134+
// NOTE: Using a ZCFMint is helpful for the usual reasons (
135+
// synchronous mint/burn, keeping assets out of contract vats, ...).
136+
// And there's just one pool, which suggests building it with zone.exo().
137+
//
138+
// But zone.exo() defines a kind and
139+
// all kinds have to be defined before any remote calls,
140+
// such as the one to the zoe vat as part of making a ZCFMint.
141+
//
142+
// So we use zone.exoClassKit above to define the liquidity pool kind
143+
// and pass the shareMint into the maker / init function.
144+
145+
const shareMint = await provideSingleton(
146+
zone.mapStore('mint'),
147+
'PoolShare',
148+
() =>
149+
zcf.makeZCFMint('PoolShares', AssetKind.NAT, {
150+
decimalPlaces: 6,
151+
}),
152+
);
153+
154+
const poolKit = zone.makeOnce('Liquidity Pool kit', () =>
155+
makeLiquidityPoolKit(shareMint, privateArgs.storageNode),
156+
);
157+
100158
return harden({ creatorFacet, publicFacet });
101159
};
102160
harden(contract);

0 commit comments

Comments
 (0)