Architecture
System Overview
MultiSub is a hybrid on-chain/off-chain system. The smart contract enforces permissions synchronously on every transaction; an off-chain Chainlink oracle handles the heavier accounting work (portfolio valuation, rolling-window tracking) and periodically writes results back on-chain.
┌────────────────────────────────────┐
│ Safe Multisig │
│ (Avatar & Owner) │
│ │
│ • Enables/disables module │
│ • Configures roles & limits │
│ • Emergency controls │
└──────────────┬─────────────────────┘
│ enableModule()
▼
┌────────────────────────────────────┐
│ DeFiInteractorModule │
│ (Custom Zodiac Module) │
│ │
│ • 2 Roles (Execute, Transfer) │
│ • Per-sub-account allowlists │
│ • Spending limit enforcement │
│ • Emergency pause │
└──────────────┬─────────────────────┘
│ exec() → Safe
▼
┌────────────────────────────────────┐
│ Sub-Accounts (EOAs) │
│ │
│ • executeOnProtocol() │
│ • executeOnProtocolWithValue() │
│ • transferToken() │
└────────────────────────────────────┘
On-Chain Flow
When a sub-account calls executeOnProtocol(target, data):
- Classify — look up the function selector in the registry to determine operation type (Swap, Deposit, Withdraw, etc.)
- Parse calldata — delegate to the protocol-specific parser to extract
tokenInandamount - Check allowance — verify the sub-account has enough spending allowance; revert if not
- Execute — call through the Safe via
exec()(the module acts as a Zodiac module) - Emit event —
ProtocolExecutionevent records the operation for the oracle
Off-Chain Oracle (Chainlink CRE)
The Chainlink Runtime Environment workflow runs on a schedule (configurable, default: hourly) and:
- Monitors
ProtocolExecutionevents from the module - Tracks spending per sub-account in a rolling 24h window
- Matches deposits to withdrawals to determine "acquired" token status
- Fetches token balances and USD prices from Chainlink price feeds
- Calculates updated
spendingAllowancevalues - Writes signed reports back on-chain via the authorized updater
Sub-Account
│
│ executeOnProtocol(target, data)
▼
DeFiInteractorModule (on-chain)
│ 1. classify selector
│ 2. parse calldata
│ 3. check spendingAllowance
│ 4. exec → Safe
│ 5. emit ProtocolExecution
▼
Chainlink CRE Workflow (off-chain)
│ 1. listen for events
│ 2. track rolling 24h window
│ 3. match deposits/withdrawals
│ 4. fetch prices
│ 5. calculate new allowances
└─→ updateSpendingAllowance() (on-chain)
Contract Structure
src/
├── DeFiInteractorModule.sol # Main module contract
├── base/
│ └── Module.sol # Zodiac base module
├── parsers/
│ ├── AaveV3Parser.sol
│ ├── MerklParser.sol
│ ├── MorphoParser.sol
│ ├── MorphoBlueParser.sol
│ ├── UniswapV2Parser.sol
│ ├── UniswapV3Parser.sol
│ ├── UniswapV4Parser.sol
│ ├── OneInchParser.sol
│ ├── ParaswapParser.sol
│ ├── KyberSwapParser.sol
│ └── UniversalRouterParser.sol
└── interfaces/
├── IAavePool.sol
├── ISafe.sol
├── ICalldataParser.sol
└── ...
Selector Registry
Every supported function (e.g. swapExactTokensForTokens, supply, withdraw) is registered in the module with:
- Its operation type (Swap / Deposit / Withdraw / Claim / Approve / Transfer)
- The parser address to use for calldata extraction
This registry is managed by the Safe. Adding or removing selectors requires a multisig transaction, giving you surgical control over which protocol interactions are allowed.
Security Model
| Layer | Mechanism |
|---|---|
| Role access | Sub-accounts must hold DEFI_EXECUTE_ROLE or DEFI_TRANSFER_ROLE |
| Protocol allowlist | Each sub-account has its own set of whitelisted protocol addresses |
| Spending limits | Oracle-updated allowances cap per-window spend as % of portfolio |
| Selector registry | Only registered selectors can be executed |
| Oracle freshness | Operations blocked if oracle data is >15 minutes stale |
| Hard cap | Oracle cannot set allowances above an absolute on-chain maximum |
| Emergency pause | Safe can freeze all module operations instantly |