diff --git a/SIP-0094.md b/SIP-0094.md new file mode 100644 index 0000000..d3a7ea9 --- /dev/null +++ b/SIP-0094.md @@ -0,0 +1,242 @@ +--- +SIP: '0094' +Title: Perimeter Fee Activation and Adoption Fund Transfer +Author: Edan Yago (@YagoBit) +Status: Ready for vote +Track: Contract +Created: 2026-08-12 +--- + +# SIP-0094 (Parts 1–3): Perimeter Fee Activation and Adoption Fund Transfer + +## Summary + +This post introduces SIP-0094, voted as three technical proposals: the governor caps any single proposal at ten actions (`proposalMaxOperations`), a split Bitocracy has used before (SIP-0046, Parts 1–4; SIP-0084, Parts 1–2). Parts 1 and 2 run on the owner governor; Part 3 runs on the admin governor, which owns the contract it touches. Each part is its own ballot, and this document covers all three – the single reference for the programme, narrative and technical alike. All three are created in the same cycle and voted in the same window; they form one programme. + +**Part 1** installs the Perimeter Fee – the fee component of the Sovryn security perimeter – across the lending protocol and Zero: ten actions, all of them code installation. +**Part 2** carries three actions: the final wiring action and, alongside it in two transfer legs, the Adoption Fund's fully-vested balance of 5,021,309.20 SOV moving to the Exchequer, continuing the arrangement Bitocracy created in SIP-0015 and has exercised since in SIP-0065 (2023), SIP-0076 (2024), and SIP-0079 (2024). +**Part 3**, a one-action proposal on the admin governor, retires the SOV emission subsidy to the Zero Stability Pool. + +**Nothing charges when these proposals execute.** The system ships installed and switched off, and stays off until a separate enable step after post-deployment verification. The launch configuration, stated here so the vote is cast with full knowledge: a uniform 0.10% (10 basis points) – $1 per $1,000 – on user-initiated withdrawal transactions, shown in the interface before the user confirms. + +Automated protocol flows – redemptions, liquidations, Stability Pool operations, rollovers – never pay the fee. They are not fee surfaces in the deployed code. + +## Background + +Sovryn holds user funds across lending pools, loan collateral, and Zero troves. In today's DeFi threat environment, value is lost at exits: an exploit converts a bug into a withdrawal in minutes. The security perimeter answers this in phases, and phase 1 is the fee rail – the piece that must exist first, launched at a deliberately minimal 0.1% rate. +On the treasury side, this is housekeeping. SIP-0015 (2021) created the Exchequer Committee and entrusted it with the SOV held in the Adoption and Development Funds as that SOV vests. Bitocracy has exercised that arrangement three times since – SIP-0065 (3,000,000 SOV, executed June 2023, tx `0xb1f0ccf6…77dfff32`), SIP-0076 (February 2024, both funds), and SIP-0079 (2024, Adoption Fund, toward the BOB campaign) – and the funds' vesting schedules have since run to completion. What remains today is the final vested residue: 5,021,309.20 SOV in the Adoption Fund and 2,486,471.60 SOV in the Development Fund (balances verified on-chain, 5 August 2026, RSK block ~9,124,829). After these transfers the funds are empty and the arrangement begun with SIP-0015 in 2021 is complete. + +## How the security perimeter works + +The perimeter is a trust boundary drawn around the protocol’s holdings. Inside it, nothing changes: positions, operations, and protocol flows compose exactly as they do today, with no new friction. The boundary acts only at the point where value leaves the protocol, because exits are where DeFi losses actually happen – an exploit only profits in the minutes it takes to turn a bug into a withdrawal. + +The perimeter ships in phases, in deliberate order. Phase 1 – this programme – installs the funding rail: fee machinery on every user-initiated exit surface, deployed switched off and enabled at a minimal rate only after post-deployment verification. Phase 2 – targeted for the weeks after execution – ships the protection itself: continuous monitoring of every exit, and the ability to intervene in the window between a suspicious withdrawal beginning and value leaving the perimeter. The rate schedule publishes together with phase 2, so rates move only as protection ships and the fee tracks the protection actually delivered. + +Rail-first is the safe order. The rail is the piece every later phase depends on, and it is the piece that changes user experience least – a visible $1-per-$1,000 fee, not a new constraint on movement. Until the perimeter is complete, every exit remains exactly as exposed as it is today; that exposure, not the fee, is what this programme exists to end. + +## Motivation + +Most money markets charge nothing on withdrawal. Those same markets can also do nothing for a user in the minutes after an exploit begins. The Perimeter Fee is the funding rail for continuous monitoring of every exit – a real, ongoing service, charged where it is delivered, at a launch rate sized to be barely perceptible: $1 per $1,000, on user-initiated withdrawal transactions, visible in the interface before every confirmation. The goal is a protocol that pays for its own defence out of its own usage rather than out of treasury drawdown. + + + +## Proposed Changes + +### SIP-0094 Part 1 – Perimeter Fee installation (ten actions) + +Ten governance actions that install the Perimeter Fee code: new lending-pool logic modules, three refreshed protocol modules, the fee module that owns the governance setters, the borrower-exit hook, and upgrades to two Zero contracts with their controller wiring. All ten execute as one atomic transaction – there is no partial-installation state. The action-by-action breakdown, with exact call data, target-by-target ownership verification, and rollback anchors for every replaced address, is in *Deployed contracts* below. + +### SIP-0094 Part 2 – final wiring and Adoption Fund transfer (three actions) + +1. The final Perimeter Fee wiring action (the deployment comprises eleven actions; the governor's ten-action cap places this one here — in the executable ordering it is the proposal's last action, asserted at creation). Executing Part 2 before Part 1 simply reverts and waits – an ordering mistake cannot create a broken state, which the fork rehearsal proves by executing the parts out of order and asserting the revert. +2. Withdrawal of the Adoption Fund's fully-vested 5,021,309.20 SOV (`0x0f31cfd6aAb4d378668Ad74DeFa89d3f4DB26633`) to the Exchequer Multisig (`0x924f5ad34698Fd20c90Fe5D5A8A0abd3b42dc711`), under the SIP-0015 arrangement, in the shape SIP-0065 and SIP-0076 used. The withdrawal's leg-by-leg decomposition is frozen into the proposal at creation, together with the balance re-read from chain. + +### SIP-0094 Part 3 – Stability Pool subsidy retirement + +A one-action proposal on the admin governor – the governor that owns the relevant contract – sets the SOV emission rate for Zero Stability Pool deposits to zero (`CommunityIssuance.setAPR(0)`), conserving the SOV currently spent to subsidise those deposits. Nothing is removed and no contract changes: it is a parameter set to zero, and a future governance vote could set it back. + +### Companion actions, disclosed for completeness + +- **Development Fund residue.** The Development Fund's 2,486,471.60 vested SOV transfers to the Exchequer by a direct multisig transaction in the same window, under the locked-token-owner authority the Exchequer holds on that contract, through the fund's deployed, verified sweep function – which empties the fund and retires it. No governance action is required for it; it is listed here so this post describes the whole programme in one place. +- **The enable step.** After Parts 1 and 2 execute, charging begins only when post-deployment verification is green, monitoring is live, and the pause switch has been rehearsed in a live drill – at which point the Exchequer sends the enable transaction. + +## Technical Details + +| Surface | Fee at launch | +| ------------------------------------------- | ------------- | +| Lending-pool withdrawal (iToken redemption) | 0.10% | +| Borrower collateral withdrawal / loan close | 0.10% | +| Zero trove collateral withdrawal / closure | 0.10% | +| Zero collateral-surplus claim | 0.10% | + +One rate everywhere: no exemptions and no per-pool differences. Every user faces the same 10 bps on the same surface. That includes the collateral-surplus claim – the outlet for exits whose moment the user did not choose, such as a full redemption against a trove, which is kept as its own surface precisely so governance can price it separately, including at zero, at any time by configuration. Automated protocol flows : redemptions, liquidations, Stability Pool operations, or rollovers, are not fee surfaces in the deployed code; for liquidations, the separation is structural, since the liquidation module lies outside the charging code entirely and is not even redeployed by this release. A fifth surface, AMM liquidity-pool withdrawal, is defined in the deployed code but ships shut, with nothing consuming it in this release; a shut gate short-circuits everything beneath it, and activating it later would be an owner configuration call, visible on-chain the moment it is made. + +- **Built to fail open.** On any fault in the fee machinery: controller unreachable, invalid quote, vault transfer failure – the product forgoes the fee and pays the user the full amount, and the user's own payout leg keeps the protocol's existing behaviour, so Perimeter Fee introduces no new way for a withdrawal to fail. The failure direction of every Perimeter Fee fault is "fee forgone," never "withdrawal reverted." This is asserted across a full failure matrix in the test suites and reconfirmed under each lens of the multi-lens internal review. +- **Everything is visible.** Every affected withdrawal shows the fee amount and an explanation before the user confirms. There are no silent deductions, and every configuration change emits a public on-chain event the moment it is made. +- **Rates are configuration, applied only forward.** Rates live on the deployed controller as basis-points settings – changeable only by the contract owner, never retroactively: a fee is resolved at the moment a transaction executes, so no change can reach a withdrawal that has already happened. The launch setting is 10 bps everywhere; the stated intent – an intent, not a code constraint – is that rates move only with the rate schedule to be published with phase 2; and the instrument for any incident is the pause switch, which halts all charging in minutes and is rehearsed as a live mainnet drill before launch. +- **Exchequer Ownership** The Exchequer Multisig – the operational committee Bitocracy created in SIP-0015 – holds both roles on the fee contracts at launch. The roles are deliberately split so that the fast operational keys are narrow: the admin role reaches only the pause switch, the fee destination, and fees already collected; it cannot touch user principal. Rates, exemptions, upgrades, and role assignment sit behind the owner role. User deposits sit in the product pools, which neither role can transfer; the one lever that can touch a future withdrawal is the rate setting itself – owner-only, forward-only, and public the instant it changes. +- **Proceeds.** Collected fees accrue in the fee vault under Exchequer control; the transferred SOV joins the treasury the Exchequer already stewards under SIP-0015. +- **Code and review.** Roughly 3,000 lines of production code carrying roughly 13,000 lines of tests, by the engineering team's count; a multi-lens internal security review (AI-assisted and manual, including a cross-model pass) with no surviving critical or important findings; five fund-safety invariants reconfirmed under each of the review lenses; and a full fork rehearsal through real governance: create, vote, queue, and execute, with charged and uncharged paths asserted on every surface. + +### Reviewing the code + +All source is public and open for review now, while deployment is being prepared. Two of the +three code bases are proposed as pull requests that stay **open** until this programme is +approved and executed — mainline follows the chain rather than leading it — so the pull request +is where the proposed change can be read as a diff against what is live today: + +| What | Where to review | +| -------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Perimeter Fee core — the shared exit-fee controller and vault | [`Sovryn-perimeter`](https://github.com/DistributedCollective/Sovryn-perimeter) (standalone repository; nothing to merge) | +| Lending and borrowing hooks, plus the proposal tooling for all three parts | PR [Sovryn-smart-contracts#580](https://github.com/DistributedCollective/Sovryn-smart-contracts/pull/580) (branch `sovryn-perimeter-fee` → `development`) | +| Zero trove-exit hooks and the collateral-surplus claim | PR [zero-contracts#10](https://github.com/DistributedCollective/zero-contracts/pull/10) (branch `sovryn-perimeter-fee` → `development`) | + +The implementations this programme installs are deployed from exactly those branch commits, and +each is source-verified on the Rootstock Blockscout explorer — so the code read in the pull +request, the bytecode on chain, and the addresses in the proposals below are the same artefact, +checkable independently by anyone. + +### Deployed contracts + +The Perimeter Fee contracts deploy — switched off, under Exchequer control — **before** this +post publishes, so the table below carries final addresses at publication. Every address is +source-verified on the Rootstock Blockscout explorer before publication, and each proposal's +on-chain actions can be checked against this table action by action. + +#### **Part 1 — the ten actions, in execution order.** Each is a single call with one address +argument; the ABI-encoded call data is that argument, and it is frozen into the proposal at +creation, where it can be read in the Bitocracy interface and rebuilt from the proposal +builders in the lending repository. + +| # | Target | Signature | Argument | +| --- | ---------------------------------------------------------------------- | -------------------------------------- | -------------------------------------------- | +| 1 | LoanTokenLogicBeaconLM `0x5b155ECcC1dC31Ea59F2c12d2F168C956Ac0FFAa` | `registerLoanTokenModule(address)` | `0x593DB96E61F59F1278068742486Fc85CDBe9D872` | +| 2 | LoanTokenLogicBeaconWrbtc `0x845eF7Be59664899398282Ef42239634aBDd752C` | `registerLoanTokenModule(address)` | `0x6c8f59D321560f4bEF84c99e83dd7FF31122e14f` | +| 3 | sovrynProtocol `0x5A0D867e0D70Fcc6Ade25C3F1B89d618b5B4Eaa7` | `replaceContract(address)` | `0xdB4fF0a861bF0c714B31E678c919c2B7EfD05329` | +| 4 | sovrynProtocol | `replaceContract(address)` | `0xa3FCC9F88De9A7f0258eda6cD8d6F7D39ef6fb8d` | +| 5 | sovrynProtocol | `replaceContract(address)` | `0xa87Bd1eF82FA2BE049473c73eAaa97d6AB9C4399` | +| 6 | sovrynProtocol | `replaceContract(address)` | `0x0562e396f5fF7e819c8e9CcF25DCb68e16A2672F` | +| 7 | sovrynProtocol | `setBorrowerExitPerimeterOps(address)` | `0xe533dc6D03Ae97977Da3357A98AA94b769738AB5` | +| 8 | CollSurplusPool proxy `0x310ec7Fe6e4943DA773De8948255E37CC45e34bb` | `setImplementation(address)` | `0xdd518C2aae5A83812883D4e350b88d3566792bE4` | +| 9 | BorrowerOperations proxy `0x5B9dB4B8bdeF3e57323187a9AC2639C5DEe5FD39` | `setImplementation(address)` | `0x020aAb1d84E38b619A4C49f48d472c3b9187ce93` | +| 10 | BorrowerOperations proxy | `setExitFeeController(address)` | `0x99994b4522483DE17F31a5bC010c5901AdD3440E` | + +Actions 9 and 10 are ordered so the implementation swap precedes the controller wiring +inside the same atomic transaction. No action carries RBTC value. + +**Core Perimeter Fee contracts** (repo [`Sovryn-perimeter`](https://github.com/DistributedCollective/Sovryn-perimeter)) — deployed and verified: + +| Contract | Address | +| -------------------------------- | -------------------------------------------- | +| ExitFeeController proxy | `0x99994b4522483DE17F31a5bC010c5901AdD3440E` | +| ExitFeeController implementation | `0x50EC5c1C156cfA7e3007a0b0C97298E4f58a552d` | +| ExitFeeVault proxy | `0xDDE75f75ff33Aa802f2316cCAe2bE77823fc6f9B` | +| ExitFeeVault implementation | `0x8f977F4c9dCcCE1a0306A34944C0460e4445deB4` | + +Both implementations are source-verified on the Rootstock Blockscout explorer (solc 0.8.20, +evm paris, optimizer 200 runs). The controller was deployed switched **off** and configured +in the same session: the four charging surfaces registered active at 10 bps, the AMM surface +registered shut, the fee receiver pointed at the vault, and both contracts' operational admin +and ownership directed to the Exchequer Multisig — read back from chain after execution. +Operational admin on both contracts is the Exchequer Multisig today. + +**Lending / borrowing implementations installed by Part 1** (repo [`Sovryn-smart-contracts`](https://github.com/DistributedCollective/Sovryn-smart-contracts), branch `sovryn-perimeter-fee`, review in PR [#580](https://github.com/DistributedCollective/Sovryn-smart-contracts/pull/580)): + +| Contract | Address | +| ------------------------ | -------------------------------------------- | +| LoanTokenLogicLM | `0x593DB96E61F59F1278068742486Fc85CDBe9D872` | +| LoanTokenLogicWrbtcLM | `0x6c8f59D321560f4bEF84c99e83dd7FF31122e14f` | +| LoanClosingsRollover | `0xdB4fF0a861bF0c714B31E678c919c2B7EfD05329` | +| LoanClosingsWith | `0xa3FCC9F88De9A7f0258eda6cD8d6F7D39ef6fb8d` | +| LoanMaintenance | `0xa87Bd1eF82FA2BE049473c73eAaa97d6AB9C4399` | +| ExitFeeModule | `0x0562e396f5fF7e819c8e9CcF25DCb68e16A2672F` | +| BorrowerExitPerimeterOps | `0xe533dc6D03Ae97977Da3357A98AA94b769738AB5` | + + +Three of the modules Part 1 registers link the stateless library `SwapsImplSovrynSwapLib`, +and all three link the same deployment of it — needed to reproduce their bytecode, and +source-verified on the explorer: + +| Registered module | Links `SwapsImplSovrynSwapLib` at | +| ---------------------- | -------------------------------------------- | +| `LoanClosingsRollover` | `0xFE2bb2d345452673C4E90622147c4F515F2F4CE0` | +| `LoanClosingsWith` | `0xFE2bb2d345452673C4E90622147c4F515F2F4CE0` | +| `LoanMaintenance` | `0xFE2bb2d345452673C4E90622147c4F515F2F4CE0` | + +`0xFE2bb2d…` was deployed during this release. The library's own source is unchanged from +the previous release, but it imports an interface this release extends, +which changes the compiled artifact and therefore its address; the two are byte-identical +apart from each one's embedded self-address and metadata fingerprint, which anyone can +confirm on-chain. The release links this copy rather than the older one because the +explorer verifies it as a **full** source match while the older copy matches only +partially — so a voter rebuilding a module's bytecode lands on source the explorer vouches +for exactly. + +The library is not registered by Part 1; it is referenced only inside the linked modules' +bytecode, and its address is listed here so every registered module can be independently +rebuilt from source. + +#### **Zero implementations installed by Part 1** (repo [`zero-contracts`](https://github.com/DistributedCollective/zero-contracts), branch `sovryn-perimeter-fee`, review in PR [#10](https://github.com/DistributedCollective/zero-contracts/pull/10)): + +| Contract | Address | +| ---------------------------------------------- | -------------------------------------------- | +| BorrowerOperations implementation (fee-hooked) | `0x020aAb1d84E38b619A4C49f48d472c3b9187ce93` | +| CollSurplusPool implementation | `0xdd518C2aae5A83812883D4e350b88d3566792bE4` | + +For every module Part 1 replaces, the table below records the currently-live implementation +it replaces – the rollback anchor – together with the runtime code hash of each new artifact, +so a voter can bind every proposal action to both the address it installs and the address it +supersedes. Anchors were read from the live protocol, beacons, and proxies on +2026-08-25; they are re-verified against the chain immediately before proposal creation, and +any drift (an unrelated upgrade landing in between) voids and re-freezes this table. + +| Replaced module | Currently live (rollback anchor) | New implementation | Runtime code hash | +| ----------------------- | ---------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | -------------------------------------------------------------------- | +| LoanClosingsRollover | `0xc7AEa780c8b891e2c101a9ED2f9EaCF87FFdc359` | `0xdB4fF0a861bF0c714B31E678c919c2B7EfD05329` | `0xdb0abc5e825a401bd3239dfddc02612088ec1373ee29dd04a2bb3692e089c347` | +| LoanClosingsWith | `0x47eCc9391fd6f5761B8d24139f36Ce61fF6a9819` | `0xa3FCC9F88De9A7f0258eda6cD8d6F7D39ef6fb8d` | `0x34083ae377445509350becb30c2dd641ea531025eb5496c590d5cd91f6013b60` | +| LoanMaintenance | `0x4BC462e2c8D106511D8e7985b05A52fD8b726464` | `0xa87Bd1eF82FA2BE049473c73eAaa97d6AB9C4399` | `0xdb251795feb63cf2e1b952e0be33e20662e7c40f63a87d11e6a23935ce529587` | +| iToken logic (LM) | `0xfaffde7161C58743B86a22EF268245560aC705dD` | `0x593DB96E61F59F1278068742486Fc85CDBe9D872` | `0x6a08b3c8c9edbb040e4d33ee45cf5dc163e82b54b3dc72fced08cf0feb63c800` | +| iToken logic (WrbtcLM) | `0x24B3687966C4f05e48ED5A3B9c14F56d9ad5BF6B` | `0x6c8f59D321560f4bEF84c99e83dd7FF31122e14f` | `0xe09a3c2e8c1edcbb2960ff350ee36bbdb3c0a57210598ad271983fa98d674981` | +| Zero BorrowerOperations | `0xD603B4c5F7BF13a2AA510F2c625546dB4138D330` | `0x020aAb1d84E38b619A4C49f48d472c3b9187ce93` | `0xe978bc12cc95f546332aa28c029306ea2b45990307548c0b80bd740ae28e5cf1` | +| Zero CollSurplusPool | `0xE9005C36452576eFA74f297906e77381E6878398` (the original implementation — never upgraded before this proposal) | `0xdd518C2aae5A83812883D4e350b88d3566792bE4` | `0xc00a07e5f8b0c1eba5b6b33e3ed38a9b15d7867e0fe6ea8611949b3bc53caa6f` | + +`ExitFeeModule` and `BorrowerExitPerimeterOps` are new registrations rather than replacements, so +they have no rollback anchor. `LoanClosingsLiquidation` is deliberately **not** redeployed: its +runtime bytecode is byte-identical to the module already registered, so liquidations keep +running against the same code throughout. + +## Risks + +Risks introduced: + +- **A fee where the market's default is zero.** Users pay 10 basis points on withdrawals that were previously free. +- **The rail precedes the rest of the perimeter.** Phase 1 charges its minimal rate before phase 2 ships. +- **New code in live paths.** The fee vault and controller are new contracts; the release also replaces three live loan-closing and maintenance modules, upgrades two Zero contracts, one for the first time, and the borrower-exit hook carries the same protocol-level authority the existing core modules already hold. + +Risk mitigants: + +- **Minimal, visible, forward-only.** $1 per $1,000, shown before every confirmation; no change ever reaches an executed transaction; every change is public instantly. +- **Fail-open everywhere.** No Perimeter Fee fault path shrinks a user's withdrawal, the system gives up its fee instead, asserted across the failure matrix and reconfirmed under every lens of the internal review. +- **Off until verified.** Both parts execute with charging globally disabled; the enable step waits for verification, live monitoring, and a rehearsed pause drill. +- **Rail-first sequencing.** Deploying the rail at a minimal rate is what makes phase 2 deployable at all; every exit today is exactly as exposed as it was yesterday until the perimeter ships, which is the problem this programme exists to end. + + +## Implementation + +| Item | Value | +| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Parts 1–3 | Three proposals (Parts 1–2 on the owner governor, Part 3 on the admin governor), created in the same cycle, voted in the same window; Part 1 executes first (wrong order reverts harmlessly) | +| Owner and admin at launch | Exchequer Multisig `0x924f5ad34698Fd20c90Fe5D5A8A0abd3b42dc711` | +| Adoption Fund (Part 2 action) | `0x0f31cfd6aAb4d378668Ad74DeFa89d3f4DB26633` – 5,021,309.20 SOV | +| Development Fund (companion multisig action) | `0x617866cC4a089c3653ddC31a618b078291839AeB` – 2,486,471.60 SOV | +| Stability Pool subsidy (Part 3 – one-action admin-governor proposal) | `CommunityIssuance.setAPR(0)` | +| Contract addresses | Deployed and explorer-verified before publication — see *Deployed contracts* above | +| Perimeter phase 2 | Targeted for the weeks following execution, with a reserve window; rate schedule published then | + +## Rationale + +Three ballots: the deployment is eleven owner-governor actions against a ten-action cap, and the subsidy retirement lives on the admin governor because that governor owns the contract it touches – the same split Bitocracy used for SIP-0046 and SIP-0084. One window because the actions form one programme: the perimeter, and the treasury housekeeping that closes out the 2021 fund arrangement. Each part is its own proposal with its own tally, so holders judge each on its merits – with one stated cost: Part 2 carries the final wiring action and the Adoption Fund withdrawal on the same ballot, so a holder weighing those two purposes weighs them together. That is the price of keeping the programme to two governance cycles, stated here rather than hidden. The Development Fund residue moves by multisig because the Exchequer already holds the relevant authority on that contract; disclosing it here, rather than exercising it silently, is the point of this section. + +## License + +Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/).