Skip to content

turingcapitalgroup/metawallet

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

59 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

MetaWallet

A hybrid smart contract combining the benefits of an ERC-4626 tokenized vault with the flexibility of a smart wallet for fund management.

Overview

MetaWallet enables institutional fund managers to operate a vault that accepts user deposits while maintaining full flexibility to deploy capital across DeFi strategies. It features:

  • ERC-4626 Vault: Standard deposit/redeem flow with share-based accounting
  • Smart Wallet: Arbitrary execution capabilities for strategy management
  • Virtual Accounting: totalAssets remains stable during invest/divest operations
  • Hook System: Modular, chainable hooks for strategy interactions
  • Merkle Proof Settlements: Off-chain attestation of external holdings

Documentation

Document Description
Architecture System design, inheritance, storage patterns
Hooks Hook system, execution flows, chaining
Interfaces Complete interface reference
Security Access control, trust model, error codes
Coding Standards Development conventions
Diagram System architecture (Mermaid)

Architecture

┌─────────────────────────────────────────────────────────────────┐
│                         MetaWallet                              │
├─────────────────────────────────────────────────────────────────┤
│  MinimalSmartAccount    │  HookExecution   │  MultiFacetProxy   │
│  (execution + roles)    │  (hook system)   │  (modules)         │
└─────────────────────────────────────────────────────────────────┘
                                 │
                    ┌────────────┴────────────┐
                    ▼                         ▼
              VaultModule                   Hooks
         (ERC4626 + accounting)     (ERC4626, 1inch Swap)

Core Components

Component Description
MetaWallet.sol Main contract inheriting wallet + vault + hook capabilities
VaultModule.sol ERC-4626 vault logic with virtual totalAssets tracking
HookExecution.sol Multi-hook execution system for strategy operations
ERC4626ApproveAndDepositHook.sol Hook for investing into ERC-4626 vaults
ERC4626RedeemHook.sol Hook for divesting from ERC-4626 vaults
OneInchSwapHook.sol Hook for token swaps via 1inch Aggregation Router

Accounting Model

MetaWallet uses a minimalistic virtual accounting model:

totalAssets = virtualTotalAssets (stored value)
totalIdle   = asset.balanceOf(vault)

Key Properties

  1. Deposits (deposit/mint): Increase virtualTotalAssets
  2. Redemptions (redeem/withdraw): Decrease virtualTotalAssets
  3. Invest/Divest: totalAssets remains unchanged
  4. Settlements: Manager updates virtualTotalAssets via settleTotalAssets()

This design ensures share price stability during strategy operations, which is critical for cross-chain deployments where assets may be "in flight".

Roles

Role Permissions
ADMIN_ROLE Install/uninstall hooks, add modules, initialize vault
WHITELISTED_ROLE Call deposit on the vault
EXECUTOR_ROLE Execute wallet operations via hooks
MANAGER_ROLE Settle total assets and merkle roots
EMERGENCY_ADMIN_ROLE Pause/unpause the vault

Note: WHITELISTED_ROLE and EXECUTOR_ROLE share the same role slot (_ROLE_1), so granting one automatically grants the other.

User Flow

Depositing

// Deposit USDC and receive shares
vault.deposit(1000e6, user);

Redeeming

// Redeem shares for USDC (limited by totalIdle)
vault.redeem(shares, user, user);

Redemptions are limited by totalIdle - users can only withdraw up to the actual USDC balance in the vault.

Manager Operations

Investing in Strategies

ERC4626ApproveAndDepositHook.ApproveAndDepositData memory data =
    ERC4626ApproveAndDepositHook.ApproveAndDepositData({
        vault: EXTERNAL_VAULT,
        assets: 5000e6,
        receiver: address(metaWallet),
        minShares: 0
    });

IHookExecution.HookExecution[] memory hooks = new IHookExecution.HookExecution[](1);
hooks[0] = IHookExecution.HookExecution({
    hookId: keccak256("hook.erc4626.deposit"),
    data: abi.encode(data)
});

metaWallet.executeWithHookExecution(hooks);

Swapping Tokens via 1inch

OneInchSwapHook.SwapData memory swapData = OneInchSwapHook.SwapData({
    router: ONEINCH_ROUTER,
    srcToken: USDC,
    dstToken: WETH,
    amountIn: 1000e6,
    minAmountOut: 0.5 ether,  // Slippage protection
    receiver: address(metaWallet),
    value: 0,  // ETH value for native swaps
    swapCalldata: oneInchCalldata  // Pre-built 1inch API calldata
});

IHookExecution.HookExecution[] memory hooks = new IHookExecution.HookExecution[](1);
hooks[0] = IHookExecution.HookExecution({
    hookId: keccak256("hook.oneinch.swap"),
    data: abi.encode(swapData)
});

metaWallet.executeWithHookExecution(hooks);

The swap hook supports:

  • Static amounts: Fixed input amount specified in amountIn
  • Dynamic amounts: Use USE_PREVIOUS_HOOK_OUTPUT to chain with previous hooks
  • Native ETH swaps: Set srcToken to 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE and value to the ETH amount
  • Slippage protection: Set minAmountOut to enforce minimum output

Settling After Yield

// Update totalAssets to reflect gains/losses
address[] memory strategies = new address[](2);
uint256[] memory values = new uint256[](2);
strategies[0] = VAULT_A;
strategies[1] = VAULT_B;
values[0] = 5000e6;
values[1] = 3000e6;

uint256 newTotalAssets = totalIdle + values[0] + values[1];
bytes32 merkleRoot = metaWallet.computeMerkleRoot(strategies, values);

metaWallet.settleTotalAssets(newTotalAssets, merkleRoot);

Pausing

// Emergency pause (blocks all user operations)
metaWallet.pause();

// Resume operations
metaWallet.unpause();

Merkle Validation

External holdings can be validated against the stored merkle root:

address[] memory strategies = new address[](2);
uint256[] memory values = new uint256[](2);
strategies[0] = VAULT_A;
strategies[1] = VAULT_B;
values[0] = 5000e6;
values[1] = 3000e6;

bool valid = metaWallet.validateTotalAssets(strategies, values, merkleRoot);

Development

Build

forge build

Test

forge test

Format

forge fmt

Generate Docs

forge doc --serve

Deployment

MetaWallet uses CREATE2 for deterministic addresses across multiple chains. The deployment process involves:

  1. Deploy implementation contracts (once per chain)
  2. Deploy proxy via factory (same address on all chains)

Prerequisites

Create a .env file:

# Required
PRIVATE_KEY=0x...
FACTORY_ADDRESS=0x...      # MinimalSmartAccountFactory (same on all chains)
REGISTRY_ADDRESS=0x...     # Your registry contract
ASSET_ADDRESS=0x...        # Underlying asset (e.g., USDC)

# Optional
DEPLOY_SALT=0              # Custom salt for deterministic address
OWNER_ADDRESS=0x...        # Defaults to deployer
VAULT_NAME="Meta USDC"
VAULT_SYMBOL="mUSDC"

# RPC URLs
MAINNET_RPC_URL=https://...
ARBITRUM_RPC_URL=https://...
OPTIMISM_RPC_URL=https://...
BASE_RPC_URL=https://...
POLYGON_RPC_URL=https://...
SEPOLIA_RPC_URL=https://...

# Block Explorer API Keys (for verification)
ETHERSCAN_API_KEY=...
ARBISCAN_API_KEY=...

Quick Start (One Command)

Deploy everything in a single transaction:

# Deploy to testnet first
make deploy-all-sepolia

# Deploy to mainnet
make deploy-all-mainnet

This deploys:

  1. MetaWallet implementation
  2. VaultModule implementation
  3. Proxy via CREATE2 factory
  4. ERC4626 deposit/redeem hooks

The proxy address is deterministic - use the same DEPLOY_SALT and deployer on all chains to get the same address.

Step-by-Step Deployment

If you prefer more control, deploy in steps:

Step 1: Deploy Implementation

Deploy the MetaWallet implementation and VaultModule to each chain:

# Deploy to Sepolia (testnet)
make deploy-impl-sepolia

# Deploy to mainnet
make deploy-impl-mainnet

# Deploy to all mainnets
make deploy-impl-all

Save the deployed addresses:

  • IMPLEMENTATION_ADDRESS - MetaWallet implementation
  • VAULT_MODULE_ADDRESS - VaultModule implementation

Step 2: Deploy Proxy

The proxy deployment requires additional environment variables:

# Add to .env
FACTORY_ADDRESS=0x...          # MinimalSmartAccountFactory (same on all chains)
IMPLEMENTATION_ADDRESS=0x...   # From step 1
VAULT_MODULE_ADDRESS=0x...     # From step 1
REGISTRY_ADDRESS=0x...         # Your registry contract
ASSET_ADDRESS=0x...            # Underlying asset (e.g., USDC)

# Optional
DEPLOY_SALT=0                  # Custom salt for deterministic address
OWNER_ADDRESS=0x...            # Defaults to deployer
VAULT_NAME="Meta USDC"
VAULT_SYMBOL="mUSDC"

Predict the proxy address before deploying:

make predict-address

Deploy the proxy (same address on all chains with same salt):

# Deploy proxy only
make deploy-proxy-sepolia

# Deploy proxy with ERC4626 hooks
make deploy-full-sepolia

# Deploy to all mainnets
make deploy-proxy-all

Step 3: Deploy Hooks (Optional)

If you deployed without hooks, you can add them later:

# Add to .env
METAWALLET_ADDRESS=0x...  # Deployed proxy address
# Deploy hooks
make deploy-hooks-mainnet

# Install hooks (requires DEPOSIT_HOOK_ADDRESS, REDEEM_HOOK_ADDRESS)
make install-hooks-mainnet

Multi-Chain Deployment

To deploy to the same address on multiple chains:

  1. Use the same DEPLOY_SALT on all chains
  2. Use the same deployer address (PRIVATE_KEY)
  3. Ensure the factory is deployed at the same address on all chains
# Predict address first
FACTORY_ADDRESS=0x... DEPLOYER_ADDRESS=0x... DEPLOY_SALT=0 make predict-address

# Deploy to each chain (will have same address)
make deploy-proxy-mainnet
make deploy-proxy-arbitrum
make deploy-proxy-optimism
make deploy-proxy-base
make deploy-proxy-polygon

Deployment Scripts

Script Command Description
DeployAll deploy-all-* One command: impl + proxy + hooks
Deploy deploy-impl-* Deploy implementation + VaultModule
DeployProxy deploy-proxy-* Deploy proxy with VaultModule
DeployProxyWithHooks deploy-full-* Deploy proxy + VaultModule + hooks
PredictProxyAddress predict-address Predict CREATE2 address
DeployHooks deploy-hooks-* Deploy hooks for existing wallet
InstallHooks install-hooks-* Install hooks on existing wallet

Dry Run

Test deployment without broadcasting by adding -dry-run suffix to any deployment command:

# Full deployment dry-run
make deploy-localhost-dry-run
make deploy-sepolia-dry-run
make deploy-mainnet-dry-run

# Step-by-step dry-run
make deploy-impl-localhost-dry-run
make deploy-proxy-sepolia-dry-run
make deploy-hooks-sepolia-dry-run

Deployment Output

Deployment addresses are saved to deployments/output/{network}/{accountId}.json:

deployments/
├── config/
│   ├── localhost.json
│   ├── sepolia.json
│   └── mainnet.json
└── output/
    ├── localhost/
    │   └── metawallet.v1.json
    └── sepolia/
        └── metawallet.v1.json

The accountId is configured in the network config file (e.g., vault.accountId: "metawallet.v1"). This allows multiple MetaWallet instances to be deployed on the same chain with different accountIds.

Dependencies

License

MIT

About

No description, website, or topics provided.

Resources

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors