Skip to content
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

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

0xpatrickdev
Copy link
Member

@0xpatrickdev 0xpatrickdev commented Nov 13, 2024

refs: #10390

Description

  • Adds borrower and repayer facets to the Liquidity Pool
  • Track outstandingLends: Amount in state for checkPoolBalance invariant
  • Track PoolMetrics like cumulative borrows, repays, and fees

Security 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 trigger zcf.shutdownWithFailure().

Upgrade Considerations

None, unreleased

Copy link

cloudflare-workers-and-pages bot commented Nov 13, 2024

Deploying agoric-sdk with  Cloudflare Pages  Cloudflare Pages

Latest commit: a78e88c
Status: ✅  Deploy successful!
Preview URL: https://e5b75d24.agoric-sdk.pages.dev
Branch Preview URL: https://pc-fusdc-advancer-with-lp.agoric-sdk.pages.dev

View logs

@0xpatrickdev 0xpatrickdev force-pushed the pc/fusdc-advancer branch 3 times, most recently from 165b3d7 to 99707ef Compare November 15, 2024 17:20
Base automatically changed from pc/fusdc-advancer to master November 15, 2024 18:07
@0xpatrickdev 0xpatrickdev changed the base branch from master to pc/fusdc-fees November 15, 2024 18:09
@0xpatrickdev 0xpatrickdev force-pushed the pc/fusdc-advancer-with-lp branch 2 times, most recently from 98844c6 to 3a73737 Compare November 15, 2024 18:23
@0xpatrickdev 0xpatrickdev force-pushed the pc/fusdc-fees branch 4 times, most recently from 3ad5d52 to a1d931d Compare November 15, 2024 20:23
Comment on lines 154 to 157
// XXX COMMIT POINT?
const { USDC: paymentP } = await withdrawFromSeat(zcf, poolSeat, {
USDC: amount,
});
Copy link
Member

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.

@0xpatrickdev 0xpatrickdev force-pushed the pc/fusdc-fees branch 2 times, most recently from cd0c2bf to 3771e7f Compare November 18, 2024 15:02
Base automatically changed from pc/fusdc-fees to master November 18, 2024 17:11
@0xpatrickdev 0xpatrickdev force-pushed the pc/fusdc-advancer-with-lp branch 2 times, most recently from 1f0314a to d91c88a Compare November 18, 2024 20:45
@0xpatrickdev 0xpatrickdev changed the title feat: fusdc advancer integrates with liquidity pool feat: fusdc liquidity pool borrow and repay facets Nov 18, 2024
@0xpatrickdev 0xpatrickdev marked this pull request as ready for review November 18, 2024 20:52
@0xpatrickdev 0xpatrickdev requested a review from a team as a code owner November 18, 2024 20:52
Copy link
Member

@dckc dckc left a 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

Comment on lines 52 to 59
const virtualTotal = add(add(available, dust), outstandingLends);
isEqual(virtualTotal, shareWorth.numerator) ||
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const virtualTotal = add(add(available, dust), outstandingLends);
isEqual(virtualTotal, shareWorth.numerator) ||
const encumberedBalance = add(add(available, dust), outstandingLends);
isEqual(encumberedBalance, shareWorth.numerator) ||

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

checkPoolBalance docstring needs updating

Copy link
Member Author

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)}`;
Copy link
Member

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
Copy link
Member

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?

Copy link
Member Author

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
Copy link
Member

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.

Suggested change
* @param {ZCFSeat} assetManagerSeat
* @param {ZCFSeat} toSeat

return {
shareMint,
shareWorth,
contractSeat,
Copy link
Member

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(),
Copy link
Member

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.

Copy link
Member Author

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) },
Copy link
Member

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.

Copy link
Member Author

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} */ ({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the cast here?

Copy link
Member Author

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),
Copy link
Member

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.

Copy link
Member Author

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

Comment on lines 234 to 238
const repayPayments = {};
for (const [kw, amount] of Object.entries(splits)) {
const pmt = await utils.pourPayment(amount);
repayPayments[kw] = pmt;
}
Copy link
Member

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:

Suggested change
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));

@0xpatrickdev 0xpatrickdev force-pushed the pc/fusdc-advancer-with-lp branch 2 times, most recently from 3af5251 to b699b96 Compare November 19, 2024 15:38
- factors existing makeLP test helper for reuse in other tests
@0xpatrickdev 0xpatrickdev force-pushed the pc/fusdc-advancer-with-lp branch 2 times, most recently from 4ce61a6 to 78b4812 Compare November 19, 2024 15:53
Copy link
Member

@dckc dckc left a 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?

Comment on lines -92 to +127
const proposalShapes = makeProposalShapes({ USDC, PoolShares });
const proposalShapes = makeProposalShapes({
USDC,
PoolShares,
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: superfluous whitespace change?

Comment on lines +135 to +136
/** used for `checkPoolBalance` invariant */
const encumberedBalance = makeEmpty(USDC);
Copy link
Member

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.

Copy link
Member

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
Copy link
Member

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.

Suggested change
* @throws {Error} if conditions are not met
* @throws {Error} if too much is requested

Comment on lines +176 to +177
!isGTE(amounts.Principal, encumberedBalance) ||
isEqual(amounts.Principal, encumberedBalance) ||
Copy link
Member

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.

Copy link
Member

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)

Fail`Cannot repay. Principal ${q(amounts.Principal)} exceeds encumberedBalance ${q(encumberedBalance)}.`;

return harden({
shareWorth: withFees(shareWorth, amounts.PoolFee),
Copy link
Member

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.

Comment on lines +341 to +342
// LPs can still withdraw (contract did not shutdown)
await lps.alice.withdraw(0.5);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👏

Comment on lines +500 to +502
encumberedBalance: {
value: 0n,
},
Copy link
Member

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?

* 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
Copy link
Member

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?

Copy link
Member

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.

Copy link
Member

@dckc dckc left a 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants