diff --git a/src/ens/proposals/ep-registrar-manager-endowment/audits/2026-03-23-cyfrin-registrar-manager-v2.0.pdf b/src/ens/proposals/ep-registrar-manager-endowment/audits/2026-03-23-cyfrin-registrar-manager-v2.0.pdf new file mode 100644 index 0000000..97d5b4a Binary files /dev/null and b/src/ens/proposals/ep-registrar-manager-endowment/audits/2026-03-23-cyfrin-registrar-manager-v2.0.pdf differ diff --git a/src/ens/proposals/ep-registrar-manager-endowment/calldataCheck.t.sol b/src/ens/proposals/ep-registrar-manager-endowment/calldataCheck.t.sol index f315155..90ca11b 100644 --- a/src/ens/proposals/ep-registrar-manager-endowment/calldataCheck.t.sol +++ b/src/ens/proposals/ep-registrar-manager-endowment/calldataCheck.t.sol @@ -32,6 +32,8 @@ interface IRegistrarController { contract Proposal_ENS_EP_Registrar_Manager_Endowment_Test is ENS_Governance, SafeHelper, ZodiacRolesHelper { IERC20 public constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + IRegistrarController public constant CURRENT_REGISTRAR = + IRegistrarController(0x253553366Da8546fC250F225fe3d25d0C782303b); IRegistrarController public constant NEW_REGISTRAR = IRegistrarController(0x59E16fcCd424Cc24e280Be16E11Bcd56fb0CE547); IRegistrarController public constant OLD_REGISTRAR = @@ -52,6 +54,7 @@ contract Proposal_ENS_EP_Registrar_Manager_Endowment_Test is ENS_Governance, Saf } function _beforeProposal() public override { + assertEq(CURRENT_REGISTRAR.owner(), address(timelock), "Current registrar owner should be timelock"); assertEq(NEW_REGISTRAR.owner(), address(timelock), "New registrar owner should be timelock"); assertEq(OLD_REGISTRAR.owner(), address(timelock), "Old registrar owner should be timelock"); @@ -70,7 +73,7 @@ contract Proposal_ENS_EP_Registrar_Manager_Endowment_Test is ENS_Governance, Saf string memory ) { - uint256 numTransactions = 8; + uint256 numTransactions = 10; targets = new address[](numTransactions); values = new uint256[](numTransactions); @@ -79,56 +82,64 @@ contract Proposal_ENS_EP_Registrar_Manager_Endowment_Test is ENS_Governance, Saf address managerAddr = address(manager); - // 1) Register new registrar controller + // 1) Register current registrar controller (controller.ens.eth) targets[0] = managerAddr; - calldatas[0] = abi.encodeWithSelector(RegistrarManager.addRegistrar.selector, address(NEW_REGISTRAR)); + calldatas[0] = abi.encodeWithSelector(RegistrarManager.addRegistrar.selector, address(CURRENT_REGISTRAR)); - // 2) Register old registrar controller + // 2) Register new registrar controller targets[1] = managerAddr; - calldatas[1] = abi.encodeWithSelector(RegistrarManager.addRegistrar.selector, address(OLD_REGISTRAR)); + calldatas[1] = abi.encodeWithSelector(RegistrarManager.addRegistrar.selector, address(NEW_REGISTRAR)); - // 3) Transfer ownership of new registrar to RegistrarManager - targets[2] = address(NEW_REGISTRAR); - calldatas[2] = abi.encodeWithSelector(IRegistrarController.transferOwnership.selector, managerAddr); + // 3) Register old registrar controller + targets[2] = managerAddr; + calldatas[2] = abi.encodeWithSelector(RegistrarManager.addRegistrar.selector, address(OLD_REGISTRAR)); - // 4) Transfer ownership of old registrar to RegistrarManager - targets[3] = address(OLD_REGISTRAR); + // 4) Transfer ownership of current registrar to RegistrarManager + targets[3] = address(CURRENT_REGISTRAR); calldatas[3] = abi.encodeWithSelector(IRegistrarController.transferOwnership.selector, managerAddr); - // 5) Zodiac: scope USDC target + // 5) Transfer ownership of new registrar to RegistrarManager + targets[4] = address(NEW_REGISTRAR); + calldatas[4] = abi.encodeWithSelector(IRegistrarController.transferOwnership.selector, managerAddr); + + // 6) Transfer ownership of old registrar to RegistrarManager + targets[5] = address(OLD_REGISTRAR); + calldatas[5] = abi.encodeWithSelector(IRegistrarController.transferOwnership.selector, managerAddr); + + // 7) Zodiac: scope USDC target { bytes memory inner = abi.encodeWithSelector(IRolesModifier.scopeTarget.selector, MANAGER_ROLE, address(USDC)); - (targets[4], calldatas[4]) = _buildSafeExecCalldata( + (targets[6], calldatas[6]) = _buildSafeExecCalldata( address(endowmentSafe), address(ROLES_MOD), inner, address(timelock) ); } - // 6) Zodiac: allow USDC.transfer(timelock, amount) + // 8) Zodiac: allow USDC.transfer(timelock, amount) { ConditionFlat[] memory conditions = _usdcTransferConditions(); bytes memory inner = abi.encodeWithSelector( IRolesModifier.scopeFunction.selector, MANAGER_ROLE, address(USDC), IERC20.transfer.selector, conditions, uint8(0) ); - (targets[5], calldatas[5]) = _buildSafeExecCalldata( + (targets[7], calldatas[7]) = _buildSafeExecCalldata( address(endowmentSafe), address(ROLES_MOD), inner, address(timelock) ); } - // 7) Zodiac: scope timelock target + // 9) Zodiac: scope timelock target { bytes memory inner = abi.encodeWithSelector(IRolesModifier.scopeTarget.selector, MANAGER_ROLE, address(timelock)); - (targets[6], calldatas[6]) = _buildSafeExecCalldata( + (targets[8], calldatas[8]) = _buildSafeExecCalldata( address(endowmentSafe), address(ROLES_MOD), inner, address(timelock) ); } - // 8) Zodiac: allow ETH sends to timelock (empty calldata, send-only) + // 10) Zodiac: allow ETH sends to timelock (empty calldata, send-only) { bytes memory inner = abi.encodeWithSelector( IRolesModifier.allowFunction.selector, MANAGER_ROLE, address(timelock), bytes4(0), EXEC_SEND ); - (targets[7], calldatas[7]) = _buildSafeExecCalldata( + (targets[9], calldatas[9]) = _buildSafeExecCalldata( address(endowmentSafe), address(ROLES_MOD), inner, address(timelock) ); } @@ -142,17 +153,20 @@ contract Proposal_ENS_EP_Registrar_Manager_Endowment_Test is ENS_Governance, Saf // RegistrarManager state assertEq(manager.owner(), address(timelock), "RegistrarManager owner should be timelock"); assertEq(manager.destination(), address(endowmentSafe), "Destination should be endowment safe"); + assertTrue(manager.isRegistrar(address(CURRENT_REGISTRAR)), "Current registrar not registered"); assertTrue(manager.isRegistrar(address(NEW_REGISTRAR)), "New registrar not registered"); assertTrue(manager.isRegistrar(address(OLD_REGISTRAR)), "Old registrar not registered"); // Ownership transferred to manager address managerAddr = address(manager); + assertEq(CURRENT_REGISTRAR.owner(), managerAddr, "Current registrar owner should be manager"); assertEq(NEW_REGISTRAR.owner(), managerAddr, "New registrar owner should be manager"); assertEq(OLD_REGISTRAR.owner(), managerAddr, "Old registrar owner should be manager"); // WithdrawAll uint256 balanceBefore = address(endowmentSafe).balance; - uint256 registrarBalance = address(NEW_REGISTRAR).balance + address(OLD_REGISTRAR).balance; + uint256 registrarBalance = + address(CURRENT_REGISTRAR).balance + address(NEW_REGISTRAR).balance + address(OLD_REGISTRAR).balance; uint256 managerBalance = address(manager).balance; manager.withdrawAll(); uint256 balanceAfter = address(endowmentSafe).balance; @@ -163,9 +177,13 @@ contract Proposal_ENS_EP_Registrar_Manager_Endowment_Test is ENS_Governance, Saf "Endowment balance should increase after withdraw" ); - // Zodiac role permissions + // Zodiac role permissions — positive tests _expectUSDCTransferAllowed(); _expectEthSendAllowed(); + + // Zodiac role permissions — negative tests (scoping verification) + _expectUSDCTransferToNonTimelockBlocked(); + _expectEthSendToNonTimelockBlocked(); } function _expectUSDCTransferNotAllowed() internal { @@ -209,6 +227,26 @@ contract Proposal_ENS_EP_Registrar_Manager_Endowment_Test is ENS_Governance, Saf assertEq(balanceAfter, balanceBefore + amount, "ETH balance should increase after transfer"); } + function _expectUSDCTransferToNonTimelockBlocked() internal { + address notTimelock = address(0xdead); + uint256 amount = 1000000; + + vm.startPrank(karpatkey); + bytes memory data = abi.encodeWithSelector(IERC20.transfer.selector, notTimelock, amount); + vm.expectRevert(); + roles.execTransactionWithRole(address(USDC), 0, data, IZodiacRoles.Operation.Call, MANAGER_ROLE, false); + vm.stopPrank(); + } + + function _expectEthSendToNonTimelockBlocked() internal { + address notTimelock = address(0xdead); + + vm.startPrank(karpatkey); + vm.expectRevert(); + roles.execTransactionWithRole(notTimelock, 1 ether, "", IZodiacRoles.Operation.Call, MANAGER_ROLE, false); + vm.stopPrank(); + } + function _usdcTransferConditions() internal view returns (ConditionFlat[] memory) { ConditionFlat[] memory conditions = new ConditionFlat[](3); conditions[0] = ConditionFlat({ @@ -232,6 +270,10 @@ contract Proposal_ENS_EP_Registrar_Manager_Endowment_Test is ENS_Governance, Saf return conditions; } + function dirPath() public pure override returns (string memory) { + return "src/ens/proposals/ep-registrar-manager-endowment"; + } + function _isProposalSubmitted() public pure override returns (bool) { return false; } diff --git a/src/ens/proposals/ep-registrar-manager-endowment/proposalCalldata.json b/src/ens/proposals/ep-registrar-manager-endowment/proposalCalldata.json new file mode 100644 index 0000000..9951b80 --- /dev/null +++ b/src/ens/proposals/ep-registrar-manager-endowment/proposalCalldata.json @@ -0,0 +1,66 @@ +{ + "proposalId": "2822069893434705582", + "type": "draft", + "executableCalls": [ + { + "target": "0x62627681D92e36b9aeE1D9A6BF181373ccd42552", + "calldata": "0xaf92a693000000000000000000000000253553366da8546fc250f225fe3d25d0c782303b", + "value": "0", + "signature": "" + }, + { + "target": "0x62627681D92e36b9aeE1D9A6BF181373ccd42552", + "calldata": "0xaf92a69300000000000000000000000059e16fccd424cc24e280be16e11bcd56fb0ce547", + "value": "0", + "signature": "" + }, + { + "target": "0x62627681D92e36b9aeE1D9A6BF181373ccd42552", + "calldata": "0xaf92a693000000000000000000000000283af0b28c62c092c9727f1ee09c02ca627eb7f5", + "value": "0", + "signature": "" + }, + { + "target": "0x253553366Da8546fC250F225fe3d25d0C782303b", + "calldata": "0xf2fde38b00000000000000000000000062627681d92e36b9aee1d9a6bf181373ccd42552", + "value": "0", + "signature": "" + }, + { + "target": "0x59E16fcCd424Cc24e280Be16E11Bcd56fb0CE547", + "calldata": "0xf2fde38b00000000000000000000000062627681d92e36b9aee1d9a6bf181373ccd42552", + "value": "0", + "signature": "" + }, + { + "target": "0x283Af0B28c62C092C9727F1Ee09c02CA627EB7F5", + "calldata": "0xf2fde38b00000000000000000000000062627681d92e36b9aee1d9a6bf181373ccd42552", + "value": "0", + "signature": "" + }, + { + "target": "0x4F2083f5fBede34C2714aFfb3105539775f7FE64", + "calldata": "0x6a761202000000000000000000000000703806e61847984346d2d7ddd853049627e50a400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000440c6c76b84d414e4147455200000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041000000000000000000000000fe89cc7abb2c4183683ab71653c4cdc9b02d44b700000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000", + "value": "0", + "signature": "" + }, + { + "target": "0x4F2083f5fBede34C2714aFfb3105539775f7FE64", + "calldata": "0x6a761202000000000000000000000000703806e61847984346d2d7ddd853049627e50a400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004a000000000000000000000000000000000000000000000000000000000000003247508dd984d414e4147455200000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48a9059cbb0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000020000000000000000000000000fe89cc7abb2c4183683ab71653c4cdc9b02d44b700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041000000000000000000000000fe89cc7abb2c4183683ab71653c4cdc9b02d44b700000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000", + "value": "0", + "signature": "" + }, + { + "target": "0x4F2083f5fBede34C2714aFfb3105539775f7FE64", + "calldata": "0x6a761202000000000000000000000000703806e61847984346d2d7ddd853049627e50a400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000440c6c76b84d414e4147455200000000000000000000000000000000000000000000000000000000000000000000000000fe89cc7abb2c4183683ab71653c4cdc9b02d44b7000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041000000000000000000000000fe89cc7abb2c4183683ab71653c4cdc9b02d44b700000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000", + "value": "0", + "signature": "" + }, + { + "target": "0x4F2083f5fBede34C2714aFfb3105539775f7FE64", + "calldata": "0x6a761202000000000000000000000000703806e61847984346d2d7ddd853049627e50a400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000084b3dd25c74d414e4147455200000000000000000000000000000000000000000000000000000000000000000000000000fe89cc7abb2c4183683ab71653c4cdc9b02d44b700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041000000000000000000000000fe89cc7abb2c4183683ab71653c4cdc9b02d44b700000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000", + "value": "0", + "signature": "" + } + ] +} \ No newline at end of file diff --git a/src/ens/proposals/ep-registrar-manager-endowment/proposalDescription.md b/src/ens/proposals/ep-registrar-manager-endowment/proposalDescription.md index 488b1a7..38d087e 100644 --- a/src/ens/proposals/ep-registrar-manager-endowment/proposalDescription.md +++ b/src/ens/proposals/ep-registrar-manager-endowment/proposalDescription.md @@ -1,154 +1,73 @@ - # [Executable] Registrar Manager + Endowment Roles Update (Draft) - - ## Abstract - -This proposal introduces a RegistrarManager that can permissionlessly withdraw ETH from ENS registrar controllers and forward funds to a configurable destination (initially the Endowment Safe). It also updates the Endowment Zodiac Roles permissions so the Endowment Manager can transfer ETH and USDC to the ENS Timelock only. - - ## Motivation - -Registrar income currently flows to the Timelock and requires a proposal to move funds into the Endowment. The RegistrarManager removes this bottleneck by allowing permissionless withdrawals from registrars and routing proceeds directly to the Endowment. Separately, limited treasury transfers (ETH/USDC) to the Timelock are required for operational funding; these are scoped to the Timelock only. - - ## Specification - - ### Description - -This proposal: - -1. Deploys `RegistrarManager` contract. -2. Registers the current and legacy registrar controllers in the manager. -3. Transfers registrar controller ownership to the manager. -4. Updates Zodiac Roles for the Endowment Manager (MANAGER role) to allow: - - `USDC.transfer(timelock, amount)` with no amount cap. - - ETH sends to the Timelock only (empty calldata, send-only). - - ### Transactions Summary - -This proposal contains **8** transaction(s) to be executed by the ENS DAO Timelock. - -| # | Contract | Function | Description | -| - | -------- | -------- | ----------- | -| 1 | RegistrarManager | `addRegistrar` | Register new ETH Registrar Controller | -| 2 | RegistrarManager | `addRegistrar` | Register legacy ETH Registrar Controller | -| 3 | ETH Registrar Controller 2 | `transferOwnership` | Transfer ownership to RegistrarManager | -| 4 | Old ETH Registrar Controller | `transferOwnership` | Transfer ownership to RegistrarManager | -| 5 | Endowment Safe | `execTransaction` | Zodiac `scopeTarget` for USDC | -| 6 | Endowment Safe | `execTransaction` | Zodiac `scopeFunction` for `USDC.transfer(timelock, amount)` | -| 7 | Endowment Safe | `execTransaction` | Zodiac `scopeTarget` for Timelock | -| 8 | Endowment Safe | `execTransaction` | Zodiac `allowFunction` for ETH sends to Timelock | - - --- - -## Detailed Transaction Information - -**Note:** The `RegistrarManager` contract has been deployed at [`0x62627681D92e36b9aeE1D9A6BF181373ccd42552`](https://etherscan.io/address/0x62627681D92e36b9aeE1D9A6BF181373ccd42552) with the Timelock as owner and Endowment Safe as destination. - -### Transaction 1: Register new ETH Registrar Controller - -**Target:** RegistrarManager -**Address:** `0x62627681D92e36b9aeE1D9A6BF181373ccd42552` -**Function:** `addRegistrar` - -**Parameters:** -* `registrar`: `0x59E16fcCd424Cc24e280Be16E11Bcd56fb0CE547` - -**Encoded Calldata:** `TBD` - - --- - - ### Transaction 2: Register legacy ETH Registrar Controller - -**Target:** RegistrarManager -**Address:** `0x62627681D92e36b9aeE1D9A6BF181373ccd42552` -**Function:** `addRegistrar` - -**Parameters:** -* `registrar`: `0x283Af0B28c62C092C9727F1Ee09c02CA627EB7F5` - -**Encoded Calldata:** `TBD` - - --- - - ### Transaction 3: Transfer ownership of ETH Registrar Controller 2 - -**Target:** ENS: ETH Registrar Controller 2 -**Address:** `0x59E16fcCd424Cc24e280Be16E11Bcd56fb0CE547` -**Function:** `transferOwnership` - -**Parameters:** -* `newOwner`: `0x62627681D92e36b9aeE1D9A6BF181373ccd42552` - -**Encoded Calldata:** `TBD` - - --- - - ### Transaction 4: Transfer ownership of Old ETH Registrar Controller - -**Target:** ENS: Old ETH Registrar Controller -**Address:** `0x283Af0B28c62C092C9727F1Ee09c02CA627EB7F5` -**Function:** `transferOwnership` - -**Parameters:** -* `newOwner`: `0x62627681D92e36b9aeE1D9A6BF181373ccd42552` - -**Encoded Calldata:** `TBD` - - --- - - ### Transaction 5: Zodiac scopeTarget for USDC - -**Target:** ENS Endowment Safe -**Address:** `0x4F2083f5fBede34C2714aFfb3105539775f7FE64` -**Function:** `execTransaction` - -**Inner Call:** `Roles.scopeTarget(MANAGER_ROLE, USDC)` - -**Encoded Calldata:** `TBD` - - --- - - ### Transaction 6: Zodiac scopeFunction for USDC transfer - -**Target:** ENS Endowment Safe -**Address:** `0x4F2083f5fBede34C2714aFfb3105539775f7FE64` -**Function:** `execTransaction` - -**Inner Call:** `Roles.scopeFunction(MANAGER_ROLE, USDC, transfer, conditions, options=0)` - -**Conditions:** `USDC.transfer(timelock, amount)` -**Encoded Calldata:** `TBD` - - --- - - ### Transaction 7: Zodiac scopeTarget for Timelock - -**Target:** ENS Endowment Safe -**Address:** `0x4F2083f5fBede34C2714aFfb3105539775f7FE64` -**Function:** `execTransaction` - -**Inner Call:** `Roles.scopeTarget(MANAGER_ROLE, timelock)` - -**Encoded Calldata:** `TBD` - - --- - - ### Transaction 8: Zodiac allowFunction for ETH sends to Timelock - -**Target:** ENS Endowment Safe -**Address:** `0x4F2083f5fBede34C2714aFfb3105539775f7FE64` -**Function:** `execTransaction` - -**Inner Call:** `Roles.allowFunction(MANAGER_ROLE, timelock, 0x00000000, options=Send)` - -**Encoded Calldata:** `TBD` - - --- - -## Notes / Assumptions - -* `RegistrarManager` contract deployed at [`0x62627681D92e36b9aeE1D9A6BF181373ccd42552`](https://etherscan.io/address/0x62627681D92e36b9aeE1D9A6BF181373ccd42552) with constructor arguments `(timelock, endowmentSafe)`. -* The Endowment Safe is the owner of the Roles modifier, so Zodiac updates are executed via Safe `execTransaction`. - -## References - -* ENS Endowment Safe: `0x4F2083f5fBede34C2714aFfb3105539775f7FE64` -* ENS Timelock: `0xFe89cc7aBB2C4183683ab71653C4cdc9B02D44b7` +# [EP 6.39] [Executable] Treasury Flow Automation +## Abstract + +ENS protocol revenue currently requires three separate steps to move from registrar controllers to productive use in the endowment and fund operations. This proposal introduces a Registrar Manager contract that takes ownership of all registrar controllers and enables permissionless withdrawals directly to the endowment. It also configures a Zodiac module permission on the endowment to allow the treasury manager to send ETH and USDC to the timelock without a proposal, following a two-year stablecoin runway policy consistent with the current Investment Policy Statement. + +The result is zero proposals for routine treasury operations, faster yield on collected revenue, permissionless withdrawals, and increased income for the DAO. A conservative estimate suggests that since January 2024, approximately $1 million in yield was missed due to idle capital in registrar controllers and the timelock. + +![](https://discuss.ens.domains/uploads/db9688/original/2X/3/398d621d97bf4820a875d790f220cab125667c48.png) + +## Specification + +### Problem + +ENS protocol revenue requires three steps to move from collection to productive use. +Registrar controllers collect ETH from .eth registrations and renewals, and the current controller (controller.ens.eth) already sends withdrawn ETH to the timelock. However, the old registrar controller at 0x283Af0B28c62C092C9727F1Ee09c02CA627eb7F5 holds roughly 772 ETH that requires a dedicated proposal just to withdraw. + +Once ETH reaches the timelock, sending it to the endowment (endowment.ensdao.eth) for investment requires another proposal. Capital can sit idle in the timelock for several months before it starts earning yield. + +When the DAO needs to fund operational expenses like ENS Labs, the Service Provider Program, or Working Groups, yet another proposal is needed to transfer from the endowment back to the timelock or to execute operations with [twap.ensdao.eth](https://etherscan.io/address/0x02D61347e5c6EA5604f3f814C5b5498421cEBdEB). For context, [EP 6.32](https://discuss.ens.domains/t/ep-6-32-executable-transfer-2-5m-usdc-from-endowment-to-wallet-ensdao-eth/21882) proposed a $2.5M USDC transfer from the endowment to the timelock just to cover Working Group budgets that had already been approved. + +On top of that, there is currently roughly 4,148 ETH sitting in the timelock that could be earning yield in the endowment. The operational funding process today is reactive and not smooth. Each transfer requires a full governance cycle, and capital that could be generating yield sits idle throughout. + +#### Key Addresses + +• Timelock: 0xFe89cc7aBB2C4183683ab71653C4cdc9B02D44b7 (wallet.ensdao.eth) +• Current Registrar Controller: 0x253553366Da8546fC250F225fE3d25d0C782303b (controller.ens.eth) +• Old Registrar Controller (~772 ETH): 0x283Af0B28c62C092C9727F1Ee09c02CA627eb7F5 +• Endowment: 0x4F2083f5fBede34C2714aFfb3105539775f7FE64 (endowment.ensdao.eth) +• Governor: 0x323A76393544d5ecca80cd6ef2A560C6a395b7E3 (governor.ensdao.eth) +• TWAP safe: 0x02D61347e5c6EA5604f3f814C5b5498421cEBdEB (twap.ensdao.eth) + +### Solution + +This proposal introduces two components that eliminate all three bottlenecks: a Registrar Manager and an Endowment Zodiac Module permission. + +#### Registrar Manager + +The Registrar Manager is a contract that becomes the owner of all registrar controllers. Its key properties: + +a) Exposes a permissionless withdraw() function that anyone can call. When called, it pulls ETH from each controller and routes it directly to the endowment. + +b) The destination address is configurable by the DAO through the timelock, so governance can redirect the flow at any time. + +c) Acts as a pass-through for governance calls, meaning the DAO retains full control over controller parameters. + +d) New controllers can be added over time. + +e) Never holds funds. + +[Source code of Registrar Manager contract.](https://github.com/blockful/dao-proposals/blob/ep-registrar-manager-endowment/src/ens/proposals/ep-registrar-manager-endowment/contracts/RegistrarManager.sol) + +#### Endowment Zodiac Module + +The Zodiac module is already part of how the endowment is managed. This proposal adds a scoped permission that allows the treasury manager (Karpatkey) to send ETH and USDC to wallet.ensdao.eth without a proposal. It cannot send to any other address or use any other token. All other endowment operations remain unchanged. + +### Funding Policy + +This is the current suggestion for the funding policy. + +The timelock maintains a 6 months runway in USDC (~$8M at current spending). Each quarter, Karpatkey calculates the current runway on the timelock. If it falls below 6 months, the endowment sends the shortfall. If it exceeds 6 months, no transfer is needed and the excess stays invested in the endowment. If an additional governance proposal requiring capital is approved, this may trigger an earlier runway evaluation and transfer, consistent with this policy. + +The initial policy is included in this executable proposal, so no separate vote is needed to get started. +Any future changes to the funding policy require a social vote. This establishes clear expectations about responsibilities and decision-making: the treasury manager handles routine rebalancing within the policy, and the community sets the policy itself. + +This proposal remains aligned with the IPS. The required three-year stablecoin runway is calculated at the Endowment level, including stablecoins held and deployed there, as is currently done. The expansion guidelines reference transferring 33% of protocol revenue to the Endowment. This proposal modifies that mechanism by routing revenue directly to the Endowment to improve operational efficiency and capital deployment. + +### Impact + +The operational funding process is reactive, requiring a full governance cycle for each transfer. Meanwhile, capital that could be earning yield sits idle in controllers and the timelock for several months at a time. + +With this proposal, capital flows from registrars to the endowment as soon as anyone calls withdraw(). The endowment begins generating yield immediately rather than after several months of governance overhead. Operational funding follows a clear quarterly process based on the two-year runway target, removing the need for ad-hoc proposals. + +To put this in perspective: looking at the period from January 2024 until now, and considering ETH staking yields if this capital had been allocated to the endowment, a conservative estimate of $1 million in yield was left on the table. This is based on the balances held in the [timelock](https://etherscan.io/address/0xFe89cc7aBB2C4183683ab71653C4cdc9B02D44b7#analytics), the [current registrar controller](https://etherscan.io/address/0x253553366Da8546fC250F225fE3d25d0C782303b#analytics), and the [old registrar controller](https://etherscan.io/address/0x283Af0B28c62C092C9727F1Ee09c02CA627eb7F5#analytics) (which still have a relevant number of registrations). Going forward, every ETH collected will begin earning yield within days, compounding the DAO's income over time.