Skip to Content
DevelopersRolesExecution & debugging

Execute as a role member

A member does not send transactions to the Safe. The member sends them to the Roles Modifier. The Roles Modifier checks the call and forwards it to the Safe.

The execution functions

function execTransactionWithRole( address to, uint256 value, bytes calldata data, Operation operation, // 0 = call, 1 = delegate call bytes32 roleKey, bool shouldRevert ) public returns (bool success);

execTransactionWithRoleReturnData has the same parameters and also returns the return data of the call.

Set shouldRevert = true in almost all cases. Then the whole transaction reverts when the inner call fails. With shouldRevert = false, a failed inner call only returns success = false.

Two more functions execute with the member’s default role: execTransactionFromModule and execTransactionFromModuleReturnData. They exist so that any contract that speaks the IAvatar interface can sit behind a Roles Modifier. The owner sets the default role with setDefaultRole.

Example with viem

This script sends 1,000 USDC from the Safe to the payroll address, through the manager role:

import { createWalletClient, encodeFunctionData, erc20Abi, http, parseAbi, } from 'viem' import { privateKeyToAccount } from 'viem/accounts' import { mainnet } from 'viem/chains' import { encodeKey } from 'zodiac-roles-sdk' const ROLES_MODIFIER = '0x...' // your Roles Modifier instance const USDC = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' const PAYROLL = '0x...' const rolesAbi = parseAbi([ 'function execTransactionWithRole(address to, uint256 value, bytes data, uint8 operation, bytes32 roleKey, bool shouldRevert) returns (bool success)', ]) const member = privateKeyToAccount(process.env.MEMBER_KEY as `0x${string}`) const client = createWalletClient({ account: member, chain: mainnet, transport: http(), }) const transferCalldata = encodeFunctionData({ abi: erc20Abi, functionName: 'transfer', args: [PAYROLL, 1_000_000_000n], // 1,000 USDC (6 decimals) }) const hash = await client.writeContract({ address: ROLES_MODIFIER, abi: rolesAbi, functionName: 'execTransactionWithRole', args: [USDC, 0n, transferCalldata, 0, encodeKey('manager'), true], })

For humans, the easier path is Zodiac Pilot. The browser extension routes normal dapp interactions through the role automatically. See Execute a transaction.

Read the revert reason

When a call violates the permissions, the Roles Modifier reverts with a typed error:

ErrorCause
NoMembership()The sender does not hold the role, or the role key is wrong.
ConditionViolation(Status status, bytes32 info)The call does not match the permissions. The status value names the failed check.
ModuleTransactionFailed()The permissions passed, but the inner call reverted (with shouldRevert = true).

The Status values in ConditionViolation:

StatusMeaning
TargetAddressNotAllowedThe role has no clearance for this target.
FunctionNotAllowedThe function is not allowed on this target.
SendNotAllowedThe call sends Ether, but the permission has no send option.
DelegateCallNotAllowedThe call is a delegate call, but the permission has no delegatecall option.
ParameterNotAllowedAn EqualTo check failed.
ParameterLessThanAllowed / ParameterGreaterThanAllowedA GreaterThan / LessThan check failed.
ParameterNotAMatchA Matches structure check failed.
OrViolation / NorViolationA logic check failed.
NotEveryArrayElementPasses / NoArrayElementPasses / ParameterNotSubsetOfAllowedAn array check failed.
BitmaskOverflow / BitmaskNotAllowedA Bitmask check failed.
AllowanceExceeded / EtherAllowanceExceeded / CallAllowanceExceededThe referenced allowance has not enough balance.
CustomConditionViolationA custom condition adapter returned false. info carries its reason code.

Debug a rejected transaction

  1. Decode the revert data with the errors above. Explorers and viem show the error name when the ABI is known.
  2. Open the Roles Modifier in the Zodiac App . Check the role: is the sender a member? Is the target listed? Is the function scoped as you expect?
  3. Check allowance balances, when the status names an allowance.
  4. For batch transactions (MultiSend), make sure an unwrapper is configured. Without it, the batch call reverts with FunctionNotAllowed. See Unwrapping.
  5. Record the intended interaction with Zodiac Pilot. The Zodiac App can compare a recorded call against the current permissions and show the difference.

Gas note

Permission checks add gas costs in proportion to the size of the condition tree. Deep trees over long calldata cost more. Measure before you automate high-frequency calls.

Last updated on