@mihailo-maksa/safe-guards
v0.1.0
Published
A library of multi-tenant transaction guards for Safe multisig wallets — enforces custom security policies via Safe's checkTransaction hooks.
Downloads
48
Maintainers
Readme
Safe Guards
A collection of transaction guards for Safe multisig wallets.
Safe Guards provides reusable, tested guard contracts that hook into Safe's checkTransaction flow to enforce custom security policies before transactions execute. For more on Safe's guard mechanism, see the Safe documentation.
Guards
| Guard | Description |
|-------|-------------|
| SignerOnlyGuard | Only Safe owners can call execTransaction. Prevents non-owners from executing pre-signed transactions. |
| AllowedTargetsGuard | Whitelist target addresses the Safe can interact with. |
| DelegateCallGuard | Block or whitelist delegatecall operations (blocks all by default). |
| SpendingLimitGuard | Enforce a maximum net native balance decrease per time period. |
| FunctionWhitelistGuard | Whitelist specific function selectors per target contract. |
Installation
npm (recommended)
npm install @mihailo-maksa/safe-guardsImport contracts:
import {SpendingLimitGuard} from "@mihailo-maksa/safe-guards/contracts/SpendingLimitGuard.sol";
import {AllowedTargetsGuard} from "@mihailo-maksa/safe-guards/contracts/AllowedTargetsGuard.sol";Foundry (forge install)
forge install mihailo-maksa/safe-guardsFoundry's auto-remapping maps safe-guards/ to lib/safe-guards/contracts/, so import without the contracts/ prefix:
import {SpendingLimitGuard} from "safe-guards/SpendingLimitGuard.sol";You must also install the Safe dependency (used by the guards internally):
npm install @safe-global/safe-smart-accountAnd add this remapping to your remappings.txt:
@safe-global/safe-smart-account/=node_modules/@safe-global/safe-smart-account/Usage
SignerOnlyGuard
Deploy the guard (stateless — one deployment serves many Safes):
SignerOnlyGuard guard = new SignerOnlyGuard();Then set it on your Safe via setGuard(address(guard)) through a Safe transaction.
AllowedTargetsGuard
AllowedTargetsGuard guard = new AllowedTargetsGuard();
// After setting guard on Safe, whitelist targets via Safe transactions:
// guard.addTarget(trustedContract);DelegateCallGuard
Blocks all delegatecalls by default. Optionally whitelist specific targets:
DelegateCallGuard guard = new DelegateCallGuard();
// After setting guard on Safe, optionally allow delegatecall targets:
// guard.allowDelegateCallTarget(trustedImplementation);SpendingLimitGuard
SpendingLimitGuard guard = new SpendingLimitGuard();
// After setting guard on Safe, configure limits via Safe transaction:
// guard.setLimit(10 ether, 1 days); // Max 10 ETH net decrease per dayHow it works: Uses a balance-snapshot pattern — measures preBalance - postBalance across checkTransaction and checkAfterExecution to track the actual net native balance decrease. This correctly handles gas refund payments and failed inner calls.
Note: Only tracks native balance changes, not ERC-20 token transfers.
FunctionWhitelistGuard
FunctionWhitelistGuard guard = new FunctionWhitelistGuard();
// After setting guard on Safe, whitelist functions via Safe transactions:
// guard.setFunction(tokenAddress, IERC20.transfer.selector, true);
// guard.setBareTransfer(recipient, true); // Allow plain ETH transfersArchitecture
All guards extend BaseGuard, which provides a default no-op checkAfterExecution implementation. Guards use msg.sender (the Safe calling the guard) rather than storing the Safe address, making them multi-tenant — a single deployment can serve multiple Safes.
Admin functions (adding/removing whitelist entries, configuring limits) are called by the Safe itself via Safe transactions. The guard keys all storage by msg.sender, isolating each Safe's configuration.
Security Considerations
- Not yet audited — a formal audit is planned for the future. Review the code carefully before deploying to production.
- Module execution is out of scope — these guards only protect
execTransaction. Safe has a separate module-guard interface (IModuleGuard) forexecTransactionFromModulethat this package does not implement. If your Safe has enabled modules, configure Safe's module guard separately. - SignerOnlyGuard scope — SignerOnlyGuard only restricts who can execute Safe transactions; it does not restrict targets, function selectors, value, or delegatecall operations. If you need owner-only execution plus additional policy checks, use a custom composite guard.
- Guard removal and self-calls — when using AllowedTargetsGuard, the Safe's own address must be whitelisted to allow any self-calls (including guard removal via
setGuard). Note that whitelisting the Safe's address permits all self-calls, not just guard management. When using FunctionWhitelistGuard, thesetGuard(address)selector must be whitelisted for the Safe's own address to allow guard removal. Configure these whitelists before setting the guard to avoid lockout. - Delegatecall — AllowedTargetsGuard, FunctionWhitelistGuard, and SpendingLimitGuard reject delegatecall operations by default. DelegateCallGuard provides fine-grained delegatecall control with an optional whitelist.
- SpendingLimitGuard only tracks native balance changes — it does not track ERC-20 token transfers or other value movements. Failed transactions with native ETH gas refunds (
gasPrice > 0,gasToken == address(0)) still consume allowance because the Safe's native balance actually decreases. - DelegateCallGuard whitelist is critical — whitelisted delegatecall targets can modify the Safe's storage arbitrarily. Only whitelist thoroughly audited or otherwise trusted contracts.
- Multi-tenant design — all guards use
msg.senderkeying, so a single deployment serves multiple Safes with isolated configuration. However, the guard contract itself is a shared dependency — if it has a bug, all Safes using it are affected. - Compiler version — contracts are tested with solc 0.8.28. The
^0.8.20pragma allows consumers to use any compatible version. - Contributions and security reviews from the community are welcome — please open an issue or pull request if you find a vulnerability.
Chain Compatibility
These guards work on any EVM-compatible chain where Safe is deployed, including:
- Ethereum Mainnet
- Arbitrum
- Optimism
- Polygon
- Base
- Gnosis Chain
See the Safe deployments registry for a full list of supported networks.
Development
Prerequisites
Setup
git clone https://github.com/mihailo-maksa/safe-guards.git
cd safe-guards
npm installBuild
forge buildTest
forge test -vvvCoverage
forge coverageFormat
forge fmt --checkContributing
Contributions are welcome! Please open an issue or pull request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/my-feature) - Run tests (
forge test -vvv) - Ensure formatting passes (
forge fmt --check) - Open a pull request
