Skip to content

Appendix A: Example End-to-End Intent Payload

The serialized ExecutionIntent payload sent by the Relayer to the public vault contract contains all the verifiable instructions required for execution without exposing sensitive policy details.

Intent Payload Structure

JSON Format

json
{
  "vaultId": "vault-eth-001",
  "nonce": 42,
  "actions": [
    {
      "protocolId": "0x4161766500000000000000000000000000000000000000000000000000000000",
      "protocol": "Aave",
      "operation": "withdraw",
      "params": {
        "asset": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
        "amount": "50000000000",
        "recipient": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e"
      }
    },
    {
      "protocolId": "0x556e697377617056330000000000000000000000000000000000000000000000",
      "protocol": "UniswapV3",
      "operation": "swap",
      "params": {
        "tokenIn": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
        "tokenOut": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
        "amountIn": "50000000000",
        "minAmountOut": "16500000000000000000",
        "fee": "3000",
        "recipient": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e"
      }
    }
  ],
  "deadline": "2025-10-14T12:00:00Z",
  "issuerPartyId": "CantonIssuer::0x1A2B3C4D5E6F..."
}

ABI-Encoded Payload

Solidity Struct Definition

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

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

Encoding Example

typescript
import { ethers } from 'ethers';

function encodeIntent(intent: Intent): string {
  const intentType = [
    'tuple(string vaultId, uint256 nonce, tuple(bytes32 protocolId, bytes4 selector, bytes params)[] actions, uint256 deadline)'
  ];

  const encoded = ethers.utils.defaultAbiCoder.encode(
    intentType,
    [{
      vaultId: intent.vaultId,
      nonce: intent.nonce,
      actions: intent.actions.map(a => ({
        protocolId: ethers.utils.id(a.protocol),
        selector: ethers.utils.id(a.operation).slice(0, 10),
        params: encodeActionParams(a)
      })),
      deadline: Math.floor(new Date(intent.deadline).getTime() / 1000)
    }]
  );

  return encoded;
}

function encodeActionParams(action: Action): string {
  switch (action.operation) {
    case 'withdraw':
      return ethers.utils.defaultAbiCoder.encode(
        ['address', 'uint256', 'address'],
        [action.params.asset, action.params.amount, action.params.recipient]
      );

    case 'swap':
      return ethers.utils.defaultAbiCoder.encode(
        ['address', 'address', 'uint256', 'uint256', 'uint24', 'address'],
        [
          action.params.tokenIn,
          action.params.tokenOut,
          action.params.amountIn,
          action.params.minAmountOut,
          action.params.fee,
          action.params.recipient
        ]
      );

    default:
      throw new Error(`Unknown operation: ${action.operation}`);
  }
}

Complete Transaction Flow

1. Canton Generates Intent

daml
-- In Canton
choice GenerateIntent : ContractId ExecutionIntent
  controller operator
  do
    now <- getTime

    create ExecutionIntent with
      vaultId = "vault-eth-001"
      nonce = 42
      actions = [
        Action with
          protocol = "Aave"
          operation = "withdraw"
          params = [("asset", "0xA0b8..."), ("amount", "50000000000")]
        ,
        Action with
          protocol = "UniswapV3"
          operation = "swap"
          params = [
            ("tokenIn", "0xA0b8..."),
            ("tokenOut", "0xC02a..."),
            ("amountIn", "50000000000"),
            ("minAmountOut", "16500000000000000000")
          ]
      ]
      deadline = addRelTime now (hours 1)
      issuer = operator

2. Canton Signs Intent

typescript
// Canton signing service
const payload = encodeIntent(intent);
const payloadHash = ethers.utils.keccak256(payload);

// Sign with Canton's HSM-backed key
const signature = await cantonSigner.signMessage(
  ethers.utils.arrayify(payloadHash)
);

console.log('Payload Hash:', payloadHash);
// 0x7b3f8e9a2c1d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f

console.log('Signature:', signature);
// 0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b...

3. Relayer Submits to Chain

typescript
// Relayer prepares transaction
const vaultContract = new ethers.Contract(
  VAULT_ADDRESS,
  VAULT_ABI,
  relayerWallet
);

const tx = await vaultContract.executeIntent(
  payload,  // ABI-encoded intent
  signature,  // Canton's signature
  '0x'  // Empty proof for Pattern 1
);

console.log('Transaction submitted:', tx.hash);
// 0x9f8e7d6c5b4a3928170615e4d3c2b1a0998877665544332211

await tx.wait();
console.log('Transaction confirmed');

4. Vault Verifies and Executes

solidity
// In the vault contract
function executeIntent(
    bytes calldata intentPayload,
    bytes calldata cantonSig,
    bytes calldata proof
) external nonReentrant whenNotPaused {
    // 1. Verify Canton signature
    bytes32 payloadHash = keccak256(intentPayload);
    address signer = recoverSigner(payloadHash, cantonSig);
    require(signer == cantonSigner, "Invalid signature");

    // 2. Decode intent
    ExecutionIntent memory intent = abi.decode(
        intentPayload,
        (ExecutionIntent)
    );

    // 3. Validate
    require(intent.nonce > nonce, "Invalid nonce");
    require(block.timestamp <= intent.deadline, "Expired");

    // 4. Execute actions
    for (uint i = 0; i < intent.actions.length; i++) {
        Action memory action = intent.actions[i];
        address adapter = adapters[action.protocolId];

        (bool success, ) = adapter.call(
            abi.encodePacked(action.selector, action.params)
        );
        require(success, "Action failed");
    }

    // 5. Update nonce
    nonce = intent.nonce;

    emit IntentExecuted(intent.nonce, payloadHash);
}

Privacy Analysis

Data Exposed Publicly

FieldValueVisibility
Vault IDvault-eth-001Public (on-chain)
Nonce42Public (on-chain)
ProtocolAave, UniswapV3Public (on-chain)
Operationwithdraw, swapPublic (on-chain)
Token Addresses0xA0b8..., 0xC02a...Public (on-chain)
Amounts50000000000, etc.Public (on-chain)
Deadline2025-10-14T12:00:00ZPublic (on-chain)

Data Kept Private (Canton Only)

FieldDescriptionVisibility
Strategy AlgorithmOptimization logicPrivate (Canton)
Risk ScoresInternal risk assessmentPrivate (Canton)
Oracle DataProprietary price feedsPrivate (Canton)
Policy DetailsFull policy constraintsPrivate (Canton)
Approval DiscussionInternal deliberationsPrivate (Canton)
Future PlansUpcoming strategy changesPrivate (Canton)

Complete End-to-End Example

1. Canton: Strategy computes optimal allocation
   Private: "Move 30% from Aave to Uniswap based on risk score 7.2"

2. Canton: Policy check passes
   Private: maxExposure=50%, current=30%, new=40% ✓

3. Canton: Generate and sign intent
   Public Payload: {
     vaultId: "vault-eth-001",
     nonce: 42,
     actions: [withdraw from Aave, swap on Uniswap],
     deadline: "..."
   }
   Signature: 0x1a2b3c...

4. Relayer: Verify signature and submit
   Transaction: 0x9f8e7d...

5. Vault Contract: Verify and execute
   Event: IntentExecuted(nonce=42, hash=0x7b3f...)

6. Relayer: Report back to Canton
   Private: success=true, gasUsed=187423, slippage=0.12%

7. Canton: Update internal state
   Private: Portfolio now 40% Uniswap, performance +2.3%

Conclusion

This JSON payload is cryptographically signed by the Canton Policy Issuer Party, resulting in the cantonSig. The Relayer then packages this signed payload and submits it, along with any necessary accompanying signatures or proofs, to the public vault contract for execution.

The payload contains the minimum necessary data for execution while keeping all sensitive strategy details, risk assessments, and policy logic completely private within the Canton domain.

Canton DeFi - Multichain DeFi Technical Reference