Skip to content

chore(ens): draft - Empowering the ENS Foundation - #101

Open
alextnetto wants to merge 5 commits into
mainfrom
ens/ep-empowering-ens-foundation
Open

chore(ens): draft - Empowering the ENS Foundation#101
alextnetto wants to merge 5 commits into
mainfrom
ens/ep-empowering-ens-foundation

Conversation

@alextnetto

@alextnetto alextnetto commented Aug 1, 2026

Copy link
Copy Markdown
Member

Draft calldata review — Empowering the ENS Foundation

Review of the Tally draft by nick.eth. Calldata was derived manually from the proposal spec and matches the draft byte-for-byte. Test simulates the full lifecycle and passes.

What it does

  1. Sends 1M ENS from the DAO treasury to the Foundation Safe (0x9C7d…A19E, 3-of-5: nick, alexurbelis, kartik + 2 unlabeled EOAs).
  2. Swaps the owner of the Endowment Safe (~$65M) from the DAO timelock to a new EndowmentTimelock (0x0bcC…406C).

Impact: the DAO gives up direct control of the Endowment. After this executes, only the Foundation Safe can initiate Endowment transactions, everything waits 9 days, and the Security Council can cancel during that window (until Aug 2028). The DAO keeps no role on the new timelock — reverting this later requires the Foundation's cooperation.

What checks out

  • New timelock is stock OZ TimelockController v4.3.2, 9-day delay, deployer renounced admin, nothing pre-scheduled. Role changes must go through its own delayed, vetoable path.
  • SC veto contract is the same bytecode as the audited security-council contract, cancel-only, owned by the current 5-of-8 SC Safe. The test exercises the veto and the delay for real.
  • Endowment's sole owner today is the DAO timelock with threshold 1, so the swapOwner call is correctly formed.
  • karpatkey's modules are untouched.

Findings (none block the calldata)

  • The proposal text references a "Transactions section" that doesn't exist, and none of the new addresses appear in the text — they're only in the calldata. Author should add them before submitting.
  • "All Endowment transactions pass through a timelock" is true for the owner path only. Two module paths bypass it and survive the swap: karpatkey's Roles module (disclosed) and a Safe Allowance Module giving the MetaGov WG a 30 ETH / 25-day spending limit (not mentioned in the proposal).
  • The 1M ENS lands unencumbered — vesting/comp rules are off-chain commitments.
  • Two of the five Foundation signers are fresh EOAs with no on-chain identity. Worth a public confirmation from the named board members.

Reproduce

forge test --match-path "src/ens/proposals/ep-empowering-ens-foundation/*" -vv

Tally draft 2913928210729141431 by nick.eth. Two calls:
1. transfer 1,000,000 ENS to the Foundation Safe (3-of-5)
2. swapOwner on the Endowment Safe: DAO timelock -> EndowmentTimelock
   (OZ TimelockController v4.3.2, 9-day delay, Foundation Safe as
   proposer, Security Council veto wrapper with cancel rights)

Manually derived calldata matches the draft byte-for-byte.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2d95846c34

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +77 to +78
(endowmentModulesBefore,) = endowmentSafe.getModulesPaginated(SENTINEL_OWNERS, 10);
assertEq(endowmentModulesBefore.length, 2, "endowment: expected 2 enabled modules");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Pin the enabled Endowment modules

The assertion accepts any two enabled modules, even though these modules bypass the new timelock and can execute Endowment transactions directly. If an unintended module was enabled at the reviewed fork, the test would snapshot and preserve it while still reporting that the expected Karpatkey modules were untouched; assert the two expected module addresses rather than only their count.

Useful? React with 👍 / 👎.

Comment on lines +100 to +102
assertEq(foundationSafe.getOwners().length, 5);
assertEq(foundationSafe.getThreshold(), 3);
assertTrue(foundationSafe.isOwner(_proposer()), "nick.eth must be a Foundation Safe owner");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Verify every Foundation Safe owner

This only proves that the Safe has five owners and that one is the proposer; the other four can be arbitrary addresses while the review still passes. Because this Safe receives the ENS grant and is the sole operational proposer for the Endowment timelock, assert that its complete owner set matches the five approved board signers.

Useful? React with 👍 / 👎.

Comment on lines +82 to +85
assertTrue(endowmentTimelock.hasRole(PROPOSER_ROLE, ENS_FOUNDATION_SAFE));
assertTrue(endowmentTimelock.hasRole(PROPOSER_ROLE, SC_VETO_CONTRACT));
assertTrue(endowmentTimelock.hasRole(EXECUTOR_ROLE, address(0)), "executor role must be open");
assertTrue(endowmentTimelock.hasRole(TIMELOCK_ADMIN_ROLE, ENDOWMENT_TIMELOCK), "self-administered");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Prove the Timelock has no additional privileged accounts

These positive role checks do not establish the claimed role configuration: an unexpected proposer or TIMELOCK_ADMIN_ROLE holder can coexist with the expected accounts while every assertion passes. Such an account could schedule Endowment transactions or grant and revoke privileged roles, so the review needs to validate the complete grant/revoke history or otherwise prove the exact privileged membership rather than checking only selected addresses.

Useful? React with 👍 / 👎.

ISecurityCouncil scVeto = ISecurityCouncil(SC_VETO_CONTRACT);
assertEq(scVeto.owner(), SECURITY_COUNCIL_SAFE);
assertEq(scVeto.timelock(), ENDOWMENT_TIMELOCK);
assertGt(scVeto.expiration(), block.timestamp, "veto power must not be expired");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Pin the Security Council veto expiration

Checking only that the wrapper has not expired at the fork allows an expiration shortly after the simulated proposal lifecycle to pass, even though the review claims the veto remains active until August 2028. In that scenario queued Endowment operations would lose Security Council oversight much earlier than reviewed, so assert the expected expiration timestamp exactly.

Useful? React with 👍 / 👎.

Comment on lines +93 to +96
ISecurityCouncil scVeto = ISecurityCouncil(SC_VETO_CONTRACT);
assertEq(scVeto.owner(), SECURITY_COUNCIL_SAFE);
assertEq(scVeto.timelock(), ENDOWMENT_TIMELOCK);
assertGt(scVeto.expiration(), block.timestamp, "veto power must not be expired");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Pin the veto wrapper implementation

The getter assertions and successful veto call do not prove that this privileged proposer is the claimed cancel-only wrapper. A different implementation could return the expected owner, timelock, and expiration while exposing an additional path that schedules arbitrary Endowment operations, and the review would still pass; assert the reviewed runtime bytecode or code hash so the veto-only property is reproducible.

Useful? React with 👍 / 👎.

- Foundation cannot revoke the SC wrapper's PROPOSER_ROLE directly
  (role admin = the timelock itself, so changes are delayed + vetoable)
- updateDelay reverts unless called by the timelock itself
- the SC wrapper does not forward schedule calls (cancel-only)
- role-admin invariants asserted in _beforeProposal
@alextnetto alextnetto changed the title chore(ens): draft calldata review — Empowering the ENS Foundation chore(ens): draft - Empowering the ENS Foundation Aug 1, 2026
- pin both Endowment module addresses (Zodiac Roles + Allowance Module)
  and require the pagination next-pointer to prove list completeness
- pin all five Foundation Safe owners exactly
- negative role checks for every plausible privileged candidate +
  document the complete role event history (10 events, deploy..renounce)
- pin SC veto expiration to 1849227635 (2028-08-07)
- pin runtime codehashes of the EndowmentTimelock and the veto wrapper
@alextnetto

Copy link
Copy Markdown
Member Author

Addressed all five Codex P1 comments in d5171bc:

  1. Modules pinned — both Endowment module addresses asserted exactly (Zodiac Roles 0x7038…0A40 + Safe Allowance Module 0xCFbF…3134), and the pagination next pointer is asserted to be the sentinel to prove the list is complete.
  2. All 5 Foundation Safe owners pinned exactly (nick.eth, alexurbelis.eth, kartik.eth + the two EOAs without reverse records, which remain flagged for public confirmation).
  3. Privileged-set completeness — AccessControl isn't enumerable on-chain, so exact membership was proven off-chain from the complete event history (10 events between deploy block 25,656,954 and the admin renounce at 25,657,026, nothing since — documented in the test comment); in-test, negative role checks now pin every plausible privileged candidate (DAO timelock, Governor, SC Safe, karpatkey, deployer, wrapper).
  4. Veto expiration pinned to 1849227635 (2028-08-07 UTC) with assertEq, not just non-expiry.
  5. Codehashes pinned for both the EndowmentTimelock and the SC veto wrapper, making the off-chain bytecode findings (OZ v4.3.2 timelock; cancel-only wrapper identical to the verified blockful/security-council-ens deployment modulo the timelock immutable) reproducible from the test alone.

Prove on-fork that neither Endowment module can alter the Safe's owner
set: karpatkey's MANAGER role rejects swapOwner via execTransactionWithRole,
and non-modules are stopped at execTransactionFromModule (GS104). Closes
the last probe-only leg of the 'no on-chain eviction path' claim.
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