Skip to content

feat: Securitize collateral vault V2 with hardened share-movement authorization - #432

Open
kasperpawlowski wants to merge 1 commit into
developmentfrom
feat/securitize-collateral-vault-v2
Open

kasperpawlowski wants to merge 1 commit into
developmentfrom
feat/securitize-collateral-vault-v2

Conversation

@kasperpawlowski

@kasperpawlowski kasperpawlowski commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

Introduces ERC4626EVCCollateralSecuritizeV2 and ERC4626EVCCollateralSecuritizeFactoryV2, superseding the v1 Securitize collateral vault/factory with two authorization fixes and versioning the factory so existing deployments are untouched.

The v1 vault and factory remain immutable on-chain and are still referenced by the existing securitizeFactory address-book key. New deployments use a new securitizeFactoryV2 key; existing per-chain address books need no edits (a missing key deserializes to address(0)).

Fixes

Both harden the vault's "value only moves within an EVC account family, except a genuine liquidation" invariant. They are motivated by bug-bounty findings Cantina #604 and Cantina #592.

Cross-frame control-collateral authorization (Cantina #604)

_requireTransferAuthorized now binds the transfer from to the account authenticated for the current EVC frame (from == _msgSender()).

Previously the cross-owner branch checked only that a controlCollateral frame was in progress and that getControllers(from)[0] was perspective-verified — it never required from to be the account that opened the frame. An allowance holder could open a controlCollateral frame for their own account (with a trivial self-installed controller) and forward transferFrom(victim, attacker, amount), borrowing an unrelated victim's genuine verified controller to move the victim's shares cross-owner. The EVK liquidation path uses transfer (where from == _msgSender() by construction) and is unaffected.

Withdraw/redeem receiver binding (Cantina #592)

The V2 vault overrides the public withdraw and redeem (mirroring the existing deposit/mint overrides) to require the underlying payout receiver to share the share owner's EVC account family (isCommonOwner(owner, receiver); otherwise NotAuthorized).

Previously withdraw/redeem inherited generic ERC-4626 receiver semantics, so an owner could route the underlying to an unrelated (still eligible) account. Because the vault is a registered platform wallet, that substitution removes the original holder from the source-side issuance hold-up evaluation. Liquidation and governor seizure move shares (not the underlying) to the recipient, which then redeems its own shares to its own family, so those flows are unaffected (verified by tests).

Deployment scripts

  • ScriptUtils PeripheryAddresses keeps securitizeFactory (v1, read-only) and adds securitizeFactoryV2 (struct + serialize + deserialize).
  • 50_CoreAndPeriphery deploys the V2 factory under the securitizeFactoryV2 key behind a deploySecuritizeFactoryV2 flag; the v1 factory is never redeployed.
  • interactiveDeployment.sh keeps the v1 loader and adds the v2 loader/prompt/compile/input; verifyContracts.sh verifies …FactoryV2.

Test plan

  • forge test --match-path test/Vault/ERC4626EVCCollateralSecuritize.t.sol — 13/13 pass
  • New regression tests: cross-frame steal reverts; withdraw/redeem to an unrelated receiver reverts
  • Liquidation/seizure unaffected: a liquidation succeeds and the liquidator redeems its seized shares to its own family (but not to an unrelated one); governor seize succeeds and the recipient redeems to its own family
  • Existing testCollateralSecuritizeVault_liquidate and ..._freeze remain green (no regression to legitimate liquidation or freeze paths)

Notes

  • The fixes apply to future V2 deployments; the already-deployed v1 vaults are immutable and out of scope for a code change.
  • Only the factory and vault are versioned to V2; the ancillary IDSToken / compliance interfaces are unchanged.

@kasperpawlowski
kasperpawlowski force-pushed the feat/securitize-collateral-vault-v2 branch from 03627ef to 2792b92 Compare September 1, 2026 11:51
…horization

Introduces ERC4626EVCCollateralSecuritizeV2 and its factory
ERC4626EVCCollateralSecuritizeFactoryV2, superseding the v1 Securitize
collateral vault/factory with two authorization fixes. The v1 vault and factory
remain immutable on-chain and are still referenced by the existing
`securitizeFactory` address-book key; new deployments use a new
`securitizeFactoryV2` key.

Fixes (relative to the audited v1):

- Cross-frame control-collateral authorization (Cantina #604):
  _requireTransferAuthorized now binds the transfer `from` to the account
  authenticated for the current EVC frame (from == _msgSender()). Previously the
  guard checked only that a controlCollateral frame was in progress and that
  getControllers(from)[0] was perspective-verified, without requiring `from` to
  be the account that opened the frame. An allowance holder could open a
  controlCollateral frame for their own account and forward
  transferFrom(victim, attacker, amount), borrowing an unrelated victim's
  verified controller to move the victim's shares cross-owner. The legitimate
  liquidation path uses transfer (from == _msgSender by construction) and is
  unaffected.

- Withdraw/redeem receiver binding (Cantina #592):
  overrides the public `withdraw` and `redeem` (mirroring the `deposit`/`mint`
  overrides) to require the underlying payout receiver to share the share
  owner's EVC account family (isCommonOwner(owner, receiver)). Previously
  withdraw/redeem inherited generic ERC-4626 receiver semantics, letting an
  owner route the underlying to an unrelated (still eligible) account and,
  because the vault is a registered platform wallet, bypass the source-side
  issuance hold-up. Liquidation and governor seizure move shares (not the
  underlying) to the recipient, which redeems its own shares to its own family,
  so those flows are unaffected (covered by regression tests).

Deployment:

- ScriptUtils PeripheryAddresses keeps `securitizeFactory` (v1, read-only) and
  adds `securitizeFactoryV2`; 50_CoreAndPeriphery deploys the V2 factory under
  the new key behind a `deploySecuritizeFactoryV2` flag; the interactive and
  verify scripts are updated accordingly. Existing per-chain address books need
  no edits (a missing key deserializes to address(0)).

Tests:

- Regression coverage: the cross-frame steal reverts; withdraw/redeem to an
  unrelated receiver reverts; a liquidation succeeds and the liquidator can
  redeem its seized shares to its own family (but not to an unrelated one);
  governor seize succeeds and the recipient can redeem to its own family; the
  existing legitimate-liquidation and freeze tests remain green.

docs/securitize-vault.md updated for the new receiver binding and the V2 names.
@kasperpawlowski
kasperpawlowski force-pushed the feat/securitize-collateral-vault-v2 branch from 2792b92 to 644a191 Compare September 1, 2026 12:46
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.

1 participant