Skip to content

Introduce the Sovryn Perimeter Fee on Zero - #10

Open
tjcloa wants to merge 8 commits into
developmentfrom
sovryn-perimeter-fee
Open

Introduce the Sovryn Perimeter Fee on Zero#10
tjcloa wants to merge 8 commits into
developmentfrom
sovryn-perimeter-fee

Conversation

@tjcloa

@tjcloa tjcloa commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Phase 1 of the Sovryn security perimeter (SIP-0094): a minimal exit fee on user-initiated withdrawal surfaces, funding continuous exit monitoring. This is the Zero half of the system; the shared fee rail lives in Sovryn-perimeter and the lending half in Sovryn-smart-contracts#580.

What this carries

  • Exit-fee hooks in BorrowerOperations on collateral withdrawal and trove closure, quoting through the shared ExitFeeController and paying the fee leg to the ExitFeeVault. Every hook fails open: a fee fault forgoes the fee, never blocks a withdrawal.
  • The surplus-claim surface: CollSurplusPool.claimCollWithFee, a BorrowerOperations-only two-leg split that keeps claimColl byte-untouched, with the pool implementation upgrade ordered strictly before the BorrowerOperations upgrade inside SIP-0094 executable part 1.
  • Implementation-only deploy scripts for both contracts (the proxy swaps are governance actions), storage-layout zero-diff guards, and the fee test suite including reentrancy/fail-open matrices and Echidna invariants.

The fee system deploys disabled and is enabled only by governance after post-deployment verification.

Merge timing

This PR is intended to stay open until SIP-0094 has been approved and executed on-chain — mainline follows the chain rather than leading it. The deployment runs from this branch's commit, so no contract file here may change between deploy and execution; the proposal builders compare the live runtime code hash at creation, so a stale implementation produces a refused proposal rather than a silent mismatch.

Phase 1 of the Sovryn security perimeter (SIP-0094): a minimal exit fee on
user-initiated withdrawal surfaces, funding continuous exit monitoring.
This change carries the Zero half of the system:

- exit-fee hooks in BorrowerOperations on collateral withdrawal and trove
  closure, quoting through the shared ExitFeeController (Sovryn-perimeter
  repo) and paying the fee leg to the ExitFeeVault; every hook fails open —
  a fee fault forgoes the fee, never blocks a withdrawal;
- the surplus-claim surface: CollSurplusPool.claimCollWithFee, a BO-only
  two-leg split that keeps claimColl byte-untouched, with the pool
  implementation upgrade ordered strictly before the BO upgrade inside
  SIP-0094 executable part 1;
- impl-only deploy scripts for both contracts (the proxy swaps are
  governance actions), storage-layout zero-diff guards, and the ColFee
  test suite incl. reentrancy/fail-open matrices and Echidna invariants.

The fee system deploys disabled and enables only by governance after
post-deployment verification.
tjcloa added 7 commits August 13, 2026 12:09
Deployed 2026-08-13 by 0x163463b7ddbce853832037a059f5c5e6606bf9c4 (nonces
25-26), both inert until SIP-0094 Part 1 calls setImplementation:

  BorrowerOperations_Implementation  0xcD22ba4b3ED7D7297b40Dcd26d982634A0207885
  CollSurplusPool_Implementation     0x71A605F81a66eB93Ce9b8091014da858bFD4b6dA

Verified after deploy: both hold code; both live proxies still point at
their current implementations (BO 0xD603B4c5…, CSP 0xE9005C36…); the
permit2 immutable baked into the new BO implementation equals the live
proxy's. Runtime codehashes match the fork rehearsal's pins exactly, so
mainnet carries bit-for-bit the code the dress run exercised.

The stale BorrowerOperations_Implementation record was moved aside before
the run: hardhat-deploy's bytecode comparison needs the historical deploy
transaction, which no public RSK endpoint still serves. The prior record
remains in git history.
Zero's half of the Phase 1 re-cut. The controller pointer slot and the two Zero
surface ids are keccak256 of a name, so renaming the name moves the value --
which is the point: Phase 2 then inherits these ids instead of migrating off
live ones.

  sovryn.exitFeeController -> sovryn.perimeterExitFeeController
  SURFACE_ZERO_*           -> PERIMETER_SURFACE_ZERO_*

Code identifiers and the tests directory follow, so the Echidna suite and the
fixtures match what Phase 2 expects.

Identifiers.pinned cross-checks the literal 32 bytes against the lending repo's
copies. Zero declares these independently, so a one-character drift between the
two repos would not fail loudly -- Zero would resolve no policy and stop
charging silently.

Zero perimeter suite 47 passing plus 4 pinned identifiers.
The Phase-1 id was keccak256("COLFEE:" + name). The rename swept the prefix to
"PERIMETER:" and left it in place, which hashes to neither the Phase-1 id nor
the one Phase 2 expects -- a constant that looks right, resolves no policy, and
stops the fee without failing.

The namespace now lives inside the name, so the preimage is the name alone,
matching the lending repo and the controller bootstrap.
The rename walked deployment/deployments and rewrote three records of what is
already on RSK mainnet: the Phase-B BorrowerOperations and CollSurplusPool
implementations and their shared solc input. Those files are evidence, not
source -- they are the rollback anchor and the input the explorer verification
was performed against. Rewriting their embedded source left records whose
metadata described code that was never deployed, while the bytecode field still
held the original build.

Restored verbatim from the Phase-1 branch. The re-cut deploys new
implementations under new records; the old ones stay exactly as they were.
The prettier pre-commit hook reformatted them on the way back in. These files
are evidence of what is deployed -- they must match the Phase-1 branch exactly,
formatting included, so committed with the hook bypassed.
The rename mapped ColFeeApplied/ColFeeSkipped onto Perimeter* here, but the
events this enum documents are ExitFeeApplied and ExitFeeSkipped -- the
Perimeter* names belong to the Echidna harness, not the product ABI. A NatSpec
line naming an event that does not exist sends a reader looking for the wrong
topic.
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