I designed the lifecycle as a one-way state machine. Only the timelock can advance states — nothing else has that authority.
COMMITTED → REVEALED → APPROVED → QUEUED → EXECUTED
↘
CANCELED
Any address with sufficient bond collateral calls commit(bytes32 commitHash) on AresProposalBook. The hash is keccak256(abi.encode(actions[], salt, proposer, block.chainid)). No action content is visible yet. The bond is locked, a proposalId is assigned, and a minRevealDelay timer starts. Duplicate hashes revert. A proposer can't have two active commits simultaneously.
After minRevealDelay, the proposer calls reveal(proposalId, actions[], salt). The contract recomputes the hash and checks it against what was committed — mismatches revert, so actions can't be swapped after the commit lands. A maximum reveal window is also enforced to prevent commits from sitting dormant indefinitely. State advances to REVEALED.
Council members sign an EIP-712 struct off-chain containing proposalId, actionsHash, nonce, chainId, and council address. Anyone submits the collected signatures via approve(proposalId, signatures[]) on AresApprovalCouncil. Each signature is checked for canonical low-s form, valid v, and an active council member. Duplicates are rejected. The nonce is consumed atomically — those signatures can never be resubmitted. If valid signers fall below threshold, the call reverts. State advances to APPROVED.
Anyone calls queue(proposalId) on AresTimelock. The timelock confirms approval status, then schedules each action with eta = block.timestamp + minDelay. Actions are stored keyed by content hash. Duplicate queue entries revert. State advances to QUEUED.
Anyone calls execute(proposalId, actionIndex) after the ETA passes. The timelock checks the timestamp window (eta <= now <= eta + gracePeriod), then calls guardRail.preExecute(action) to verify the target is allowlisted, the selector is permitted, and the ETH value fits within the daily cap. If all checks pass, the timelock routes to treasury.execute(action). The action is marked EXECUTED in storage before the external call runs. State becomes EXECUTED once all actions complete.
The vetoGuardian, proposal-book guardian, or the original proposer (pre-queue) can call cancel(proposalId). Guardian cancellations slash the bond. Proposer self-cancellations before queuing return the bond. Queue entries are deleted. State becomes CANCELED permanently.
The timelock sets epoch roots via setEpochRoot(epoch, root) through the full proposal pipeline. Recipients call claim(epoch, index, recipient, amount, proof[]). The contract verifies the merkle proof against the epoch root and checks a per-epoch bitmap to prevent double claims. Each claimed index flips a bit — 256 flags per storage slot.