-
Notifications
You must be signed in to change notification settings - Fork 214
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: fusdc liquidity pool borrow and repay facets #10474
Conversation
37d455f
to
1673c45
Compare
30c1f02
to
c2ad998
Compare
Deploying agoric-sdk with Cloudflare Pages
|
6900cb1
to
1d6c5e8
Compare
165b3d7
to
99707ef
Compare
1d6c5e8
to
b0ec75a
Compare
06ca4dd
to
31a6f68
Compare
98844c6
to
3a73737
Compare
3ad5d52
to
a1d931d
Compare
// XXX COMMIT POINT? | ||
const { USDC: paymentP } = await withdrawFromSeat(zcf, poolSeat, { | ||
USDC: amount, | ||
}); |
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.
as discussed, rather than dealing with payments here, let's have the caller supply a seat.
The advancer can make a temp seat.
Then I think we can avoid turn boundaries in borrow
and repay
.
cd0c2bf
to
3771e7f
Compare
1f0314a
to
d91c88a
Compare
033e2ea
to
5da5aad
Compare
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.
this is before the fixups
const virtualTotal = add(add(available, dust), outstandingLends); | ||
isEqual(virtualTotal, shareWorth.numerator) || |
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.
const virtualTotal = add(add(available, dust), outstandingLends); | |
isEqual(virtualTotal, shareWorth.numerator) || | |
const encumberedBalance = add(add(available, dust), outstandingLends); | |
isEqual(encumberedBalance, shareWorth.numerator) || |
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.
checkPoolBalance
docstring needs updating
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.
Since encumbered and unencumbered balance are mutually exclusive, I don't think encumberedBalance
is a good name here.
I did rename outstandingLends
to encumberedBalance
and use unencumberedBalance
to represent the current poolSeat allocation. I'm calling the sum of encumbered and unencumbered grossTotal
, but am open to additional suggestions.
const available = poolSeat.getAmountAllocated('USDC', USDC); | ||
const dust = makeDust(USDC); | ||
isEqual(add(available, dust), shareWorth.numerator) || | ||
const virtualTotal = add(add(available, dust), outstandingLends); | ||
isEqual(virtualTotal, shareWorth.numerator) || | ||
Fail`🚨 pool balance ${q(available)} inconsistent with shareWorth ${q(shareWorth)}`; |
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.
I wonder if this should crash the vat. (out of scope of this PR, I suppose)
* @param {Brand<'nat'>} USDC | ||
* @param {object} powers | ||
* @param {ZCF} powers.zcf | ||
* @param {{brand: Brand<'nat'>; issuer: Issuer<'nat'>;}} powers.usdc |
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.
I don't see any use of usdc.issuer
. Why this change?
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.
vestigial, removed
/** | ||
* @param {Amount<'nat'>} amount | ||
* @param {Payment<'nat'>} payment | ||
* @param {ZCFSeat} assetManagerSeat |
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.
assetManagerSeat
seems like a left-over.
* @param {ZCFSeat} assetManagerSeat | |
* @param {ZCFSeat} toSeat |
return { | ||
shareMint, | ||
shareWorth, | ||
contractSeat, |
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.
It seems like there are lots of seats in this contract, so the name contractSeat
doesn't help much. I had to look at the rest of the code to see what this one is for.
We've used feeSeat
in other contracts. There's more than one kind of fee here, but I think I still prefer it.
receive: M.call(AmountShape, PaymentShape).returns(M.promise()), | ||
borrower: M.interface('borrower', { | ||
lookupBalance: M.call().returns(AmountShape), | ||
borrow: M.call(SeatShape, AmountKeywordRecordShape).returns(), |
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.
The method implementation assumes the keywords are right but this pattern doesn't ensure it.
Since the USDC brand is in scope, the pattern can constrain that too.
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.
Good suggestion, done
assetManagerSeat, | ||
poolSeat, | ||
rest, | ||
{ USDC: add(amounts.PoolFee, amounts.Principal) }, |
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.
Add a test where the fromSeat
doesn't have enough to cover these amounts, please? I think the atomicRearrange
will fail, taking the contract down with it.
It looks like we should push this add()
down into repayCalc
. And repayCalc
should get the seat allocation and the amounts. This isn't a zoe offer handler, so we shouldn't rely on the caller to ensure that the seat has enough.
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.
Please see "repay fails when amounts do not match seat allocation" and "repay fails when seat allocation does not equal amounts".
I did not push the add()
into repayCalc
, but believe we can confidence atomicRearrange
will not fail. There are additional tests that show the AKWR guards working as expected.
// Consumers of this .write() are off-chain / outside the VM. | ||
// And there's no way to recover from a failed write. | ||
// So don't await. | ||
void recorder.write(shareWorth); | ||
void recorder.write( | ||
/** @type {PoolMetrics} */ ({ |
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.
Why the cast here?
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.
removed
void recorder.write(shareWorth); | ||
void recorder.write( | ||
/** @type {PoolMetrics} */ ({ | ||
availableBalance: poolSeat.getAmountAllocated('USDC', usdc.brand), |
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.
I wonder if that's a slight misnomer. If the poolSeat allocation is 1 USDC, you can't borrow all of it. The poolSeat can never go empty.
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.
Per our convo, we'll publish encumberedBalance
in PoolMetrics
instead of availableBalance
const repayPayments = {}; | ||
for (const [kw, amount] of Object.entries(splits)) { | ||
const pmt = await utils.pourPayment(amount); | ||
repayPayments[kw] = pmt; | ||
} |
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.
This should avoid the @ts-expect-error
below:
const repayPayments = {}; | |
for (const [kw, amount] of Object.entries(splits)) { | |
const pmt = await utils.pourPayment(amount); | |
repayPayments[kw] = pmt; | |
} | |
const repayPayments = await deeplyFulfilledObject(objectMap(splits, utils.pourPayment)); |
3af5251
to
b699b96
Compare
- factors existing makeLP test helper for reuse in other tests
78b4812
to
a78e88c
Compare
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.
isn't the encumbered balance the pool seat allocation minus outstanding borrows?
const proposalShapes = makeProposalShapes({ USDC, PoolShares }); | ||
const proposalShapes = makeProposalShapes({ | ||
USDC, | ||
PoolShares, | ||
}); |
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.
nit: superfluous whitespace change?
/** used for `checkPoolBalance` invariant */ | ||
const encumberedBalance = makeEmpty(USDC); |
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.
Perhaps expand to say: poolSeat USDC allocation plus outstanding borrows.
I wonder if this docstring would get associated with state.encumberedBalance
if it were moved down to the return
statement.
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.
Perhaps expand to say: poolSeat USDC allocation plus outstanding borrows. aka outstanding borrows
* @param {Amount<'nat'>} poolSeatAllocation | ||
* @param {Amount<'nat'>} encumberedBalance | ||
* @param {PoolStats} poolStats | ||
* @throws {Error} if conditions are not met |
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.
Let's either say what conditions or skip this @throws
.
* @throws {Error} if conditions are not met | |
* @throws {Error} if too much is requested |
!isGTE(amounts.Principal, encumberedBalance) || | ||
isEqual(amounts.Principal, encumberedBalance) || |
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.
why test both >= and = except to give separate errors?
if x = y, then x >= y is true, and !(x >= y) will fail. The isEqual
branch can never be reached.
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.
The isEqual branch can never be reached.
oops. this is an or.
but (!(x >= y) || x = y)
is just y >= x
, yes? so we can shorten this to isGTE(encumberedBalance, amounts.Principal)
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.
Thanks, amended to simplify. I think soon™️ I'll have the hang of AmountMath
. Looking at the source, it's interesting to see virtually every comparator is implemented with isGTE
.
Fail`Cannot repay. Principal ${q(amounts.Principal)} exceeds encumberedBalance ${q(encumberedBalance)}.`; | ||
|
||
return harden({ | ||
shareWorth: withFees(shareWorth, amounts.PoolFee), |
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.
maybe withFees
should be inlined... or at least not exported any more.
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.
Agree, but there's an existing unit test that relies on it so I've left the export for now
// LPs can still withdraw (contract did not shutdown) | ||
await lps.alice.withdraw(0.5); |
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.
👏
encumberedBalance: { | ||
value: 0n, | ||
}, |
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.
how did the encumberedBalance
get to zero? and if it's zero, how can alice withdraw below?
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.
We expect 0n
here since all borrows have been returned
* Verifies that the total pool balance (unencumbered + encumbered) matches the | ||
* shareWorth numerator. The total pool balance consists of: | ||
* 1. unencumbered balance - USDC available in the pool for borrowing | ||
* 2. encumbered balance - USDC currently lent out |
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.
isn't the encumbered balance the pool seat allocation minus outstanding borrows?
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.
ok. after discussion... the docs here are right.
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.
some minor comments / suggestions
Sorry some of them were based on confusion.
strongest suggestion is to rewrite (!(x >= y) || x = y)
as y >= x
in pool-share-math
a78e88c
to
3117eef
Compare
refs: #10390
Description
borrower
andrepayer
facets to theLiquidity Pool
outstandingLends: Amount
in state forcheckPoolBalance
invariantPoolMetrics
like cumulative borrows, repays, and feesSecurity Considerations
Handles payment allocations, but is able to do so synchronously using a temporary seats provided by the callers.
Scaling Considerations
Makes a vstorage writes for each borrow and repay.
Documentation Considerations
Updates code comments for maintainers.
Testing Considerations
Tests use test-only methods on the createFacet to simulate fees from a borrow/repay sequence. Tests ensure
zcf.atomicRearrange()
in.borrow()
and.repay()
will not fail and triggerzcf.shutdownWithFailure()
.Upgrade Considerations
None, unreleased