-
Notifications
You must be signed in to change notification settings - Fork 207
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(fast-usdc): deposit, withdraw liquidity in exchange for shares #10400
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,239 @@ | ||
import { | ||
AmountMath, | ||
AmountShape, | ||
PaymentShape, | ||
RatioShape, | ||
} from '@agoric/ertp'; | ||
import { | ||
makeRecorderTopic, | ||
TopicsRecordShape, | ||
} from '@agoric/zoe/src/contractSupport/topics.js'; | ||
import { depositToSeat } from '@agoric/zoe/src/contractSupport/zoeHelpers.js'; | ||
import { SeatShape } from '@agoric/zoe/src/typeGuards.js'; | ||
import { M } from '@endo/patterns'; | ||
import { Fail, q } from '@endo/errors'; | ||
import { | ||
depositCalc, | ||
makeParity, | ||
withdrawCalc, | ||
withFees, | ||
} from '../pool-share-math.js'; | ||
import { makeProposalShapes } from '../type-guards.js'; | ||
|
||
/** | ||
* @import {Zone} from '@agoric/zone'; | ||
* @import {Remote, TypedPattern} from '@agoric/internal' | ||
* @import {StorageNode} from '@agoric/internal/src/lib-chainStorage.js' | ||
* @import {MakeRecorderKit, RecorderKit} from '@agoric/zoe/src/contractSupport/recorder.js' | ||
* @import {USDCProposalShapes, ShareWorth} from '../pool-share-math.js' | ||
*/ | ||
|
||
const { add, isEqual } = AmountMath; | ||
|
||
/** @param {Brand} brand */ | ||
const makeDust = brand => AmountMath.make(brand, 1n); | ||
|
||
/** | ||
* Use of pool-share-math in offer handlers below assumes that | ||
* the pool balance represented by the USDC allocation in poolSeat | ||
* is the same as the pool balance represented by the numerator | ||
* of shareWorth. | ||
* | ||
* Well, almost: they're the same modulo the dust used | ||
* to initialize shareWorth with a non-zero denominator. | ||
* | ||
* @param {ZCFSeat} poolSeat | ||
* @param {ShareWorth} shareWorth | ||
* @param {Brand} USDC | ||
*/ | ||
const checkPoolBalance = (poolSeat, shareWorth, USDC) => { | ||
const available = poolSeat.getAmountAllocated('USDC', USDC); | ||
const dust = makeDust(USDC); | ||
isEqual(add(available, dust), shareWorth.numerator) || | ||
Fail`🚨 pool balance ${q(available)} inconsistent with shareWorth ${q(shareWorth)}`; | ||
}; | ||
|
||
/** | ||
* @param {Zone} zone | ||
* @param {ZCF} zcf | ||
* @param {Brand<'nat'>} USDC | ||
* @param {{ | ||
* makeRecorderKit: MakeRecorderKit; | ||
* }} tools | ||
*/ | ||
export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => { | ||
return zone.exoClassKit( | ||
'Liquidity Pool', | ||
{ | ||
feeSink: M.interface('feeSink', { | ||
receive: M.call(AmountShape, PaymentShape).returns(M.promise()), | ||
}), | ||
external: M.interface('external', { | ||
publishShareWorth: M.call().returns(), | ||
}), | ||
depositHandler: M.interface('depositHandler', { | ||
handle: M.call(SeatShape, M.any()).returns(M.promise()), | ||
}), | ||
withdrawHandler: M.interface('withdrawHandler', { | ||
handle: M.call(SeatShape, M.any()).returns(M.promise()), | ||
}), | ||
public: M.interface('public', { | ||
makeDepositInvitation: M.call().returns(M.promise()), | ||
makeWithdrawInvitation: M.call().returns(M.promise()), | ||
getPublicTopics: M.call().returns(TopicsRecordShape), | ||
}), | ||
}, | ||
/** | ||
* @param {ZCFMint<'nat'>} shareMint | ||
* @param {Remote<StorageNode>} node | ||
*/ | ||
(shareMint, node) => { | ||
turadg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const { brand: PoolShares } = shareMint.getIssuerRecord(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: should we call it There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hardly a nit! the keys of I'll follow up with product. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suppose the name here is local to this contract. But I'm also working on the core-eval (#10301), which will put a name in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Here's the current list of brands in agoricNames,
I think the abbreviations are generally for vbank brands. Using the more verbose style would be appropriate here, in part to convey it's not a vbank brand. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not a vbank brand? why not? we don't want folks sending it over IBC to trade on the open market for some reason? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we punted on it being a vbank brand, but with the option to make it one later. That suggests that the name be amenable to vbank. But ay you also point out, it's easy to change later. We can defer to milestone 3: #10432 |
||
const proposalShapes = makeProposalShapes({ USDC, PoolShares }); | ||
const shareWorth = makeParity(makeDust(USDC), PoolShares); | ||
const { zcfSeat: poolSeat } = zcf.makeEmptySeatKit(); | ||
const shareWorthRecorderKit = tools.makeRecorderKit(node, RatioShape); | ||
return { | ||
shareMint, | ||
shareWorth, | ||
poolSeat, | ||
PoolShares, | ||
proposalShapes, | ||
shareWorthRecorderKit, | ||
}; | ||
}, | ||
{ | ||
feeSink: { | ||
/** | ||
* @param {Amount<'nat'>} amount | ||
* @param {Payment<'nat'>} payment | ||
*/ | ||
async receive(amount, payment) { | ||
const { poolSeat, shareWorth } = this.state; | ||
const { external } = this.facets; | ||
await depositToSeat( | ||
zcf, | ||
poolSeat, | ||
harden({ USDC: amount }), | ||
harden({ USDC: payment }), | ||
); | ||
this.state.shareWorth = withFees(shareWorth, amount); | ||
external.publishShareWorth(); | ||
}, | ||
}, | ||
|
||
external: { | ||
publishShareWorth() { | ||
const { shareWorth } = this.state; | ||
const { recorder } = this.state.shareWorthRecorderKit; | ||
// Consumers of this .write() are off-chain / outside the VM. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. helpful comment! |
||
// And there's no way to recover from a failed write. | ||
// So don't await. | ||
void recorder.write(shareWorth); | ||
}, | ||
}, | ||
|
||
depositHandler: { | ||
/** @param {ZCFSeat} lp */ | ||
async handle(lp) { | ||
const { shareWorth, shareMint, poolSeat } = this.state; | ||
const { external } = this.facets; | ||
|
||
/** @type {USDCProposalShapes['deposit']} */ | ||
// @ts-expect-error ensured by proposalShape | ||
const proposal = lp.getProposal(); | ||
checkPoolBalance(poolSeat, shareWorth, USDC); | ||
const post = depositCalc(shareWorth, proposal); | ||
|
||
// COMMIT POINT | ||
turadg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
try { | ||
const mint = shareMint.mintGains(post.payouts); | ||
this.state.shareWorth = post.shareWorth; | ||
zcf.atomicRearrange( | ||
harden([ | ||
// zoe guarantees lp has proposal.give allocated | ||
[lp, poolSeat, proposal.give], | ||
// mintGains() above establishes that mint has post.payouts | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🙏 |
||
[mint, lp, post.payouts], | ||
]), | ||
); | ||
lp.exit(); | ||
mint.exit(); | ||
} catch (cause) { | ||
const reason = Error('🚨 cannot commit deposit', { cause }); | ||
console.error(reason.message, cause); | ||
zcf.shutdownWithFailure(reason); | ||
} | ||
external.publishShareWorth(); | ||
}, | ||
}, | ||
withdrawHandler: { | ||
/** @param {ZCFSeat} lp */ | ||
async handle(lp) { | ||
const { shareWorth, shareMint, poolSeat } = this.state; | ||
const { external } = this.facets; | ||
|
||
/** @type {USDCProposalShapes['withdraw']} */ | ||
// @ts-expect-error ensured by proposalShape | ||
const proposal = lp.getProposal(); | ||
const { zcfSeat: burn } = zcf.makeEmptySeatKit(); | ||
checkPoolBalance(poolSeat, shareWorth, USDC); | ||
const post = withdrawCalc(shareWorth, proposal); | ||
|
||
// COMMIT POINT | ||
|
||
try { | ||
this.state.shareWorth = post.shareWorth; | ||
zcf.atomicRearrange( | ||
harden([ | ||
// zoe guarantees lp has proposal.give allocated | ||
[lp, burn, proposal.give], | ||
// checkPoolBalance() + withdrawCalc() guarantee poolSeat has enough | ||
[poolSeat, lp, post.payouts], | ||
]), | ||
); | ||
shareMint.burnLosses(proposal.give, burn); | ||
lp.exit(); | ||
burn.exit(); | ||
} catch (cause) { | ||
const reason = Error('🚨 cannot commit withdraw', { cause }); | ||
console.error(reason.message, cause); | ||
zcf.shutdownWithFailure(reason); | ||
} | ||
external.publishShareWorth(); | ||
}, | ||
}, | ||
public: { | ||
makeDepositInvitation() { | ||
return zcf.makeInvitation( | ||
this.facets.depositHandler, | ||
'Deposit', | ||
undefined, | ||
this.state.proposalShapes.deposit, | ||
); | ||
}, | ||
makeWithdrawInvitation() { | ||
return zcf.makeInvitation( | ||
this.facets.withdrawHandler, | ||
'Withdraw', | ||
undefined, | ||
this.state.proposalShapes.withdraw, | ||
); | ||
}, | ||
getPublicTopics() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was surprised to see Consider making this public facet be its own There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good idea, but after looking into it, I'd like to postpone this until we get more pieces of the contract together. I started on it, but the LP exo isn't available until after remote calls, which would mean doing the same gymnastics for the public facet. |
||
const { shareWorthRecorderKit } = this.state; | ||
return { | ||
shareWorth: makeRecorderTopic('shareWorth', shareWorthRecorderKit), | ||
}; | ||
}, | ||
}, | ||
}, | ||
{ | ||
finish: ({ facets: { external } }) => { | ||
void external.publishShareWorth(); | ||
}, | ||
}, | ||
); | ||
}; | ||
harden(prepareLiquidityPoolKit); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,13 @@ | ||
import { AssetKind } from '@agoric/ertp'; | ||
import { BrandShape } from '@agoric/ertp/src/typeGuards.js'; | ||
import { assertAllDefined, makeTracer } from '@agoric/internal'; | ||
import { observeIteration, subscribeEach } from '@agoric/notifier'; | ||
import { withOrchestration } from '@agoric/orchestration'; | ||
import { provideSingleton } from '@agoric/zoe/src/contractSupport/durability.js'; | ||
import { prepareRecorderKitMakers } from '@agoric/zoe/src/contractSupport/recorder.js'; | ||
import { M } from '@endo/patterns'; | ||
import { prepareAdvancer } from './exos/advancer.js'; | ||
import { prepareLiquidityPoolKit } from './exos/liquidity-pool.js'; | ||
import { prepareSettler } from './exos/settler.js'; | ||
import { prepareStatusManager } from './exos/status-manager.js'; | ||
import { prepareTransactionFeedKit } from './exos/transaction-feed.js'; | ||
|
@@ -43,7 +47,11 @@ export const contract = async (zcf, privateArgs, zone, tools) => { | |
assert(tools, 'no tools'); | ||
const terms = zcf.getTerms(); | ||
assert('USDC' in terms.brands, 'no USDC brand'); | ||
assert('PoolShares' in terms.brands, 'no PoolShares brand'); | ||
|
||
const { makeRecorderKit } = prepareRecorderKitMakers( | ||
zone.mapStore('vstorage'), | ||
turadg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
privateArgs.marshaller, | ||
); | ||
|
||
const statusManager = prepareStatusManager(zone); | ||
const makeSettler = prepareSettler(zone, { statusManager }); | ||
|
@@ -71,8 +79,20 @@ export const contract = async (zcf, privateArgs, zone, tools) => { | |
} | ||
}, | ||
}); | ||
const makeLiquidityPoolKit = prepareLiquidityPoolKit( | ||
zone, | ||
zcf, | ||
terms.brands.USDC, | ||
{ makeRecorderKit }, | ||
); | ||
|
||
const creatorFacet = zone.exo('Fast USDC Creator', undefined, {}); | ||
const creatorFacet = zone.exo('Fast USDC Creator', undefined, { | ||
simulateFeesFromAdvance(amount, payment) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. might be worth a louder call to remove There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What will the interfaces be for the advancer? Would be great to see them here. It will need to:
|
||
console.log('🚧🚧 UNTIL: advance fees are implemented 🚧🚧'); | ||
// eslint-disable-next-line no-use-before-define | ||
return poolKit.feeSink.receive(amount, payment); | ||
}, | ||
}); | ||
|
||
const publicFacet = zone.exo('Fast USDC Public', undefined, { | ||
// XXX to be removed before production | ||
|
@@ -95,8 +115,46 @@ export const contract = async (zcf, privateArgs, zone, tools) => { | |
return 'noop; evidence was pushed in the invitation maker call'; | ||
}, 'noop invitation'); | ||
}, | ||
makeDepositInvitation() { | ||
// eslint-disable-next-line no-use-before-define | ||
return poolKit.public.makeDepositInvitation(); | ||
}, | ||
makeWithdrawInvitation() { | ||
// eslint-disable-next-line no-use-before-define | ||
return poolKit.public.makeWithdrawInvitation(); | ||
}, | ||
getPublicTopics() { | ||
// eslint-disable-next-line no-use-before-define | ||
return poolKit.public.getPublicTopics(); | ||
}, | ||
}); | ||
|
||
// ^^^ Define all kinds above this line. Keep remote calls below. vvv | ||
|
||
// NOTE: Using a ZCFMint is helpful for the usual reasons ( | ||
// synchronous mint/burn, keeping assets out of contract vats, ...). | ||
// And there's just one pool, which suggests building it with zone.exo(). | ||
// | ||
// But zone.exo() defines a kind and | ||
// all kinds have to be defined before any remote calls, | ||
// such as the one to the zoe vat as part of making a ZCFMint. | ||
// | ||
// So we use zone.exoClassKit above to define the liquidity pool kind | ||
// and pass the shareMint into the maker / init function. | ||
|
||
const shareMint = await provideSingleton( | ||
zone.mapStore('mint'), | ||
'PoolShare', | ||
() => | ||
zcf.makeZCFMint('PoolShares', AssetKind.NAT, { | ||
decimalPlaces: 6, | ||
}), | ||
); | ||
|
||
const poolKit = zone.makeOnce('Liquidity Pool kit', () => | ||
makeLiquidityPoolKit(shareMint, privateArgs.storageNode), | ||
); | ||
|
||
return harden({ creatorFacet, publicFacet }); | ||
}; | ||
harden(contract); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
assertPoolBalance
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
assertFoo(x)
usually means: assert thatx
is aFoo
.The
atomicRearrange
s that follow the call todepositCalc
/withdrawCalc
assume that the pool balance represented by thepoolSeat
USDC allocation is the same as the pool balance represented by the numerator ofshareWorth
. When I first added this check, my tests failed; they differ by the dust used to make the initial shareworth denominator non-zero.The fact that this answer didn't come in the form of a pointer to existing docs says I should add more.
In flight in what sense? Its only use is in normal straight-line synchronous offer handlers: gather all the relevant info in one place and compute the outcome, synchronously. Things happening outside this vat may happen causally before or after, or in an incomparable causal order. In any case, the local computation does what it does.