Skip to content

fix(contracts): unify VaultRouter <-> tier-vault ABI and asset-scope accounting - #146

Merged
wumibals merged 1 commit into
LadderMine:mainfrom
soundsng:feature/138-vault-abi-asset-scoping
Aug 21, 2026
Merged

fix(contracts): unify VaultRouter <-> tier-vault ABI and asset-scope accounting#146
wumibals merged 1 commit into
LadderMine:mainfrom
soundsng:feature/138-vault-abi-asset-scoping

Conversation

@soundsng

Copy link
Copy Markdown
Contributor

Summary

Foundation issue for the payment track (1 of 8): VaultRouter already forwards deposit/withdraw/early_exit/balance/shares/lock_until using an asset-scoped (user, asset, ...) signature, but none of the four tier vaults (VaultFlex, VaultL3, VaultL6, VaultL12) implemented it — they still took (user, amount) with single-asset, non-asset-scoped storage. Real (non-mock) deposit/withdraw/early_exit calls against the actual vaults would fail outright.

Changes

  • Asset-scoped every tier vault's storage: Balance, Shares, LockUntil, Checkpoint (and VaultL3's EmergencyUnlock) are now keyed by (user, asset) instead of user alone. shared::checkpoint is asset-scoped the same way.
  • Standardized deposit(user, asset, amount) / withdraw(...) -> i128 / early_exit(...) -> i128 / balance / shares / lock_until across all four vaults, matching what VaultRouter already calls.
  • relock was still (user)-only on both the router and the locked vaults — ambiguous once LockUntil is asset-scoped, since a vault can't tell which asset's lock to renew. Added an asset parameter to both sides (router + VaultL3/L6/L12), and updated the router's own MockVault-based tests accordingly.
  • Removed a duplicate token transfer inside each tier vault's deposit(). VaultRouter already moves tokens from the user to the vault before invoking it; the vault's own additional user -> strategy transfer (which also hardcoded the tier's original single USDC address regardless of the asset parameter) double-charged the user and moved the wrong token for any non-USDC asset. Tier vaults are now bookkeeping-only — VaultRouter owns all token movement. This is what actually makes multi-asset deposits work, not just the argument-count fix. See the new test_deposit_moves_funds_exactly_once_per_asset regression test.
  • VaultFlex: added partial withdrawal (previously only full withdrawal existed), plus early_exit/lock_until for ABI parity with the locked tiers. Its previous pro-rata yield payout depended on an externally supplied strategy_balance argument the router can no longer provide under the unified signature, and StrategyVault pools capital from all tier vaults without attributing balance back to a single caller — there's no accurate callback available without a StrategyVault redesign, which is out of scope here. VaultFlex now tracks its own principal balance like the locked tiers (documented on VaultFlex::withdraw).
  • set_max_tvl/max_tvl/remaining_capacity are intentionally left as-is (single global cap per vault, not asset-scoped) — VaultRouter's existing set_max_tvl/vault_capacity calls don't pass an asset either, and this is outside the acceptance criteria for this issue.
  • New integration tests in vault_router/tests/real_vaults_integration.rs exercise VaultRouter against the real tier vault contracts (not the unit-test MockVault) across two allowlisted assets (a real Stellar Asset Contract test token for each), including the double-transfer regression test above and a same-tier-two-assets independence test.
  • Added unit tests to all four tier vaults (previously had zero test coverage) and to shared::checkpoint.
  • CHANGELOG.md entry.

Test plan

CI on this repo only runs cargo build --target wasm32-unknown-unknown --release for contracts (no cargo test gate). This Windows dev box has no Windows SDK installed, so I could not run cargo test/cargo build natively here (even linker-free cargo check fails because dependercies' build scripts still need a working host linker) — I reviewed the changes carefully by hand instead and am relying on CI (Ubuntu, proper toolchain) to verify the build. Please let CI run and flag me if anything fails.

  • cargo build --target wasm32-unknown-unknown --release (CI gate)
  • Manual review of all signature changes against every call site (VaultRouter, tests, shared::checkpoint callers)
  • New tests cover: per-tier deposit/withdraw/early_exit for 2 assets end-to-end against real vaults, the double-transfer regression, asset-independence within one tier, VaultFlex partial withdrawal, emergency-unlock per-asset scoping, and the full state-machine per vault

Closes #138

…accounting

VaultRouter already called deposit/withdraw/early_exit/balance/shares/
lock_until with an asset-scoped (user, asset, ...) signature, but none of
the four tier vaults (VaultFlex, VaultL3, VaultL6, VaultL12) implemented
it — they still took (user, amount) with single-asset global storage keys,
so any real (non-mock) deposit/withdraw/early_exit call would fail.

- Asset-scope every tier vault's storage: Balance, Shares, LockUntil,
  Checkpoint (and VaultL3's EmergencyUnlock) are now keyed by (user, asset)
  instead of user alone, so two assets held by the same user in the same
  tier no longer share state. shared::checkpoint is asset-scoped the same
  way, since it's only ever used per-position.
- Standardize deposit(user, asset, amount) / withdraw(...) -> i128 /
  early_exit(...) -> i128 / balance / shares / lock_until across all four
  vaults, matching what VaultRouter already calls.
- relock was still (user) on both the router and the locked vaults, which
  becomes ambiguous once LockUntil is asset-scoped; added an asset
  parameter to both sides.
- Removed a duplicate token transfer inside each tier vault's deposit():
  VaultRouter already moves tokens from user to vault before invoking it,
  so the vault's own second user -> strategy transfer (hardcoded to the
  tier's original single USDC address regardless of the asset parameter)
  double-charged the user and moved the wrong token for any non-USDC
  asset. Tier vaults are now bookkeeping-only; VaultRouter owns all token
  movement — this is also what makes real multi-asset deposits work at
  all rather than just fixing the argument count.
- VaultFlex: added partial withdrawal (previously only full withdrawal
  existed), plus early_exit/lock_until for ABI parity with the locked
  tiers. Its previous pro-rata yield payout depended on an externally
  supplied strategy_balance argument the router can no longer provide
  under the unified signature, and StrategyVault has no way to attribute
  its pooled balance back to a single calling vault, so Flex now tracks
  its own principal balance like the locked tiers (documented on
  VaultFlex::withdraw).
- New integration tests in vault_router/tests/ exercise VaultRouter
  against the real tier vault contracts (not the unit-test MockVault)
  across two allowlisted assets, including a regression test for the
  double-transfer bug above.

Closes LadderMine#138
@vercel

vercel Bot commented Aug 21, 2026

Copy link
Copy Markdown

@soundsng is attempting to deploy a commit to the wumibals' projects Team on Vercel.

A member of the Team first needs to authorize it.

@wumibals wumibals left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed the VaultRouter <-> tier-vault ABI unification (1/8 in the payment track).

  • Asset-scoping Balance/Shares/LockUntil/Checkpoint (and EmergencyUnlock on L3) by (user, asset) instead of user alone is the correct fix — the router already spoke asset-scoped, the vaults didn't, so real (non-mock) multi-asset calls were broken.
  • The standout fix is removing the duplicate user -> strategy transfer inside each vault's deposit(). That transfer both double-charged the user and hardcoded the tier's original single-USDC address regardless of the asset param — silently moving the wrong token for any non-USDC asset. Making tier vaults bookkeeping-only, with VaultRouter as sole owner of token movement, is the right architectural boundary and is exactly what actually enables multi-asset deposits (not just the signature change). Good catch, and good that there's now a dedicated regression test for it.
  • Adding asset to relock on both router and locked vaults closes a real ambiguity that would've existed once LockUntil became asset-scoped.
  • VaultFlex tracking its own principal balance instead of depending on an external strategy_balance callback is a reasonable, well-documented workaround given StrategyVault doesn't attribute balance per caller — and correctly scoped as out-of-bounds for a StrategyVault redesign here.
  • Leaving max_tvl/remaining_capacity un-scoped is consistent with VaultRouter's existing behavior and correctly called out as outside this issue's acceptance criteria.
  • New integration tests against the real (non-mock) tier vaults, across two assets, covering the double-transfer regression and same-tier asset independence, are a meaningful addition — this repo's four tier vaults had zero test coverage before this PR.

CI (Soroban contracts build, Next.js dashboard, TypeScript SDK) is green; the Vercel status failure is an unauthorized deployment integration link, unrelated to the code.

One thing worth tracking as a fast follow: CI here only runs cargo build, not cargo test, so the new test suite's pass/fail hasn't been machine-verified yet (noted in the PR's own test plan) — worth wiring cargo test into CI given this code moves real funds. Not a blocker for this PR, but flagging for the next issue in the track.

Approving — good foundational fix for the payment track.

@wumibals
wumibals merged commit 5772c6c into LadderMine:main Aug 21, 2026
3 of 4 checks passed
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.

Payments 1/8: Fix VaultRouter <-> tier-vault ABI mismatch and unify asset-scoped accounting

2 participants