-
Notifications
You must be signed in to change notification settings - Fork 215
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
Conversation
Deploying agoric-sdk with Cloudflare Pages
|
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.
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 {
{ | ||
want: M.splitRecord( | ||
{}, | ||
{ PoolShare: makeNatAmountShape(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.
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?
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'm struggling to understand the change. It adds a 3rd arg to splitRecord(...)
? What does that do? (looking it up...)
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 adds a 3rd arg to splitRecord(...)? What does that do?
I think each arg in M.splitRecord({}, {PoolShares: ... }, {});
says:
- can be an empty object
- if PoolShares is present, the value must be a USDC amount shape
- no other keys allowed
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... I see rest
in splitRecord docs.
I wonder how many other places I have missed this.
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.
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.
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.
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) |
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.
// (https://github.com/Agoric/agoric-private/issues/234) |
let's avoid pointers from public stuff to private stuff
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.
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) |
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.
// (https://github.com/Agoric/agoric-private/issues/234) |
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.
Done
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 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) |
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.
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) |
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.
Done
{ | ||
want: M.splitRecord( | ||
{}, | ||
{ PoolShare: makeNatAmountShape(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.
If we make the { want: { PoolShare: ... }} part required, we can probably simplify some code that consumes it.
Done
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 catch
This pull request has been removed from the queue for the following reason: 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 |
❌ This pull request head commit has not been previously disembarked from queue. |
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:
agoric-sdk/packages/fast-usdc/src/exos/liquidity-pool.js
Lines 272 to 285 in ca25dd5
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