Skip to main content

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):

  1. Classify — look up the function selector in the registry to determine operation type (Swap, Deposit, Withdraw, etc.)
  2. Parse calldata — delegate to the protocol-specific parser to extract tokenIn and amount
  3. Check allowance — verify the sub-account has enough spending allowance; revert if not
  4. Execute — call through the Safe via exec() (the module acts as a Zodiac module)
  5. Emit eventProtocolExecution event records the operation for the oracle

The Chainlink Runtime Environment workflow runs on a schedule (configurable, default: hourly) and:

  1. Monitors ProtocolExecution events from the module
  2. Tracks spending per sub-account in a rolling 24h window
  3. Matches deposits to withdrawals to determine "acquired" token status
  4. Fetches token balances and USD prices from Chainlink price feeds
  5. Calculates updated spendingAllowance values
  6. 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

LayerMechanism
Role accessSub-accounts must hold DEFI_EXECUTE_ROLE or DEFI_TRANSFER_ROLE
Protocol allowlistEach sub-account has its own set of whitelisted protocol addresses
Spending limitsOracle-updated allowances cap per-window spend as % of portfolio
Selector registryOnly registered selectors can be executed
Oracle freshnessOperations blocked if oracle data is >15 minutes stale
Hard capOracle cannot set allowances above an absolute on-chain maximum
Emergency pauseSafe can freeze all module operations instantly