en un clic
state-mate
// Configure and verify state-mate YAML for EVM smart contract state checks, including contracts, proxies, access control, ABI updates, and troubleshooting.
// Configure and verify state-mate YAML for EVM smart contract state checks, including contracts, proxies, access control, ABI updates, and troubleshooting.
| name | state-mate |
| description | Configure and verify state-mate YAML for EVM smart contract state checks, including contracts, proxies, access control, ABI updates, and troubleshooting. |
Name: state-mate
This document describes patterns and processes for configuring state-mate YAML files to verify smart contract states on EVM chains.
state-mate validates contract states against a YAML-based description. It calls view functions and compares outputs to expected values.
deployed:
l1: # or l2, etc.
- &contractAddress "0x..."
- &proxyAdminAddress "0x..."
- &implementationAddress "0x..."
misc:
- &ZERO_ADDRESS "0x0000000000000000000000000000000000000000"
- &ZERO_BYTES32 "0x0000000000000000000000000000000000000000000000000000000000000000"
- &EIP1967_ADMIN_SLOT "0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103"
- &EIP1967_IMPLEMENTATION_SLOT "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"
roles:
- &DEFAULT_ADMIN_ROLE "0x0000000000000000000000000000000000000000000000000000000000000000"
- &SOME_ROLE "0x..."
l1:
rpcUrl: L1_MAINNET_RPC_URL # env variable name
explorerHostname: api.etherscan.io/v2/api
explorerTokenEnv: ETHERSCAN_TOKEN
chainId: 1
contracts:
contractName:
# ... contract config
contractName:
name: ContractName # Must match ABI filename
address: *contractAddress
checks:
functionName: expectedValue
anotherFunction: *someVariable
contractName:
name: ContractName
address: *contractAddress
proxyName: TransparentUpgradeableProxy
implementation: *implementationAddress
proxyChecks: {} # Usually empty for transparent proxies
storage:
- slot: *EIP1967_ADMIN_SLOT
expected: *proxyAdminAddress
label: admin
- slot: *EIP1967_IMPLEMENTATION_SLOT
expected: *implementationAddress
label: implementation
checks:
# Checks run against the proxy (with implementation logic)
someFunction: expectedValue
implementationChecks:
# Checks run directly against implementation (uninitialized state)
someFunction: *ZERO_ADDRESS # Usually zero/empty values
proxyAdminName:
name: ProxyAdmin
address: *proxyAdminAddress
checks:
UPGRADE_INTERFACE_VERSION: "5.0.0"
owner: *ownerAddress
checks:
functionName: expectedValue
decimals: 18
name: "Token Name"
checks:
functionWithArgs: # Empty value = skipped
anotherComplexFunction:
checks:
balanceOf:
- args: [*userAddress]
result: "1000000000000000000"
hasRole:
- args: [*SOME_ROLE, *holderAddress]
result: true
- args: [*OTHER_ROLE, *holderAddress]
result: false
checks:
functionAt:
- args: [0]
result: *firstItem
- args: [1]
mustRevert: true # Index out of bounds
checks:
flags: [false, false, false, false, false, 0]
getState: # Complex tuple - skip if too complex
Use ozAcl when the contract inherits from AccessControlEnumerable (has getRoleMemberCount):
checks:
# ... other checks
ozAcl:
*DEFAULT_ADMIN_ROLE : [*adminAddress]
*SOME_ROLE : [*holder1, *holder2]
*UNUSED_ROLE : [] # Verify role has 0 members
When contract uses standard AccessControl (no enumeration), use hasRole checks:
checks:
hasRole:
- args: [*DEFAULT_ADMIN_ROLE, *adminAddress]
result: true
- args: [*SOME_ROLE, *holderAddress]
result: true
- args: [*OTHER_ROLE, *nonHolderAddress]
result: false
How to detect which pattern to use:
# If this succeeds, use ozAcl
cast call $CONTRACT "getRoleMemberCount(bytes32)(uint256)" $ROLE --rpc-url $RPC_URL
# If it reverts, use hasRole checks instead
Use placeholder values like "REPLACEME" to discover actual on-chain values:
checks:
merkleRoot: "REPLACEME"
Run state-mate - the error output shows expected vs actual:
✗ .merkleRoot: expected REPLACEME, got 0xb13b0c93...
Note: Don't use REPLACEME in deployed section - causes "invalid address" errors. For addresses, use storage slot reads first.
Read the EIP-1967 admin storage slot:
EIP1967_ADMIN_SLOT="0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103"
cast storage $PROXY_ADDRESS $EIP1967_ADMIN_SLOT --rpc-url $RPC_URL
# Returns: 0x000000000000000000000000{proxyAdminAddress}
EIP1967_IMPLEMENTATION_SLOT="0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"
cast storage $PROXY_ADDRESS $EIP1967_IMPLEMENTATION_SLOT --rpc-url $RPC_URL
# For AccessControlEnumerable
cast call $CONTRACT "getRoleMemberCount(bytes32)(uint256)" $ROLE --rpc-url $RPC_URL
cast call $CONTRACT "getRoleMember(bytes32,uint256)(address)" $ROLE 0 --rpc-url $RPC_URL
# For standard AccessControl
cast call $CONTRACT "hasRole(bytes32,address)(bool)" $ROLE $ADDRESS --rpc-url $RPC_URL
deployed section:deployed:
l1:
- &newContract "0x..."
- &newContractProxyAdmin "0x..." # Discover via storage slot
- &newContractImplementation "0x..." # Discover via storage slot
newContractProxyAdmin:
name: ProxyAdmin
address: *newContractProxyAdmin
checks:
UPGRADE_INTERFACE_VERSION: "5.0.0"
owner: *proxyAdminOwner
newContract:
name: NewContract
address: *newContract
proxyName: TransparentUpgradeableProxy
implementation: *newContractImplementation
proxyChecks: {}
storage:
- slot: *EIP1967_ADMIN_SLOT
expected: *newContractProxyAdmin
label: admin
- slot: *EIP1967_IMPLEMENTATION_SLOT
expected: *newContractImplementation
label: implementation
checks:
# Add all view functions from ABI
implementationChecks:
# Same functions but with uninitialized values
For comprehensive role verification:
roles section[]deployed:
l1:
- &deployer "0x..." # Add deployer address for verification
ozAcl:
# Roles with members
*DEFAULT_ADMIN_ROLE : [*admin]
*OPERATOR_ROLE : [*operator1, *operator2]
# Roles not yet granted (verify 0 members)
*FUTURE_ROLE : []
*ANOTHER_UNUSED_ROLE : []
After deployment, verify the deployer address has renounced all roles. Add deployer to hasRole checks with result: false:
deployed:
l1:
- &deployer "0x..."
# In contract checks:
checks:
hasRole:
- args: [*DEFAULT_ADMIN_ROLE, *deployer]
result: false
- args: [*OPERATOR_ROLE, *deployer]
result: false
Or with ozAcl, just ensure deployer is NOT in any role's holder list.
# Full config check
yarn start configs/path/to/config.yml
# Check specific section
yarn start configs/path/to/config.yml -o l1
# Check specific contract
yarn start configs/path/to/config.yml -o l1/contractName
# Check specific function type
yarn start configs/path/to/config.yml -o l1/contractName/checks/functionName
# Update ABIs (download all)
yarn start configs/path/to/config.yml --update-abi
# Update only missing ABIs
yarn start configs/path/to/config.yml --update-abi-missing
--update-abi to download ABIsFor implementationChecks, use uninitialized/default values:
*ZERO_ADDRESS*ZERO_BYTES320""false or initial stateThe implementation contract should be in an uninitialized state since all state is stored in the proxy.