RestrictedActionsGuard
is a Safe Guard that allows onwers to perform only allowed actions on whitelisted targets.
It introduces a new Guard threshold concept that replaces the usual Safe threshold, allowing more nuanced permissions.
For example, a 3/5 Safe (3 owners out of 5 must sign in order to execute a transaction) can be converted into a 2/3/5 Safe (at least 2 owners out of 5 must sign in order to perform any of the allowed actions on whitelisted targets, while all other actions require at least 3 signatures out of 5 to execute a transaction).
- 1/2/3 Safe where one of the owners is an automated account that can place limit orders but not market orders.
- 2/2/3 Safe where two of the owners are AI agents that can Safe manage funds but only if the recipient is the Safe account.
- 2/3/5 Safe for a DAO treasury where at two owners can execute approved governance proposals, submit emergency transactions, but need 3 signatures for token transfers, or protocol parameter changes
Network | Address |
---|---|
Arbitrum | 0xa3212332057C479937EA5efE4c92EcE8d3a3100a |
Optimism | 0xa3212332057C479937EA5efE4c92EcE8d3a3100a |
Celo | 0xa3212332057C479937EA5efE4c92EcE8d3a3100a |
Linea | 0xa3212332057C479937EA5efE4c92EcE8d3a3100a |
Avalanche | 0xa3212332057C479937EA5efE4c92EcE8d3a3100a |
- Setup: After deploying the Safe, the
RestrictedActionsGuard
must be set up with a chosen Guard threshold. - Defining Restricted Actions: Owners specify which actions are allowed on particular
target
addresses. This is done using an array ofpattern
+mask
pairs:- Pattern: A predefined calldata signature that represents a function call
- Mask: A wildcard mask that defines which parts of the calldata must match exactly and which can be flexible. The byte
0x00
represents a "wildcard" operator, while any non-null byte represents an "exact match" operator. This allows owners to configure actions such as "deposit is allowed only if the recipient is the safe account"
- Execution: When a transaction is proposed, the guard checks:
- If it meets the standard Safe threshold.
- If it matches a whitelisted restricted action and meets the Guard threshold.
- If neither condition is met, the transaction is blocked.
// example configuration for a 1/2/3 Safe
Safe singleton = address(new Safe());
RestrictedActionsGuard restrictedActionsGuard = new RestrictedActionsGuard();
uint256 threshold = 2;
// 0. Deploy a 2/3 Safe
address[] memory owners = new address[](3);
owners[0] = owner1;
owners[1] = owner2;
owners[2] = owner3;
SafeProxy proxy = safeProxyFactory.createProxyWithNonce(
singleton,
abi.encodeCall(
Safe.setup,
(owners, threshold, address(0), bytes(""), address(0), address(0), 0, payable(address(0)))
),
0
);
Safe safe = Safe(payable(address(proxy)));
// 1. Call `RestrictedActionsGuard.setup` through your Safe:
uint256 guardThreshold = 1;
safe.execTransaction(
address(restrictedActionsGuard),
0,
abi.encodeCall(RestrictedActionsGuard.setup, (guardThreshold)),
Enum.Operation.Call,
0,
0,
0,
address(0),
payable(address(0)),
signatures
);
// 2. Set the guard on your Safe
safe.execTransaction(
address(safe),
0,
abi.encodeCall(GuardManager.setGuard, (address(restrictedActionsGuard))),
Enum.Operation.Call,
0,
0,
0,
address(0),
payable(address(0)),
signatures
);
// 3. Optionally, reduce your Safe threshold to have a 1/2/3 Safe
safe.execTransaction(
address(safe),
0,
abi.encodeCall(OwnerManager.changeThreshold, (1)),
Enum.Operation.Call,
0,
0,
0,
address(0),
payable(address(0)),
signatures
);
// 4. Whitelist the restricted actions
// 4.1 Any owner can deposit to a vault but only if the recipient is the safe account
bytes memory pattern1 = abi.encodeCall(ERC4626.deposit, (0, address(safe)));
bytes memory mask1 = abi.encodeWithSelector(bytes4(0xFFFFFFFF), 0, address(uint160(type(uint160).max)));
// 4.2 Any owner can withdraw from a vault but only if the recipient is the safe account
bytes memory pattern2 = abi.encodeCall(ERC4626.withdraw, (0, address(safe), address(0)));
bytes memory mask2 = abi.encodeWithSelector(bytes4(0xFFFFFFFF), 0, address(uint160(type(uint160).max), 0));
address target = address(vault);
bytes[] memory patterns = new bytes[](2);
patterns[0] = pattern1;
patterns[1] = pattern2;
bytes[] memory masks = new bytes[](2);
masks[0] = mask1;
masks[1] = mask2;
safe.execTransaction(
address(restrictedOwnersGuard),
0,
abi.encodeCall(RestrictedOwnersGuard.setRestrictedActions, (target, datas, masks)),
Enum.Operation.Call,
0,
0,
0,
address(0),
payable(address(0)),
signatures
);
// 5. Remove the default `CompatibilityFallbackHandler` to disable EIP-1271 signing (https://x.com/mveehkim/status/1889295776200704431)
safe.execTransaction(
address(safe),
0,
abi.encodeCall(FallbackManager.setFallbackHandler, (address(0))),
Enum.Operation.Call,
0,
0,
0,
address(0),
payable(address(0)),
signatures
);
Another approach to implementing restricted actions on a Safe is using the Zodiac Roles Modifier by Gnosis Guild. This modifier enables role-based access control, allowing specific roles to call pre-approved functions with defined parameters.
While the Zodiac Roles Modifier provides a high degree of flexibility, it comes with added complexity. Managing permissions requires defining roles, assigning them to addresses, and explicitly scoping their permissions across multiple dimensions. This can be powerful but cumbersome to manage at scale, especially in multi-signature or automated environments where rules must be updated frequently.
If your use case requires straightforward permission control over a Safe while maintaining a simple setup, RestrictedActionsGuard
can be a more practical choice.
This code is provided "as is" and has not undergone a formal security audit.
While a live mainnet CTF with +$5k worth of assets on the line was able to uncover two vulnerabilities (fixed in v0.0.3), it does not constitute a formal review and does not prevent the code from containing other issues.
Use it at your own risk. The author(s) assume no liability for any damages or losses resulting from the use of this code. It is your responsibility to thoroughly review, test, and validate its security and functionality before deploying or relying on it in any environment.