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

fix(fast-usdc): recover from bad lp proposal #10787

Merged
merged 1 commit into from
Jan 2, 2025
Merged

fix(fast-usdc): recover from bad lp proposal #10787

merged 1 commit into from
Jan 2, 2025

Conversation

samsiegart
Copy link
Contributor

@samsiegart samsiegart commented Dec 31, 2024

closes https://github.com/Agoric/agoric-private/issues/234

Description

See issue for context. It also seems related to #10684 because the code path that was triggering the bad state was this:

try {
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
[mint, lp, post.payouts],
]),
);
} catch (cause) {
// UNTIL #10684: ability to terminate an incarnation w/o terminating the contract
throw new Error('🚨 cannot commit deposit', { cause });
} finally {

As you can see, we were updating the pool state, but then the atomicRearrange failed, so the pool state was left invalid. This PR makes it so that the bad proposal shape is caught by the type guard earlier, so this code path never happens.

The withdraw path was already being handled correctly because the typeguard was specific enough.

If the proposal shape is correct, but the amounts are incorrect, the contract already handles that fine by failing before the state update.

Security Considerations

The bug would allow anyone to send an offer that breaks the liquidity pool.

Scaling Considerations

None

Documentation Considerations

None

Testing Considerations

Added a bootstrap test that fails accordingly without the fix.

Upgrade Considerations

None

@samsiegart samsiegart requested a review from a team as a code owner December 31, 2024 00:28
Copy link

cloudflare-workers-and-pages bot commented Dec 31, 2024

Deploying agoric-sdk with  Cloudflare Pages  Cloudflare Pages

Latest commit: 21d19ba
Status: ✅  Deploy successful!
Preview URL: https://406d5d01.agoric-sdk.pages.dev
Branch Preview URL: https://srs-bad-lp.agoric-sdk.pages.dev

View logs

Copy link
Member

@0xpatrickdev 0xpatrickdev left a comment

Choose a reason for hiding this comment

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

LGTM. I'll hold approval to give @dckc an opportunity to review as well.

I wonder if we should consider updating the deposit and withdraw handlers:

diff --git a/packages/fast-usdc/src/exos/liquidity-pool.js b/packages/fast-usdc/src/exos/liquidity-pool.js
index 5df4c86bbf..791a68a9ca 100644
--- a/packages/fast-usdc/src/exos/liquidity-pool.js
+++ b/packages/fast-usdc/src/exos/liquidity-pool.js
@@ -257,15 +257,19 @@ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => {
       depositHandler: {
         /** @param {ZCFSeat} lp */
         async handle(lp) {
-          const { shareWorth, shareMint, poolSeat, encumberedBalance } =
-            this.state;
+          const {
+            shareWorth: currShareWorth,
+            shareMint,
+            poolSeat,
+            encumberedBalance,
+          } = this.state;
           const { external } = this.facets;
 
           /** @type {USDCProposalShapes['deposit']} */
           // @ts-expect-error ensured by proposalShape
           const proposal = lp.getProposal();
-          checkPoolBalance(poolSeat, shareWorth, USDC, encumberedBalance);
-          const post = depositCalc(shareWorth, proposal);
+          checkPoolBalance(poolSeat, currShareWorth, USDC, encumberedBalance);
+          const post = depositCalc(currShareWorth, proposal);
 
           // COMMIT POINT
           const mint = shareMint.mintGains(post.payouts);
@@ -280,6 +284,7 @@ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => {
               ]),
             );
           } catch (cause) {
+            this.state.shareWorth = currShareWorth;
             // UNTIL #10684: ability to terminate an incarnation w/o terminating the contract
             throw new Error('🚨 cannot commit deposit', { cause });
           } finally {

Comment on lines 24 to 30
{
want: M.splitRecord(
{},
{ PoolShare: makeNatAmountShape(PoolShares) },
{},
),
},
Copy link
Member

@0xpatrickdev 0xpatrickdev Dec 31, 2024

Choose a reason for hiding this comment

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

Good find. I wonder if this should just be:

{ want: { PoolShare: makeNatAmountShape(PoolShares) } },

But I suppose someone could deposit without wanting shares in return? Edit: if that's the case, would wanting 0 pool shares work?

Copy link
Member

Choose a reason for hiding this comment

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

I'm struggling to understand the change. It adds a 3rd arg to splitRecord(...)? What does that do? (looking it up...)

Copy link
Member

@0xpatrickdev 0xpatrickdev Dec 31, 2024

Choose a reason for hiding this comment

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

It adds a 3rd arg to splitRecord(...)? What does that do?

I think each arg in M.splitRecord({}, {PoolShares: ... }, {}); says:

  1. can be an empty object
  2. if PoolShares is present, the value must be a USDC amount shape
  3. no other keys allowed

Copy link
Member

Choose a reason for hiding this comment

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

OK... I see rest in splitRecord docs.

I wonder how many other places I have missed this.

Copy link
Member

Choose a reason for hiding this comment

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

yes, 0 pool shares would work.

I was trying to avoid burdening callers with getting access to the PoolShares brand when they are OK relying on the contract to figure it out... but that's really not a normal scenario. In normal usage, clients should not rely on the contract to behave nicely. That's sort of the whole point of offer safety.

Let's be sure to keep the type and the typeguard in sync. If we make the { want: { PoolShare: ... }} part required, we can probably simplify some code that consumes it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If we make the { want: { PoolShare: ... }} part required, we can probably simplify some code that consumes it.

Done

const fastLP = agoricNamesRemotes.vbankAsset.FastLP.brand as Brand<'nat'>;

// Send a bad proposal first to make sure it's recoverable.
// (https://github.com/Agoric/agoric-private/issues/234)
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
// (https://github.com/Agoric/agoric-private/issues/234)

let's avoid pointers from public stuff to private stuff

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

const fastLP = agoricNamesRemotes.vbankAsset.FastLP.brand as Brand<'nat'>;

// Send a bad proposal first to make sure it's recoverable.
// (https://github.com/Agoric/agoric-private/issues/234)
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
// (https://github.com/Agoric/agoric-private/issues/234)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

Copy link
Contributor Author

@samsiegart samsiegart left a comment

Choose a reason for hiding this comment

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

I wonder if we should consider updating the deposit and withdraw handlers

I considered this but I figured we were doing all the validation between checkPoolBalance, depositCalc, and type guards, so I didn't want to mess with the intended logic of this try-catch. Maybe it doesn't hurt to restore the state though? WDYT @dckc ?

const fastLP = agoricNamesRemotes.vbankAsset.FastLP.brand as Brand<'nat'>;

// Send a bad proposal first to make sure it's recoverable.
// (https://github.com/Agoric/agoric-private/issues/234)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

const fastLP = agoricNamesRemotes.vbankAsset.FastLP.brand as Brand<'nat'>;

// Send a bad proposal first to make sure it's recoverable.
// (https://github.com/Agoric/agoric-private/issues/234)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

Comment on lines 24 to 30
{
want: M.splitRecord(
{},
{ PoolShare: makeNatAmountShape(PoolShares) },
{},
),
},
Copy link
Contributor Author

Choose a reason for hiding this comment

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

If we make the { want: { PoolShare: ... }} part required, we can probably simplify some code that consumes it.

Done

@samsiegart samsiegart requested a review from dckc December 31, 2024 21:16
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.

Good catch

@samsiegart samsiegart added the automerge:rebase Automatically rebase updates, then merge label Jan 2, 2025
Copy link
Contributor

mergify bot commented Jan 2, 2025

This pull request has been removed from the queue for the following reason: checks failed.

The merge conditions cannot be satisfied due to failing checks:

You should look at the reason for the failure and decide if the pull request needs to be fixed or if you want to requeue it.

If you want to requeue this pull request, you need to post a comment with the text: @mergifyio requeue

@samsiegart
Copy link
Contributor Author

@Mergifyio requeue

Copy link
Contributor

mergify bot commented Jan 2, 2025

requeue

❌ This pull request head commit has not been previously disembarked from queue.

@mergify mergify bot merged commit 7750fde into master Jan 2, 2025
81 checks passed
@mergify mergify bot deleted the srs-bad-lp branch January 2, 2025 03:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
automerge:rebase Automatically rebase updates, then merge
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants