|
| 1 | +// tBTC Bridge RebateStaking Governance Runbook |
| 2 | +// |
| 3 | +// Network: Ethereum Mainnet |
| 4 | +// Date: November 2025 |
| 5 | + |
| 6 | += tBTC Bridge RebateStaking – Governance Execution Plan |
| 7 | + |
| 8 | +== Overview |
| 9 | + |
| 10 | +This document describes the exact on-chain steps and payloads |
| 11 | +required to complete the activation of the tBTC Rebate System by |
| 12 | +connecting the `RebateStaking` contract to the tBTC Bridge via the |
| 13 | +new `BridgeGovernance.setRebateStaking(address)` entrypoint. |
| 14 | + |
| 15 | +The plan assumes that the Bridge proxy has already been upgraded |
| 16 | +to a new implementation that supports rebate staking ("Action 1" |
| 17 | +from the October 22 Deployment Plan), and focuses on the missing |
| 18 | +governance hook ("Action 2"). |
| 19 | + |
| 20 | +The Bridge implementation enforces that the rebate staking address |
| 21 | +is configured exactly once and cannot be changed via governance; |
| 22 | +changing it later requires a dedicated Bridge implementation |
| 23 | +upgrade. |
| 24 | + |
| 25 | +== Key Contracts & Parameters |
| 26 | + |
| 27 | +* Network: Ethereum Mainnet |
| 28 | +* Bridge (Proxy):: `0x5e4861a80B55f035D899f66772117F00FA0E8e7B` |
| 29 | +* Bridge Implementation (New):: `0x98e19c416100Ad0C66B52d05075F4C899184f2aD` |
| 30 | +* BridgeGovernance (OLD):: `0xf286EA706A2512d2B9232FE7F8b2724880230b45` |
| 31 | +* BridgeGovernance (NEW):: `<NEW_BRIDGE_GOVERNANCE>` |
| 32 | +* RebateStaking (Proxy):: `0x0184739C32edc3471D3e4860c8E39a5f3Ff85A45` |
| 33 | +* ProxyAdmin:: `0x16A76d3cd3C1e3CE843C6680d6B37E9116b5C706` |
| 34 | +* Governance / Council EOA (BridgeGovernance owner):: |
| 35 | +** `0x9F6e831c8F8939DC0C830C6e492e7cEf4f9C2F5f` |
| 36 | +* Bridge governance delay (seconds):: `172800` (48 hours) |
| 37 | + |
| 38 | +Use the following commands (with a configured `ETHEREUM_RPC_URL`) |
| 39 | +to confirm the current state on mainnet: |
| 40 | + |
| 41 | +[,bash] |
| 42 | +---- |
| 43 | +# Bridge governance should be OLD BridgeGovernance |
| 44 | +cast call 0x5e4861a80B55f035D899f66772117F00FA0E8e7B \ |
| 45 | + "governance()(address)" |
| 46 | +
|
| 47 | +# Expected: 0xf286EA706A2512d2B9232FE7F8b2724880230b45 |
| 48 | +
|
| 49 | +# BridgeGovernance (OLD) governance delay should be 48h |
| 50 | +cast call 0xf286EA706A2512d2B9232FE7F8b2724880230b45 \ |
| 51 | + "governanceDelays(uint256)(uint256)" 0 |
| 52 | +
|
| 53 | +# Expected: 172800 |
| 54 | +
|
| 55 | +# RebateStaking should not yet be wired |
| 56 | +cast call 0x5e4861a80B55f035D899f66772117F00FA0E8e7B \ |
| 57 | + "getRebateStaking()(address)" |
| 58 | +
|
| 59 | +# Expected: 0x0000000000000000000000000000000000000000 |
| 60 | +---- |
| 61 | + |
| 62 | +== Pre-checks (read-only) |
| 63 | + |
| 64 | +* Bridge proxy implementation is already upgraded to the rebate-aware version. |
| 65 | +* Bridge governance owner is the OLD BridgeGovernance: |
| 66 | +** `cast call <BRIDGE> "governance()(address)"` -> `0xf286EA706A2512d2B9232FE7F8b2724880230b45` |
| 67 | +* RebateStaking wiring is not yet set: |
| 68 | +** `cast call <BRIDGE> "getRebateStaking()(address)"` -> `0x0000...0000` |
| 69 | +* RebateStaking proxy points at the Bridge proxy: |
| 70 | +** `cast call <REBATE_STAKING> "bridge()(address)"` -> `<BRIDGE>` |
| 71 | + |
| 72 | +== High-Level Steps |
| 73 | + |
| 74 | +0. (Already executed) Upgrade the Bridge proxy implementation. |
| 75 | +1. Deploy NEW `BridgeGovernance` with the new |
| 76 | + `setRebateStaking(address)` function. |
| 77 | +2. (Optional) Transfer ownership of NEW `BridgeGovernance` to the |
| 78 | + Council EOA. |
| 79 | +3. Using OLD `BridgeGovernance`, call |
| 80 | + `beginBridgeGovernanceTransfer(newBridgeGovernance)` to start the |
| 81 | + governance timelock for Bridge governance transfer (48 hours on |
| 82 | + mainnet, 60 seconds on Sepolia). |
| 83 | +4. After the governance delay elapses, call |
| 84 | + `finalizeBridgeGovernanceTransfer()` on OLD `BridgeGovernance` to |
| 85 | + make NEW `BridgeGovernance` the Bridge owner. |
| 86 | +5. Using NEW `BridgeGovernance`, call |
| 87 | + `setRebateStaking(RebateStakingProxy)` once to connect the |
| 88 | + RebateStaking contract to the Bridge implementation. This call |
| 89 | + reverts if the address is zero or already set. |
| 90 | +6. Verify post-conditions on-chain and, optionally, via the |
| 91 | + existing Hardhat verification script. |
| 92 | + |
| 93 | +All payloads below are given as raw calldata so they can be |
| 94 | +submitted through a multisig UI, a timelock, or directly by the |
| 95 | +Council, depending on operational setup. |
| 96 | + |
| 97 | +== Action 0 – (For Reference) Bridge Proxy Upgrade |
| 98 | + |
| 99 | +This action has already been executed and is documented here for |
| 100 | +completeness. It upgraded the Bridge proxy to a new |
| 101 | +implementation that supports rebate staking. |
| 102 | + |
| 103 | +* Executor:: ProxyAdmin owner |
| 104 | +* To:: `0x16A76d3cd3C1e3CE843C6680d6B37E9116b5C706` (ProxyAdmin) |
| 105 | +* Method:: `upgrade(address proxy, address implementation)` |
| 106 | + |
| 107 | +[,json] |
| 108 | +---- |
| 109 | +{ |
| 110 | + "to": "0x16A76d3cd3C1e3CE843C6680d6B37E9116b5C706", |
| 111 | + "value": "0", |
| 112 | + "data": "0x99a88ec40000000000000000000000005e4861a80b55f035d899f66772117f00fa0e8e7b00000000000000000000000098e19c416100ad0c66b52d05075f4c899184f2ad" |
| 113 | +} |
| 114 | +---- |
| 115 | + |
| 116 | +Decoded: |
| 117 | + |
| 118 | +* `upgrade(proxy = 0x5e48…, implementation = 0x98e1…)` |
| 119 | + |
| 120 | +== Action 1 – Deploy NEW BridgeGovernance |
| 121 | + |
| 122 | +NEW `BridgeGovernance` must be deployed from the PR implementation |
| 123 | +that includes: |
| 124 | + |
| 125 | +* `setRebateStaking(address rebateStaking)` forwarding to the |
| 126 | + Bridge implementation, and |
| 127 | +* The constructor: |
| 128 | + `constructor(Bridge _bridge, uint256 _governanceDelay)`. |
| 129 | + |
| 130 | +Constructor arguments: |
| 131 | + |
| 132 | +* `_bridge`:: `0x5e4861a80B55f035D899f66772117F00FA0E8e7B` |
| 133 | +* `_governanceDelay`:: `172800` |
| 134 | + |
| 135 | +This step is typically executed via the existing Hardhat deployment |
| 136 | +scripts and produces a new mainnet address: |
| 137 | + |
| 138 | +* `BridgeGovernance (NEW): <NEW_BRIDGE_GOVERNANCE>` |
| 139 | + |
| 140 | +That address must be recorded and used in all subsequent steps. |
| 141 | + |
| 142 | +=== Optional – Transfer Ownership of NEW BridgeGovernance |
| 143 | + |
| 144 | +If the deployer is not already the Council EOA, ownership of NEW |
| 145 | +`BridgeGovernance` should be transferred to the Council. |
| 146 | + |
| 147 | +* Executor:: Deployer (current `owner()` of NEW BridgeGovernance) |
| 148 | +* To:: `<NEW_BRIDGE_GOVERNANCE>` |
| 149 | +* Method:: `transferOwnership(address newOwner)` |
| 150 | + |
| 151 | +Parameters: |
| 152 | + |
| 153 | +* `newOwner`:: `0x9F6e831c8F8939DC0C830C6e492e7cEf4f9C2F5f` |
| 154 | + |
| 155 | +Calldata (generated via `cast calldata`): |
| 156 | + |
| 157 | +[,bash] |
| 158 | +---- |
| 159 | +cast calldata "transferOwnership(address)" \ |
| 160 | + 0x9F6e831c8F8939DC0C830C6e492e7cEf4f9C2F5f |
| 161 | +
|
| 162 | +# 0xf2fde38b0000000000000000000000009f6e831c8f8939dc0c830c6e492e7cef4f9c2f5f |
| 163 | +---- |
| 164 | + |
| 165 | +Tx template: |
| 166 | + |
| 167 | +[,json] |
| 168 | +---- |
| 169 | +{ |
| 170 | + "from": "<DEPLOYER_ADDRESS>", |
| 171 | + "to": "<NEW_BRIDGE_GOVERNANCE>", |
| 172 | + "value": "0", |
| 173 | + "data": "0xf2fde38b0000000000000000000000009f6e831c8f8939dc0c830c6e492e7cef4f9c2f5f" |
| 174 | +} |
| 175 | +---- |
| 176 | + |
| 177 | +Post-condition check: |
| 178 | + |
| 179 | +[,bash] |
| 180 | +---- |
| 181 | +cast call <NEW_BRIDGE_GOVERNANCE> "owner()(address)" --rpc-url $ETHEREUM_RPC_URL |
| 182 | +
|
| 183 | +# Expected: 0x9F6e831c8F8939DC0C830C6e492e7cEf4f9C2F5f |
| 184 | +---- |
| 185 | + |
| 186 | +== Action 2 – Begin Bridge Governance Transfer (OLD → NEW) |
| 187 | + |
| 188 | +This action starts the on-chain, 48-hour governance delay for |
| 189 | +transferring the Bridge governance from OLD to NEW |
| 190 | +`BridgeGovernance`. |
| 191 | + |
| 192 | +* Executor:: Council EOA (owner of OLD BridgeGovernance) |
| 193 | +* From:: `0x9F6e831c8F8939DC0C830C6e492e7cEf4f9C2F5f` |
| 194 | +* To:: `0xf286EA706A2512d2B9232FE7F8b2724880230b45` |
| 195 | +* Method:: `beginBridgeGovernanceTransfer(address _newBridgeGovernance)` |
| 196 | + |
| 197 | +Parameters: |
| 198 | + |
| 199 | +* `_newBridgeGovernance`:: `<NEW_BRIDGE_GOVERNANCE>` |
| 200 | + |
| 201 | +Calldata (generate with your actual NEW address): |
| 202 | + |
| 203 | +[,bash] |
| 204 | +---- |
| 205 | +cast calldata "beginBridgeGovernanceTransfer(address)" \ |
| 206 | + <NEW_BRIDGE_GOVERNANCE> |
| 207 | +
|
| 208 | +# Example shape (do not copy literally): |
| 209 | +# 0x8b18a505000000000000000000000000<NEW_BRIDGE_GOVERNANCE_20_BYTES> |
| 210 | +---- |
| 211 | + |
| 212 | +Tx template: |
| 213 | + |
| 214 | +[,json] |
| 215 | +---- |
| 216 | +{ |
| 217 | + "from": "0x9F6e831c8F8939DC0C830C6e492e7cEf4f9C2F5f", |
| 218 | + "to": "0xf286EA706A2512d2B9232FE7F8b2724880230b45", |
| 219 | + "value": "0", |
| 220 | + "data": "0x8b18a505000000000000000000000000<NEW_BRIDGE_GOVERNANCE_20_BYTES>" |
| 221 | +} |
| 222 | +---- |
| 223 | + |
| 224 | +Post-condition check: |
| 225 | + |
| 226 | +[,bash] |
| 227 | +---- |
| 228 | +cast call 0xf286EA706A2512d2B9232FE7F8b2724880230b45 \ |
| 229 | + "bridgeGovernanceTransferChangeInitiated()(uint256)" |
| 230 | +
|
| 231 | +# Expected: non-zero timestamp after this transaction. |
| 232 | +---- |
| 233 | + |
| 234 | +== Action 3 – Finalize Bridge Governance Transfer |
| 235 | + |
| 236 | +At least `172800` seconds (48 hours) after the timestamp returned |
| 237 | +by `bridgeGovernanceTransferChangeInitiated()`, finalize the |
| 238 | +governance transfer so that NEW `BridgeGovernance` becomes the |
| 239 | +governance owner of the Bridge. |
| 240 | + |
| 241 | +* Executor:: Council EOA |
| 242 | +* From:: `0x9F6e831c8F8939DC0C830C6e492e7cEf4f9C2F5f` |
| 243 | +* To:: `0xf286EA706A2512d2B9232FE7F8b2724880230b45` |
| 244 | +* Method:: `finalizeBridgeGovernanceTransfer()` |
| 245 | + |
| 246 | +Calldata: |
| 247 | + |
| 248 | +[,bash] |
| 249 | +---- |
| 250 | +cast calldata "finalizeBridgeGovernanceTransfer()" |
| 251 | +
|
| 252 | +# 0x605da053 |
| 253 | +---- |
| 254 | + |
| 255 | +Tx template: |
| 256 | + |
| 257 | +[,json] |
| 258 | +---- |
| 259 | +{ |
| 260 | + "from": "0x9F6e831c8F8939DC0C830C6e492e7cEf4f9C2F5f", |
| 261 | + "to": "0xf286EA706A2512d2B9232FE7F8b2724880230b45", |
| 262 | + "value": "0", |
| 263 | + "data": "0x605da053" |
| 264 | +} |
| 265 | +---- |
| 266 | + |
| 267 | +Post-condition checks: |
| 268 | + |
| 269 | +[,bash] |
| 270 | +---- |
| 271 | +# Bridge governance should now be NEW BridgeGovernance |
| 272 | +cast call 0x5e4861a80B55f035D899f66772117F00FA0E8e7B \ |
| 273 | + "governance()(address)" |
| 274 | +
|
| 275 | +# Expected: <NEW_BRIDGE_GOVERNANCE> |
| 276 | +
|
| 277 | +# OLD BridgeGovernance should have reset its pending transfer state |
| 278 | +cast call 0xf286EA706A2512d2B9232FE7F8b2724880230b45 \ |
| 279 | + "bridgeGovernanceTransferChangeInitiated()(uint256)" |
| 280 | +
|
| 281 | +# Expected: 0 |
| 282 | +---- |
| 283 | + |
| 284 | +== Action 4 – Set RebateStaking via NEW BridgeGovernance |
| 285 | + |
| 286 | +Once the Bridge `governance()` points to NEW `BridgeGovernance`, |
| 287 | +the Council can perform the one-off wiring of the RebateStaking |
| 288 | +contract to the Bridge. |
| 289 | + |
| 290 | +* Executor:: Council EOA (owner of NEW BridgeGovernance) |
| 291 | +* From:: `0x9F6e831c8F8939DC0C830C6e492e7cEf4f9C2F5f` |
| 292 | +* To:: `<NEW_BRIDGE_GOVERNANCE>` |
| 293 | +* Method:: `setRebateStaking(address rebateStaking)` |
| 294 | + |
| 295 | +Parameters: |
| 296 | + |
| 297 | +* `rebateStaking`:: `0x0184739C32edc3471D3e4860c8E39a5f3Ff85A45` |
| 298 | + |
| 299 | +Calldata (fully determined): |
| 300 | + |
| 301 | +[,bash] |
| 302 | +---- |
| 303 | +cast calldata "setRebateStaking(address)" \ |
| 304 | + 0x0184739C32edc3471D3e4860c8E39a5f3Ff85A45 |
| 305 | +
|
| 306 | +# 0xca73c4620000000000000000000000000184739c32edc3471d3e4860c8e39a5f3ff85a45 |
| 307 | +---- |
| 308 | + |
| 309 | +Tx template: |
| 310 | + |
| 311 | +[,json] |
| 312 | +---- |
| 313 | +{ |
| 314 | + "from": "0x9F6e831c8F8939DC0C830C6e492e7cEf4f9C2F5f", |
| 315 | + "to": "<NEW_BRIDGE_GOVERNANCE>", |
| 316 | + "value": "0", |
| 317 | + "data": "0xca73c4620000000000000000000000000184739c32edc3471d3e4860c8e39a5f3ff85a45" |
| 318 | +} |
| 319 | +---- |
| 320 | + |
| 321 | +Decoded call path: |
| 322 | + |
| 323 | +* External:: `BridgeGovernance(NEW).setRebateStaking(RebateStakingProxy)` |
| 324 | +* Internal:: `Bridge.setRebateStaking(RebateStakingProxy)` |
| 325 | +* Bridge state library:: `BridgeState.setRebateStaking(RebateStakingProxy)` |
| 326 | + |
| 327 | +This transaction is expected to succeed exactly once for a given |
| 328 | +Bridge implementation; subsequent attempts will revert on-chain. |
| 329 | + |
| 330 | +Post-condition check: |
| 331 | + |
| 332 | +[,bash] |
| 333 | +---- |
| 334 | +cast call 0x5e4861a80B55f035D899f66772117F00FA0E8e7B \ |
| 335 | + "getRebateStaking()(address)" |
| 336 | +
|
| 337 | +# Expected: 0x0184739C32edc3471D3e4860c8E39a5f3Ff85A45 |
| 338 | +---- |
| 339 | + |
| 340 | +== Final Verification |
| 341 | + |
| 342 | +After all actions above are executed, perform the following checks: |
| 343 | + |
| 344 | +[,bash] |
| 345 | +---- |
| 346 | +# 1. NEW BridgeGovernance controls the Bridge |
| 347 | +cast call 0x5e4861a80B55f035D899f66772117F00FA0E8e7B \ |
| 348 | + "governance()(address)" |
| 349 | +
|
| 350 | +# Expected: <NEW_BRIDGE_GOVERNANCE> |
| 351 | +
|
| 352 | +# 2. RebateStaking is connected to the Bridge |
| 353 | +cast call 0x5e4861a80B55f035D899f66772117F00FA0E8e7B \ |
| 354 | + "getRebateStaking()(address)" |
| 355 | +
|
| 356 | +# Expected: 0x0184739C32edc3471D3e4860c8E39a5f3Ff85A45 |
| 357 | +
|
| 358 | +# 3. (Optional) Run the existing verification script |
| 359 | +npx hardhat deploy --network mainnet --tags VerifyRebateDeployment |
| 360 | +
|
| 361 | +== Governance follow-up overview |
| 362 | +
|
| 363 | +After Action 1 produces the new governance address, the Council EOA (the owner |
| 364 | +of the OLD `BridgeGovernance`) must: (1) call |
| 365 | +`beginBridgeGovernanceTransfer(<NEW_BRIDGE_GOVERNANCE>)`, (2) wait the 48-hour |
| 366 | +delay, and (3) call `finalizeBridgeGovernanceTransfer()` to hand control of the |
| 367 | +Bridge proxy to the new contract. All calldata templates above are illustrative; |
| 368 | +replace `<NEW_BRIDGE_GOVERNANCE>` with the coordinate address generated in |
| 369 | +Action 1. No other state-changing calls are required until Action 4. |
| 370 | +---- |
| 371 | + |
| 372 | +If all checks pass, the tBTC Rebate System is fully wired and |
| 373 | +governable via the new BridgeGovernance contract. |
0 commit comments