Skip to main content
The Vault is an escrow contract that holds intent rewards until they can be claimed by solvers or refunded to creators. Each intent has its own dedicated vault, deployed deterministically using CREATE2 for predictable addressing.

Architecture

Vaults use a minimal proxy pattern (ERC-1167) for gas-efficient deployment:
Vault Implementation (deployed once)

Proxy (deployed per intent via CREATE2)
    ↓ delegates to
Vault Implementation
Key Properties:
  • One vault per intent, deployed on-demand
  • Deterministic addresses computed from intent hash
  • Only the Portal can manage vault operations
  • No persistent state between intents (stateless implementation)

Deployment

CREATE2 Determinism

Vault addresses are computed using CREATE2 with the intent hash as salt:
address vaultAddress = keccak256(
    abi.encodePacked(
        CREATE2_PREFIX,        // 0xff (standard) or 0x41 (TRON)
        portalAddress,         // Deployer
        intentHash,            // Salt
        keccak256(             // Init code hash
            abi.encodePacked(
                type(Proxy).creationCode,
                abi.encode(vaultImplementation)
            )
        )
    )
)

On-Demand Deployment

Vaults are deployed lazily when first needed:
  • Intent Publishing: Address is computed but not deployed
  • First Funding: Vault deployed if not already exists
  • Withdrawal/Refund: Vault deployed if not already exists
This saves gas when intents are published but never funded.

Chain Compatibility

The CREATE2 prefix adapts to different chains:
  • Standard EVM Chains: 0xff prefix
  • TRON Mainnet (728126428): 0x41 prefix
  • TRON Testnet (2494104990): 0x41 prefix

Access Control

modifier onlyPortal()
All vault operations are restricted to the Portal contract that deployed the implementation. This ensures:
  • Only validated intents can withdraw funds
  • Refunds require proper deadline checks
  • Token recovery follows protocol rules

Funding

Standard Funding

Called during Portal.publishAndFund() or Portal.fund():
function fundFor(
    Reward calldata reward,
    address funder,
    IPermit permit
) external payable onlyPortal returns (bool fullyFunded)
Funding Process:
  1. Check native token balance against reward.nativeAmount
  2. For each ERC20 token:
    • Attempt permit-based transfer first (if permit contract provided)
    • Fall back to standard transferFrom with allowance
  3. Return true only if all tokens fully funded
Partial Funding: The function transfers as much as available based on:
  • Funder’s token balance
  • Funder’s allowance/permit to vault
  • Existing vault balance

Permit-Based Funding

Vaults support gasless approvals via permit contracts (e.g., Permit2): Permit Transfer:
function _fundFromPermit(
    address funder,
    IERC20 token,
    uint256 rewardAmount,
    IPermit permit
) internal returns (uint256 remaining)
Queries permit contract for allowance and transfers tokens directly without requiring prior approve() call. Standard Transfer (Fallback):
function _fundFrom(
    address funder,
    IERC20 token,
    uint256 remainingAmount
) internal returns (uint256 remaining)
Uses standard ERC20 transferFrom based on existing allowance. Funding Strategy:
  1. Check current vault balance
  2. Try permit transfer if permit contract provided
  3. Fall back to standard transfer for any remaining amount
  4. Return unfunded amount

Withdrawal

Solvers claim rewards after intent fulfillment and proof verification:
function withdraw(
    Reward calldata reward,
    address claimant
) external onlyPortal
Withdrawal Process:
  1. Transfer all reward ERC20 tokens to claimant (up to reward amounts)
  2. Transfer native tokens to claimant (up to reward amount)
  3. Use actual balance (may be less than reward if partially funded)
Safety Features:
  • Uses SafeERC20.safeTransfer for token transfers
  • Minimum of reward amount and actual balance prevents over-withdrawal
  • Native transfers use low-level call with success check
  • Reverts on failed native transfer with NativeTransferFailed error

Refund

Creators reclaim rewards after intent expiry:
function refund(Reward calldata reward) external onlyPortal
Refund Process:
  1. Transfer all ERC20 token balances to reward.creator
  2. Transfer all native token balance to reward.creator
  3. No amount restrictions (returns full vault contents)
Key Differences from Withdrawal:
  • Returns actual balance, not limited to reward amounts
  • Always transfers to reward.creator
  • Portal validates deadline before calling

Token Recovery

Recover tokens mistakenly sent to vault:
function recover(address refundee, address token) external onlyPortal
Use Cases:
  • User accidentally sent tokens directly to vault address
  • Tokens sent to wrong vault
  • Extra tokens beyond reward amounts
Restrictions (enforced by Portal):
  • Cannot recover zero address (native tokens)
  • Cannot recover any token in reward.tokens array
  • Intent must have zero native rewards OR already be claimed/refunded

Fund Flow

Publishing Flow

User → Portal.publishAndFund()

Portal → Vault.fundFor()

Vault ← User (tokens via transferFrom/permit)

Withdrawal Flow

Solver → Portal.withdraw()

Portal → Prover (verify proof)

Portal → Vault.withdraw()

Vault → Solver (claimant address)

Refund Flow

Creator → Portal.refund()

Portal (validates deadline/proof)

Portal → Vault.refund()

Vault → Creator

Security Model

Immutable Portal

The portal address is set at construction and cannot be changed:
address private immutable portal;
This creates a strong binding between vault and portal, preventing:
  • Unauthorized withdrawals
  • Fake refunds before expiry
  • Bypassing protocol validation

Balance-Based Logic

All transfers use actual balance rather than storing state:
  • No accounting variables to manipulate
  • Always transfers real tokens held by vault
  • Cannot withdraw more than vault contains

SafeERC20 Integration

Uses OpenZeppelin’s SafeERC20 for all token transfers:
  • Handles non-standard ERC20 returns
  • Prevents silent transfer failures
  • Reverts on insufficient balance

Minimal Proxy Pattern

Using ERC-1167 minimal proxies provides:
  • Gas Efficiency: ~2,500 gas to deploy vs ~200,000 for full contract
  • Security: Implementation cannot be changed after proxy deployment
  • Predictability: Deterministic addresses enable pre-funding

Integration Examples

Computing Vault Address

// From Portal
Intent memory intent = /* ... */;
address vault = portal.intentVaultAddress(intent);

// Or with components
bytes32 intentHash = portal.getIntentHash(
    destination,
    route,
    reward
);
address vault = portal.intentVaultAddress(
    destination,
    abi.encode(route),
    reward
);

Pre-Funding Vault

Users can send tokens directly to the computed vault address before publishing:
// Compute vault address
address vault = portal.intentVaultAddress(intent);

// Send tokens to vault
IERC20(tokenAddress).transfer(vault, amount);

// Publish without funding (vault already has tokens)
portal.publish(intent);
Note: This skips the Funded status check, so verify funding manually with portal.isIntentFunded().

Partial Funding

// User has limited balance
uint256 userBalance = token.balanceOf(user);
uint256 rewardAmount = 1000e18;

// Approve partial amount
token.approve(address(portal), userBalance);

// Publish with partial funding allowed
portal.publishAndFund{value: nativeAmount}(intent, true);
// allowPartial = true

// Check funding status
bool funded = portal.isIntentFunded(intent);
// Returns false if partially funded

Using Permit for Gasless Funding

// User signs permit off-chain
IPermit permit = IPermit(permitContract);

// Relayer calls publishAndFundFor
portal.publishAndFundFor(
    intent,
    true,              // allowPartial
    user,              // funder
    address(permit)    // permit contract
);

// Vault uses permit.transferFrom instead of token.transferFrom

Error Handling

  • NotPortalCaller(address): Caller is not the authorized Portal contract
  • NativeTransferFailed(address, uint256): Failed to send native tokens to recipient
  • ZeroRecoverTokenBalance(address): Attempted to recover token with zero balance

Gas Considerations

Deployment Costs

  • Full Vault Contract: ~200,000 gas
  • Minimal Proxy: ~2,500 gas (98.75% savings)
  • Implementation: One-time cost, shared across all vaults

Operation Costs

Funding:
  • Native transfer: ~21,000 gas
  • ERC20 transfer: ~50,000 gas per token
  • Permit transfer: ~70,000 gas per token (includes permit verification)
Withdrawal:
  • Per token: ~50,000 gas
  • Native transfer: ~21,000 gas
Refund:
  • Similar to withdrawal costs
  • No amount validation overhead

Optimization Strategies

  1. Batch Operations: Use Portal.batchWithdraw() to amortize vault deployment costs
  2. Pre-Funding: Send tokens to vault address before publishing to skip funding transaction
  3. Single Token Rewards: Minimize token array length in rewards
  4. Standard Transfers: Use regular approvals instead of permits when possible (lower gas)

Cross-Chain Considerations

Address Consistency

Vault addresses are deterministic across chains with same:
  • Portal deployment address
  • Intent parameters (destination, route, reward)
  • CREATE2 prefix (chain-specific)
Example:
Ethereum: 0x1234...5678 (0xff prefix)
TRON:     0x4321...8765 (0x41 prefix, different address)

Native Token Handling

Each chain’s native token is handled uniformly:
  • Ethereum: ETH
  • Polygon: MATIC
  • Arbitrum: ETH
  • TRON: TRX
Vaults treat all as “native amount” in reward.nativeAmount.

Best Practices

For Intent Creators

  1. Verify Funding: Call portal.isIntentFunded() before expecting fulfillment
  2. Adequate Approvals: Ensure sufficient allowance for all reward tokens
  3. Gas Budgeting: Include native amount for destination chain execution
  4. Deadline Buffer: Set reward.deadline with buffer for proof verification time

For Solvers

  1. Check Vault Balance: Verify vault is fully funded before fulfilling
  2. Gas Estimation: Account for withdrawal transaction costs in profitability calculations
  3. Claimant Address: Ensure claimant address is correct and can receive tokens
  4. Token Compatibility: Verify all reward tokens are standard ERC20

For Integrators

  1. Address Prediction: Compute vault addresses off-chain to display to users
  2. Event Monitoring: Watch IntentFunded events to track funding progress
  3. Error Handling: Handle partial funding scenarios gracefully
  4. Permit Integration: Support gasless approvals for better UX
I