Skip to content

3.3 On-Chain Vault Contract Implementation

The public smart contract vault must implement a minimal execution interface to limit attack vectors and focus on verification and dispatch.

Interface Definition

solidity
interface IRelayedVault {
    /// @notice Execute a Canton-signed intent
    /// @param intentPayload ABI-encoded intent data
    /// @param cantonSig Canton signature over intentPayload hash
    /// @param proof Additional proof data (pattern-specific)
    function executeIntent(
        bytes calldata intentPayload,
        bytes calldata cantonSig,
        bytes calldata proof
    ) external;

    /// @notice Get current nonce for replay protection
    function getNonce() external view returns (uint256);

    /// @notice Emergency pause mechanism
    function pause() external;

    /// @notice Resume operations
    function unpause() external;
}

Core Implementation

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";

contract CantonDeFiVault is
    IRelayedVault,
    Pausable,
    ReentrancyGuard,
    AccessControl
{
    bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
    bytes32 public constant EMERGENCY_ROLE = keccak256("EMERGENCY_ROLE");

    // Canton authorization
    address public cantonSigner;
    bytes32 public authorizedPartyId;

    // Replay protection
    uint256 public nonce;

    // Protocol adapters
    mapping(bytes32 => address) public adapters;

    // Events
    event IntentExecuted(
        uint256 indexed nonce,
        bytes32 indexed intentHash,
        address[] protocols,
        bool success
    );

    event AdapterRegistered(
        bytes32 indexed protocolId,
        address adapter
    );

    struct Intent {
        string vaultId;
        uint256 nonce;
        Action[] actions;
        uint256 deadline;
    }

    struct Action {
        bytes32 protocolId;
        bytes4 selector;
        bytes params;
    }

    constructor(
        address _cantonSigner,
        bytes32 _authorizedPartyId
    ) {
        cantonSigner = _cantonSigner;
        authorizedPartyId = _authorizedPartyId;
        _setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _setupRole(ADMIN_ROLE, msg.sender);
        _setupRole(EMERGENCY_ROLE, msg.sender);
    }

    /// @inheritdoc IRelayedVault
    function executeIntent(
        bytes calldata intentPayload,
        bytes calldata cantonSig,
        bytes calldata proof
    )
        external
        override
        nonReentrant
        whenNotPaused
    {
        // 1. Verify Canton signature
        _verifyCantonSignature(intentPayload, cantonSig, proof);

        // 2. Parse intent
        Intent memory intent = abi.decode(intentPayload, (Intent));

        // 3. Validate intent
        _validateIntent(intent);

        // 4. Update nonce
        nonce = intent.nonce;

        // 5. Execute actions
        address[] memory protocols = _executeActions(intent.actions);

        // 6. Emit event
        emit IntentExecuted(
            intent.nonce,
            keccak256(intentPayload),
            protocols,
            true
        );
    }

    /// @notice Verify Canton signature on intent
    function _verifyCantonSignature(
        bytes calldata intentPayload,
        bytes calldata cantonSig,
        bytes calldata /* proof */
    ) internal view {
        bytes32 intentHash = keccak256(intentPayload);
        bytes32 ethSignedHash = keccak256(
            abi.encodePacked("\x19Ethereum Signed Message:\n32", intentHash)
        );

        address recoveredSigner = _recoverSigner(ethSignedHash, cantonSig);
        require(
            recoveredSigner == cantonSigner,
            "Invalid Canton signature"
        );
    }

    /// @notice Validate intent structure and timing
    function _validateIntent(Intent memory intent) internal view {
        // Nonce check
        require(
            intent.nonce > nonce,
            "Invalid nonce"
        );

        // Deadline check
        require(
            block.timestamp <= intent.deadline,
            "Intent expired"
        );

        // Actions non-empty
        require(
            intent.actions.length > 0,
            "No actions"
        );
    }

    /// @notice Execute all actions in the intent
    function _executeActions(Action[] memory actions)
        internal
        returns (address[] memory)
    {
        address[] memory protocols = new address[](actions.length);

        for (uint256 i = 0; i < actions.length; i++) {
            Action memory action = actions[i];

            // Get adapter for protocol
            address adapter = adapters[action.protocolId];
            require(adapter != address(0), "Unknown protocol");

            // Execute via adapter
            (bool success, bytes memory result) = adapter.call(
                abi.encodePacked(action.selector, action.params)
            );

            require(success, string(result));
            protocols[i] = adapter;
        }

        return protocols;
    }

    /// @notice Recover signer from signature
    function _recoverSigner(bytes32 hash, bytes memory signature)
        internal
        pure
        returns (address)
    {
        require(signature.length == 65, "Invalid signature length");

        bytes32 r;
        bytes32 s;
        uint8 v;

        assembly {
            r := mload(add(signature, 32))
            s := mload(add(signature, 64))
            v := byte(0, mload(add(signature, 96)))
        }

        if (v < 27) {
            v += 27;
        }

        require(v == 27 || v == 28, "Invalid signature v");

        return ecrecover(hash, v, r, s);
    }

    /// @inheritdoc IRelayedVault
    function getNonce() external view override returns (uint256) {
        return nonce;
    }

    /// @notice Register protocol adapter
    function registerAdapter(
        bytes32 protocolId,
        address adapter
    ) external onlyRole(ADMIN_ROLE) {
        require(adapter != address(0), "Invalid adapter");
        adapters[protocolId] = adapter;
        emit AdapterRegistered(protocolId, adapter);
    }

    /// @notice Update Canton signer
    function updateCantonSigner(address newSigner)
        external
        onlyRole(ADMIN_ROLE)
    {
        require(newSigner != address(0), "Invalid signer");
        cantonSigner = newSigner;
    }

    /// @inheritdoc IRelayedVault
    function pause() external override onlyRole(EMERGENCY_ROLE) {
        _pause();
    }

    /// @inheritdoc IRelayedVault
    function unpause() external override onlyRole(EMERGENCY_ROLE) {
        _unpause();
    }

    /// @notice Emergency withdraw
    function emergencyWithdraw(
        address token,
        uint256 amount,
        address recipient
    ) external onlyRole(EMERGENCY_ROLE) whenPaused {
        if (token == address(0)) {
            payable(recipient).transfer(amount);
        } else {
            IERC20(token).transfer(recipient, amount);
        }
    }
}

Security Features

1. Intent Validation

The contract rigorously validates all intents:

solidity
function _validateIntent(Intent memory intent) internal view {
    // ✓ Nonce must be strictly increasing
    require(intent.nonce > nonce, "Invalid nonce");

    // ✓ Intent must not be expired
    require(block.timestamp <= intent.deadline, "Intent expired");

    // ✓ Actions must be non-empty
    require(intent.actions.length > 0, "No actions");

    // ✓ Vault ID must match (if tracked)
    require(
        keccak256(bytes(intent.vaultId)) == keccak256(bytes(vaultId)),
        "Vault mismatch"
    );
}

2. Replay Protection

Multiple layers of replay protection:

Nonce Enforcement:

solidity
// Strict ordering: must be greater than current nonce
require(intent.nonce > nonce, "Invalid nonce");

// Update immediately after validation
nonce = intent.nonce;

Deadline Enforcement:

solidity
// Intent cannot be executed after deadline
require(
    block.timestamp <= intent.deadline,
    "Intent expired"
);

Intent Hash Tracking (Optional):

solidity
mapping(bytes32 => bool) public executedIntents;

function _checkReplay(bytes32 intentHash) internal {
    require(!executedIntents[intentHash], "Already executed");
    executedIntents[intentHash] = true;
}

3. Access Control

Role-based access for administrative functions:

solidity
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
bytes32 public constant EMERGENCY_ROLE = keccak256("EMERGENCY_ROLE");

// Only admins can register adapters
function registerAdapter(bytes32 protocolId, address adapter)
    external
    onlyRole(ADMIN_ROLE)
{
    adapters[protocolId] = adapter;
}

// Emergency actions require EMERGENCY_ROLE
function pause() external onlyRole(EMERGENCY_ROLE) {
    _pause();
}

Protocol Adapters

Modular adapters isolate protocol-specific logic:

Base Adapter Interface

solidity
interface IProtocolAdapter {
    function execute(bytes calldata params)
        external
        returns (bytes memory result);

    function protocol() external view returns (string memory);
}

Example: Uniswap V3 Adapter

solidity
contract UniswapV3Adapter is IProtocolAdapter {
    ISwapRouter public immutable swapRouter;
    address public immutable vault;

    constructor(address _swapRouter, address _vault) {
        swapRouter = ISwapRouter(_swapRouter);
        vault = _vault;
    }

    modifier onlyVault() {
        require(msg.sender == vault, "Not vault");
        _;
    }

    function swap(
        address tokenIn,
        address tokenOut,
        uint256 amountIn,
        uint256 minAmountOut,
        uint24 fee
    ) external onlyVault returns (uint256 amountOut) {
        // Approve router
        IERC20(tokenIn).approve(address(swapRouter), amountIn);

        // Prepare swap params
        ISwapRouter.ExactInputSingleParams memory params =
            ISwapRouter.ExactInputSingleParams({
                tokenIn: tokenIn,
                tokenOut: tokenOut,
                fee: fee,
                recipient: vault,
                deadline: block.timestamp,
                amountIn: amountIn,
                amountOutMinimum: minAmountOut,
                sqrtPriceLimitX96: 0
            });

        // Execute swap
        amountOut = swapRouter.exactInputSingle(params);

        require(amountOut >= minAmountOut, "Insufficient output");
    }

    function protocol() external pure override returns (string memory) {
        return "Uniswap V3";
    }
}

Example: Aave V3 Adapter

solidity
contract AaveV3Adapter is IProtocolAdapter {
    IPool public immutable lendingPool;
    address public immutable vault;

    constructor(address _lendingPool, address _vault) {
        lendingPool = IPool(_lendingPool);
        vault = _vault;
    }

    modifier onlyVault() {
        require(msg.sender == vault, "Not vault");
        _;
    }

    function supply(
        address asset,
        uint256 amount
    ) external onlyVault {
        IERC20(asset).approve(address(lendingPool), amount);
        lendingPool.supply(asset, amount, vault, 0);
    }

    function withdraw(
        address asset,
        uint256 amount
    ) external onlyVault returns (uint256) {
        return lendingPool.withdraw(asset, amount, vault);
    }

    function protocol() external pure override returns (string memory) {
        return "Aave V3";
    }
}

Gas Optimization

Batch Actions

Process multiple actions in a single transaction:

solidity
function _executeActions(Action[] memory actions)
    internal
    returns (address[] memory protocols)
{
    protocols = new address[](actions.length);

    for (uint256 i = 0; i < actions.length; i++) {
        // Cache to memory
        Action memory action = actions[i];

        // Single SLOAD for adapter lookup
        address adapter = adapters[action.protocolId];
        require(adapter != address(0), "Unknown protocol");

        // Execute
        (bool success, bytes memory result) = adapter.call(
            abi.encodePacked(action.selector, action.params)
        );

        require(success, string(result));
        protocols[i] = adapter;
    }
}

Storage Optimization

solidity
// Pack related variables
struct VaultState {
    uint128 nonce;           // 16 bytes
    uint64 lastExecuted;     // 8 bytes
    uint64 totalExecutions;  // 8 bytes
    // Total: 32 bytes (single slot)
}

Circuit Breakers

Implement safety limits:

solidity
contract CircuitBreaker {
    uint256 public maxActionsPerIntent = 10;
    uint256 public maxValuePerAction = 1_000_000e18;
    uint256 public dailyVolumeLimit = 10_000_000e18;

    uint256 public dailyVolume;
    uint256 public lastResetTimestamp;

    function _checkCircuitBreakers(Intent memory intent) internal {
        // Max actions check
        require(
            intent.actions.length <= maxActionsPerIntent,
            "Too many actions"
        );

        // Daily volume check
        if (block.timestamp > lastResetTimestamp + 1 days) {
            dailyVolume = 0;
            lastResetTimestamp = block.timestamp;
        }

        require(
            dailyVolume < dailyVolumeLimit,
            "Daily limit exceeded"
        );
    }
}

Events for Monitoring

solidity
event IntentExecuted(
    uint256 indexed nonce,
    bytes32 indexed intentHash,
    address[] protocols,
    bool success
);

event ActionExecuted(
    uint256 indexed nonce,
    uint256 indexed actionIndex,
    bytes32 protocolId,
    bytes4 selector,
    bool success
);

event IntentFailed(
    uint256 indexed nonce,
    bytes32 indexed intentHash,
    string reason
);

event EmergencyPause(
    address indexed caller,
    uint256 timestamp
);

Testing Checklist

  • [ ] Signature verification with valid Canton keys
  • [ ] Rejection of invalid signatures
  • [ ] Nonce replay protection
  • [ ] Deadline expiration handling
  • [ ] Action execution success/failure
  • [ ] Adapter registration and updates
  • [ ] Emergency pause functionality
  • [ ] Emergency withdrawal
  • [ ] Gas optimization benchmarks
  • [ ] Reentrancy attack prevention

Canton DeFi - Multichain DeFi Technical Reference