A secure executor module for smart accounts that enables session keys to execute whitelisted DeFi operations while maintaining smart account security through whitelist validation and ERC20 transfer restrictions.
Imagine you have a smart wallet (like a digital safe) that holds your crypto. You want to let a friend use it to do specific things, like swap tokens on Uniswap or deposit into Aave, but you don't want them to be able to withdraw all your money or send it to random addresses.
This protocol is like giving your friend a special key that:
- β Can only do things you've pre-approved (like "swap tokens on Uniswap")
- β Can't send your tokens to random people
- β Can only send tokens to safe places (your wallet, back to you, or trusted protocols)
- β You can turn off immediately if needed (pause functionality)
The "whitelist" is like a list of approved actions. Before the friend can do anything, the system checks: "Is this action on the approved list?" If yes, it's allowed. If no, it's blocked.
GuardedExecModule is an ERC-7579 executor module that provides secure, whitelisted execution capabilities for smart accounts. It enables session keys (limited-authority keys) to execute batch operations on whitelisted DeFi protocols while maintaining the security and context of the smart account.
- Whitelist-Based Execution: Only pre-approved target contract + function selector combinations can be executed
- ERC20 Transfer Restrictions: Prevents arbitrary token transfers; only allows transfers to authorized recipients
- Direct Whitelist Management: Owner can directly add/remove whitelisted targets and selectors
- Emergency Pause: Module and registry can be paused immediately if session keys are compromised
- Upgradeable: UUPS upgradeable pattern allows fixing bugs and adding features
- Batch Operations: Execute multiple operations in a single transaction for gas efficiency
- Whitelist Validation: Every execution checks if the target+selector is whitelisted
- ERC20 Transfer Authorization: ERC20 transfers are only allowed to:
- The smart wallet itself
- Wallet owners
- Explicitly authorized recipients (e.g., DEX routers, lending protocols)
- Owner-Controlled Whitelist: Only contract owner can modify whitelist (should be multisig for production)
- Pausable: Emergency stop capability for compromised session keys or malicious whitelist changes
- Two-Step Ownership Transfer: Prevents accidental or malicious ownership transfers
Role: Primary controller of the smart account Capabilities:
- Install/uninstall the GuardedExecModule on their smart account
- Own the TargetRegistry (if they deploy it)
- Pause/unpause the module if session key is compromised
- Upgrade the module (if owner of module)
- Update registry address (if owner of module)
Limitations:
- Cannot execute operations directly through the module (must use session keys or smart account directly)
Role: Controls the TargetRegistry contract Capabilities:
- Add/remove whitelisted target+selector combinations (immediate)
- Add/remove authorized ERC20 token recipients (immediate)
- Pause/unpause the registry (emergency stop)
- Transfer ownership (two-step process)
Limitations:
- Cannot modify whitelist when registry is paused
- Cannot modify ERC20 recipient authorization when registry is paused
Role: Limited-authority key that can execute whitelisted operations Capabilities:
- Execute batch operations on whitelisted target+selector combinations
- Execute operations that maintain smart account context (msg.sender = smart account)
- Execute ERC20 transfers to authorized recipients only
Limitations:
- Cannot execute operations on non-whitelisted target+selectors
- Cannot transfer ERC20 tokens to arbitrary addresses
- Cannot pause the module
- Cannot modify the whitelist
- Cannot upgrade the module
-
GuardedExecModuleUpgradeable (
src/module/GuardedExecModuleUpgradeable.sol)- Main executor module contract
- Implements ERC-7579 executor interface
- Validates whitelist and ERC20 transfer authorization
- Upgradeable via UUPS pattern
-
TargetRegistry (
src/registry/TargetRegistry.sol)- Manages whitelist of target+selector combinations
- Owner-controlled whitelist management (immediate changes)
- Manages ERC20 transfer recipient authorization
- Pausable for emergency stops
-
ISafeWallet (
src/interfaces/ISafeWallet.sol)- Interface for querying smart wallet owners
- Used for ERC20 transfer authorization checks
Smart Account
β
βββ> GuardedExecModuleUpgradeable (Executor Module)
β β
β βββ> TargetRegistry (Whitelist Verification)
β β
β βββ> ISafeWallet (Owner Query for ERC20 Auth)
β
βββ> Session Keys (Execute via Module)
-
Execution Flow:
- Session key calls
executeGuardedBatch()on GuardedExecModuleUpgradeable - Module validates all target+selector combinations against TargetRegistry whitelist
- Module validates ERC20 transfers (if any) against authorized recipients
- Module executes batch operations via smart account (maintains context)
- Session key calls
-
Whitelist Management Flow:
- Registry owner calls
addToWhitelist()orremoveFromWhitelist()on TargetRegistry - Changes take effect immediately (if registry is not paused)
- Owner can pause the registry to prevent any whitelist modifications
- Registry owner calls
- Foundry (latest version)
- Node.js and pnpm (for Hardhat scripts, if needed)
# Clone the repository
git clone https://github.com/ondefy/zyfai-executor-module.git
cd rhinestone-executor-module
# Install dependencies (if using pnpm)
pnpm install
# Install Foundry dependencies
forge install# Compile all contracts
forge build
# Compile with optimizations (for production)
FOUNDRY_PROFILE=optimized forge build# Remove build artifacts
forge cleanThe code should compile without errors or warnings. If warnings are present, they should be documented and explained.
# Run all tests
forge test
# Run with verbose output
forge test -vv
# Run with very verbose output (trace level)
forge test -vvv# Run TargetRegistry tests
forge test --match-path "test/TargetRegistryTest.t.sol"
# Run GuardedExecModuleUpgradeable tests
forge test --match-path "test/GuardedExecModuleUpgradeableTest.t.sol"# Run a specific test function
forge test --match-test test_AddToWhitelist -vv
# Run tests matching a pattern
forge test --match-test "test_*ERC20*" -vvImportant: Test coverage must be >80% and all tests must pass.
# Generate coverage report (summary)
forge coverage --report summary
# Generate detailed LCOV coverage report
forge coverage --report lcov
# View coverage with minimum optimization (fixes "stack too deep" issues)
forge coverage --report summary --ir-minimum- Target Coverage: >80% for all main contracts
- Current Coverage:
GuardedExecModuleUpgradeable.sol: 98.11% lines, 88.00% statements βTargetRegistry.sol: 91.34% lines, 79.43% statements β
- Test Status: All 54 tests passing β
To test in a clean environment (as required by Sherlock):
# 1. Clean all artifacts
forge clean
rm -rf cache out
# 2. Install dependencies
forge install
pnpm install # if using Hardhat scripts
# 3. Build
forge build
# 4. Run tests
forge test
# 5. Check coverage
forge coverage --report summaryAll tests should pass and coverage should be >80% in a clean environment.
| Contract | Lines | Statements | Branches | Functions |
|---|---|---|---|---|
GuardedExecModuleUpgradeable.sol |
98.11% (52/53) | 100.00% (50/50) | 100.00% (10/10) | 92.86% (13/14) |
TargetRegistry.sol |
91.34% (116/127) | 89.36% (126/141) | 70.97% (22/31) | 92.00% (23/25) |
| Overall | 94.44% | 94.24% | 82.93% | 92.31% |
test/GuardedExecModuleUpgradeableTest.t.sol: 32 teststest/TargetRegistryTest.t.sol: 22 tests- Total: 54 tests
- β Whitelist validation tests
- β ERC20 transfer authorization tests
- β Direct whitelist management tests
- β Pause/unpause tests
- β Upgrade tests
- β Batch operation tests
- β Two-step ownership transfer tests
- β Edge cases and error conditions
- β Input validation tests (empty batches, length mismatches)
- β Invalid input tests (zero addresses, invalid selectors)
- β Calldata validation tests (too short, malformed)
- β Access control tests (owner-only functions)
All tests follow Sherlock's requirement: Every test has a way to fail. Tests use:
vm.expectRevert()- Tests will fail if the expected revert doesn't occurassertTrue(),assertFalse(),assertEq()- Tests will fail if assertions don't hold- Error selector matching - Tests verify specific error types
All contracts have comprehensive natspec comments with >80% comment-to-code ratio:
- TargetRegistry.sol: 98.85% comment ratio
- GuardedExecModuleUpgradeable.sol: 124.79% comment ratio
Comments include:
- Contract-level documentation
- Function documentation with @notice, @dev, @param, @return
- Security considerations
- Gas optimization notes
- Inline comments for complex logic
// 1. Deploy registry
TargetRegistry registry = new TargetRegistry(owner);
// 2. Deploy module implementation
GuardedExecModuleUpgradeable implementation = new GuardedExecModuleUpgradeable();
// 3. Deploy proxy and initialize
ERC1967Proxy proxy = new ERC1967Proxy(
address(implementation),
abi.encodeWithSelector(
GuardedExecModuleUpgradeable.initialize.selector,
address(registry),
owner
)
);
// 4. Install on smart account
smartAccount.installModule(MODULE_TYPE_EXECUTOR, address(proxy), "");# Deploy ERC1967 module
PRIVATE_KEY=pk forge script script/0-DeployUpgradeableSimple.s.sol --rpc-url https://base-mainnet.g.alchemy.com/v2/key --broadcast -vvvv
# Deploy TargetRegistry
PRIVATE_KEY=pk forge script script/1-DeployTargetRegistry.s.sol --rpc-url https://sonic-mainnet.g.alchemy.com/v2/key --broadcast -vvvv
# Verify TargetRegistry
forge verify-contract <Registry-Address> src/registry/TargetRegistry.sol:TargetRegistry --rpc-url https://base-mainnet.g.alchemy.com/v2/key --chain-id 8453 --compiler-version 0.8.30 --etherscan-api-key etherscan-key --constructor-args 0x000000000000000000000000d61C43c089852e0AB68B967dD1eDe03a18e52223
# Upgrade Module
TARGET_REGISTRY_ADDRESS=<Registry-Address> forge script script/2-UpgradeAndUpdateModule.s.sol --rpc-url https://base-mainnet.g.alchemy.com/v2/key --private-key pk --broadcast -vvvv
# Verify New Impl of Module
forge verify-contract <NEW-IMPL-Address> src/module/GuardedExecModuleUpgradeable.sol:GuardedExecModuleUpgradeable --rpc-url https://base-mainnet.g.alchemy.com/v2/key --chain-id 8453 --compiler-version 0.8.30 --etherscan-api-key etherscan-key
# Proxy Verify
forge verify-contract <NEW-IMPL-Address> lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol:ERC1967Proxy --constructor-args 0x000000000000000000000000079c22bbd7b5b91bde24687036d3d3ee2b6c634c00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000044485cc955000000000000000000000000a0bee327a95f786f5097028ee250c4834dfeb629000000000000000000000000d61c43c089852e0ab68b967dd1ede03a18e5222300000000000000000000000000000000000000000000000000000000 --rpc-url https://base-mainnet.g.alchemy.com/v2/key --chain-id 8453 --compiler-version 0.8.30 --etherscan-api-key etherscan-key
forge verify-contract \
0xf8DAAe25b9388762eb24e83324d9f4ec46c000fc \
src/module/GuardedExecModuleUpgradeable.sol:GuardedExecModuleUpgradeable \
--rpc-url https://arb-mainnet.g.alchemy.com/v2/key \
--chain-id 42161 \
--compiler-version 0.8.30 \
--etherscan-api-key key
# for Arb
forge verify-contract \
0xF659d30D4EB88B06A909F20839D8959Bd77d8790 \
lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol:ERC1967Proxy \
--constructor-args \
0x000000000000000000000000d5C2dFD6d34c2bEA1dbec14DE4780d4A9D45ea1700000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000 \
--rpc-url https://eth-mainnet.g.alchemy.com/v2/key \
--chain-id 1 \
--compiler-version 0.8.30 \
--etherscan-api-key key
forge script script/DeployWithCREATE3.s.sol:DeployWithCREATE3 \
--rpc-url https://eth-mainnet.g.alchemy.com/v2/key \
--broadcast \
-vvvv
# Final Command
PRIVATE_KEY=pk forge script script/1-DeployTargetRegistry.s.sol --rpc-url https://sonic-mainnet.g.alchemy.com/v2/key --broadcast -vvvv
forge script script/DeployWithCREATE3.s.sol:DeployWithCREATE3 \
--rpc-url https://eth-mainnet.g.alchemy.com/v2/key \
--broadcast \
-vvvv
forge verify-contract \
<TARGET_REGISTRY_ADDRESS> \
src/registry/TargetRegistry.sol:TargetRegistry \
--rpc-url https://base-mainnet.g.alchemy.com/v2/key \
--chain-id 8453 \
--compiler-version 0.8.30 \
--etherscan-api-key etherscan-key \
--constructor-args 0x000000000000000000000000d61C43c089852e0AB68B967dD1eDe03a18e52223
forge verify-contract \
<MODULE_IMPL_ADDRESS> \
src/module/GuardedExecModuleUpgradeable.sol:GuardedExecModuleUpgradeable \
--rpc-url https://base-mainnet.g.alchemy.com/v2/key \
--chain-id 8453 \
--compiler-version 0.8.30 \
--etherscan-api-key etherscan-key
forge verify-contract \
<MODULE_PROXY_ADDRESS> \
lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol:ERC1967Proxy \
--constructor-args \
0x000000000000000000000000c8F535f56C191D4D623e252D540BA1Fcb656120800000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000 \
--rpc-url https://base-mainnet.g.alchemy.com/v2/key \
--chain-id 8453 \
--compiler-version 0.8.30 \
--etherscan-api-key etherscan-key
# For plasma
forge verify-contract \
0xc8F535f56C191D4D623e252D540BA1Fcb6561208 \
src/registry/TargetRegistry.sol:TargetRegistry \
--rpc-url https://plasma-mainnet.g.alchemy.com/v2/key \
--chain-id 9745 \
--compiler-version 0.8.30 \
--verifier custom \
--verifier-url 'https://api.routescan.io/v2/network/mainnet/evm/9745/etherscan' \
--verifier-api-key "verifyContract" \
--constructor-args 0x000000000000000000000000d61C43c089852e0AB68B967dD1eDe03a18e52223
forge verify-contract \
0xA49EA89806c53966f12920d12F1B484C63E43AEA \
src/module/GuardedExecModuleUpgradeable.sol:GuardedExecModuleUpgradeable \
--rpc-url https://plasma-mainnet.g.alchemy.com/v2/key \
--chain-id 9745 \
--compiler-version 0.8.30 \
--verifier custom \
--verifier-url 'https://api.routescan.io/v2/network/mainnet/evm/9745/etherscan' \
--verifier-api-key "verifyContract"- Module Owner: Can pause/unpause and upgrade (should be multisig)
- Registry Owner: Can modify whitelist and ERC20 recipient authorization (should be multisig)
- Session Keys: Can only execute whitelisted operations
- Two-Step Ownership: Ownership transfers require explicit acceptance from new owner
- Compromised Session Key: Pause the module immediately via
pause() - Malicious Whitelist Change: Pause the registry immediately via
pause()to prevent further changes - Critical Vulnerability: Pause both module and registry
- Incorrect Whitelist Addition: Remove the entry immediately via
removeFromWhitelist()
MIT
ZyFAI
Note: This protocol is designed for production use with proper access control (multisig wallets for both module and registry owners). Always conduct security audits before deploying to mainnet.