SecurityCouncilAzorius is an IGuard-compatible veto guard for Azorius proposal transactions.
On Safe 1.3.0, this guard must be installed on the Azorius module (Azorius.setGuard(...)) to affect module execution.
This repository contains the production contract, deployment script, and verification-focused test suites for unit, lifecycle, and invariants.
| Document | Description |
|---|---|
| Governance Parameters | Current on-chain parameters, suggested changes, before/after timeline, and security analysis |
| Integrations and Addresses | Contract dependencies, per-network address registry, and deployment verification checklist |
| Operations Runbook | Cross-proposal hash collisions, council rotation, guard placement, and deployment controls |
| Security Properties | Core guarantees, event semantics, non-goals, and verification status |
Azorius proposals contain one or more transactions. When all transactions are passed in a single executeProposal call (the standard path), execution is atomic. Azorius also supports partial execution across multiple calls via executionCounter. This guard adds a council-controlled safety layer at execution time:
- Council can veto or unveto at proposal scope (
vetoProposal,unvetoProposal) - Council can veto or unveto at transaction-hash scope (
vetoTx,unvetoTx) - Azorius module execution is blocked if
checkTransactionresolves to a vetoed Azorius transaction hash
- Contract:
src/SecurityCouncilAzorius.sol - Integration points:
- Reads proposal transaction hashes from Azorius (
IAzorius.getProposalTxHashes) - Computes execution hash through Azorius (
IAzorius.getTxHash) - Enforces gate through
IGuard.checkTransactionwhen configured as the Azorius module guard
- Reads proposal transaction hashes from Azorius (
- Authority model:
azoriusis immutable (set at deployment)- council authority is
owner()via OpenZeppelinOwnable(set to_councilat deployment, transferable viatransferOwnership) - only
owner()(the council) can mutate veto state renounceOwnership()is disabled to prevent leaving the guard without a council
Detailed integration and address registry:
docs/INTEGRATIONS_AND_ADDRESSES.md- includes Safe proxy address, Safe singleton address, and Safe contract version tracking per network
- includes current mainnet values for Safe and Azorius plus proposal-fork testing companion contracts
Veto state is stored as:
mapping(bytes32 => bool) public vetoedTxHash;Key implication: state is global by txHash, not scoped by proposalId.
If two proposals include the same tx hash, vetoing either one blocks both execution paths until unvetoed.
vetoProposal(uint32 proposalId)- Loads all tx hashes from Azorius for the proposal
- Marks each non-vetoed hash as vetoed
- Emits per-hash and aggregate events
unvetoProposal(uint32 proposalId)- Clears veto for each currently vetoed hash in that proposal
- Emits per-hash and aggregate events
vetoTx(bytes32 txHash)andunvetoTx(bytes32 txHash)- Fine-grained emergency controls for a single hash
multicall(bytes[] calldata calls)- Allows council to batch internal operations atomically
- Bubbles original revert data if any subcall fails
transferOwnership(address newOwner)(inherited fromOwnable)- Rotates council authority to a new address without guard redeployment
- Existing
vetoedTxHashstate is preserved across ownership transfers
renounceOwnership()(inherited fromOwnable, overridden)- Always reverts with
RenounceOwnershipDisabled()to prevent leaving the guard without a council
- Always reverts with
checkTransaction(...)computes the Azorius tx hash for the pending execution- Reverts with
TransactionVetoed(txHash)when hash is vetoed checkAfterExecution(...)is intentionally a no-op
- Safe
1.3.0does not run Safe transaction guards for module execution (execTransactionFromModule). - Azorius proposal execution uses the module path.
- Therefore veto enforcement requires setting this contract as Azorius guard (
Azorius.setGuard(address(guard))).
isProposalVetoed(uint32 proposalId)returns true only when all proposal hashes are currently vetoed (returns false for proposals with zero tx hashes)supportsInterfacereturns support for:IGuardIERC165
- Deploy guard with
council(initialowner()) and immutableazoriusaddresses. - Install guard on Azorius module (
setGuard) for module-path enforcement. - Verify configuration and run post-activation smoke checks.
- Azorius proposal exists with one or more tx hashes.
- A module execution attempts to run one transaction through Safe.
- Azorius module invokes
checkTransactionon its configured guard. - Guard asks Azorius to compute tx hash from execution params.
- If hash is not vetoed, execution continues.
- Council identifies risky transaction/proposal.
- Council calls
vetoTxorvetoProposal. - Any matching future execution attempt reverts at guard check.
- Council coordinates remediation and governance comms.
- Risk is resolved or governance intent is restored.
- Council calls
unvetoTxorunvetoProposal. - Matching execution paths become available again.
Council authority is owner() via OpenZeppelin Ownable. Rotation is done via transferOwnership(newCouncil) — no guard redeployment needed. Existing vetoedTxHash state is preserved across ownership transfers.
Operational details: docs/OPERATIONS.md
Canonical properties and expectations are documented in:
docs/SECURITY_PROPERTIES.md
Important non-goals:
- This guard does not manage Azorius timelock/execution windows.
- This guard does not maintain proposal lifecycle state.
- This guard is an execution-time veto layer only.
src/SecurityCouncilAzorius.solcore guard contractscript/DeploySecurityCouncilAzorius.s.soldeployment entrypointtest/unit/function-level and event semantics teststest/lifecycle/governance flow and module execution behaviortest/invariant/state and execution invariants under randomized action sequencestest/fork/fork-oriented governance scaffolding and address/version assertionsdocs/security properties and operational runbooksdocs/INTEGRATIONS_AND_ADDRESSES.mdinteracted contracts and per-network address registry
Run complete suite:
forge test -vvvRun only unit tests:
forge test --match-path "test/unit/*" -vvvRun only lifecycle tests:
forge test --match-path "test/lifecycle/*" -vvvRun only invariant tests:
forge test --match-path "test/invariant/*" -vvv- Copy
.env.exampleto.env. - Set required variables.
- Run deployment script.
source .env
forge script script/DeploySecurityCouncilAzorius.s.sol:DeploySecurityCouncilAzorius \
--rpc-url "$RPC_URL" \
--broadcast \
--verify \
-vvvvRequired environment variables:
RPC_URLDEPLOYER_PRIVATE_KEYCOUNCIL_ADDRESSAZORIUS_ADDRESSETHERSCAN_API_KEY(when using--verify)
- Run unit, lifecycle, fuzz, and invariant suites with no skips.
- Review constructor args from approved source of truth.
- Dry-run deploy and guard wiring on a fork of target chain.
- Verify source and constructor args on explorer.
- Verify
Azorius.guard()equals deployedSecurityCouncilAzoriusaddress. - Rehearse veto and unveto procedures with operators.
- Validate council rotation runbook before first production deployment.