feat(stablecoin): rebuild withdraw_collateral with the collateralization check - #348
Conversation
3a92d45 to
b13e825
Compare
d6108e0 to
17d1634
Compare
There was a problem hiding this comment.
🟡 Changes recommended
There are still doc/test inconsistencies in the modified areas (outdated panic/docs and a test that claims to assert four echoed globals but only checks one), which should be corrected before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR rebuilds the stablecoin program’s withdraw_collateral instruction to follow the spec’s fee-aware, post-decrement collateralization invariant, expanding the ABI to include additional read-only global/state accounts and updating the guest entrypoint, IDL, unit tests, and the zkVM integration test fixtures accordingly.
Changes:
- Replaces the scaffolded “no debt allowed” guard with a spec-aligned collateralization check using projected accumulator/redemption-price values and adds the protocol frozen gate.
- Expands
WithdrawCollateralaccount list from 4 → 8 (adds accumulator, redemption price state, protocol parameters, and clock) and regenerates IDL/guest wiring. - Updates stablecoin unit tests and the integration test harness to seed/pass the new global accounts.
File summaries
| File | Description |
|---|---|
| programs/stablecoin/src/withdraw_collateral.rs | Implements freeze check + post-decrement collateralization enforcement; adds global PDA validation helper; updates destination naming. |
| programs/stablecoin/src/tests.rs | Adds new withdraw-collateral tests and updates existing ones for the 8-account ABI. |
| programs/stablecoin/methods/guest/src/bin/stablecoin.rs | Updates guest instruction signature to accept the four new accounts. |
| programs/stablecoin/core/src/lib.rs | Updates Instruction::WithdrawCollateral docs to reflect the new account list and semantics. |
| programs/integration_tests/tests/stablecoin.rs | Seeds and passes the new global accounts in the e2e test state and call site. |
| artifacts/stablecoin-idl.json | Regenerates IDL to match the new withdraw-collateral ABI (account names/order). |
Review details
Suppressed comments (3)
programs/stablecoin/src/withdraw_collateral.rs:37
- The panic list still claims non-zero debt is rejected, but debt is now allowed as long as the post-withdrawal position remains collateralized. Update this bullet to reflect the actual failure mode.
/// - `Position.normalized_debt_amount` is non-zero.
programs/stablecoin/src/tests.rs:1310
- Test name still uses the old
destinationterminology, but the account is nowuser_collateral_holding. Renaming the test improves clarity and keeps terminology consistent.
STABLECOIN_PROGRAM_ID,
100,
);
}
programs/stablecoin/src/tests.rs:1333
- Test name still uses the old
destinationterminology, but the account is nowuser_collateral_holding. Renaming the test improves clarity and keeps terminology consistent.
protocol_parameters_account(false),
clock_account(NOW),
STABLECOIN_PROGRAM_ID,
100,
);
}
- Files reviewed: 6/6 changed files
- Comments generated: 4
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| assert_eq!(post_states.len(), 8); | ||
| assert_eq!(chained_calls.len(), 1); | ||
| assert_eq!( | ||
| *post_states[7].account(), | ||
| clock_account(NOW).account, | ||
| "clock must be echoed unchanged" | ||
| ); |
| /// `Position.normalized_debt_amount == 0` instead of accruing fees and | ||
| /// checking the collateralization ratio. | ||
| WithdrawCollateral { | ||
| /// Amount of collateral tokens to move from the vault back to `destination`. |
| expected = "Destination token definition does not match the position's collateral definition" | ||
| expected = "User collateral holding definition does not match the position's collateral definition" | ||
| )] | ||
| fn withdraw_collateral_rejects_destination_for_other_definition() { |
| /// Withdraw `amount` collateral tokens from `position`'s vault back to `user_collateral_holding`. | ||
| /// | ||
| /// Decreases `Position.collateral_amount` by `amount` and emits a single chained | ||
| /// `Token::Transfer` from the vault to `destination`, authorized by the vault | ||
| /// `Token::Transfer` from the vault to `user_collateral_holding`, authorized by the vault | ||
| /// PDA seed. The position post-state uses plain [`AccountPostState::new`] — |
Rebuilds
withdraw_collateralto spec §10.6, replacing the Plan 1 scaffold'snormalized_debt_amount == 0placeholder with the real §6.2 collateralizationcheck.
Accounts go 4 → 8, adding
stability_fee_accumulator,redemption_price_state,protocol_parametersandclock, all read-only and echoed unchanged. The checkruns after the decrement, against the accumulator and redemption price both
projected forward to the clock timestamp (§5.3), using the helper from #336.
Also adds the frozen check, which the scaffold never had.
All three globals are validated at their canonical PDAs via a small shared
decode_globalhelper — initialized, program-owned, right address — so the samesubstitution gap Copilot flagged on #341 can't apply here.
8 new tests: the four read-only globals echoed, withdrawal with debt when
collateralization holds, the exact ratio boundary, one unit below it, frozen, and
an uninitialized variant of each global. The scaffold's
rejects_withdrawal_with_outstanding_debttest is removed — it pinned theplaceholder this issue deletes.
The open/withdraw e2e seeds the two new globals and passes all four accounts.
Fourth of eight issues in Plan 3 (#173). Stacked on #345.
closes #177