Skip to main content

What are Attestations?

Attestations are cryptographic signatures that prove a cross-chain message is legitimate. In CCTP, attestations serve as the security bridge between chains:
  • Burn Proof: Attestation confirms tokens were actually burned on the source chain
  • Authorization: Only valid attestations allow minting on the destination chain
  • Decentralization: Multiple independent attesters must sign (m-of-n multisig)
  • Replay Prevention: Each attestation is valid for exactly one (sourceDomain, nonce) pair
Without valid attestations, it’s impossible to mint tokens on the destination chain, preventing unauthorized USDC creation.

Why Attestations Are Needed

Blockchains cannot natively communicate with each other. CCTP bridges this gap:

The Problem

Avalanche contracts cannot directly verify that USDC was burned on Ethereum. Without attestations:
  • Anyone could claim to have burned tokens
  • Malicious actors could mint unlimited USDC
  • Cross-chain transfers would be impossible to trust

The Solution

Trusted attesters observe burns and provide cryptographic proof that minting is authorized.

Attester Management

CCTP uses the Attestable role contract (see src/roles/Attestable.sol:22) to manage attesters:

Attester Lifecycle

Enabling Attesters

Only the attesterManager can enable new attesters:
function enableAttester(address newAttester) public onlyAttesterManager {
    require(newAttester != address(0), "New attester must be nonzero");
    require(enabledAttesters.add(newAttester), "Attester already enabled");
    emit AttesterEnabled(newAttester);
}
Requirements:
  • newAttester must not be zero address
  • newAttester must not already be enabled
  • Caller must be the attester manager
Example:
await messageTransmitter.methods
  .enableAttester('0x1234...')
  .send({ from: attesterManagerAddress });

Disabling Attesters

To disable a compromised or retired attester:
function disableAttester(address attester) external onlyAttesterManager {
    // Cannot disable if only 1 attester remains
    uint256 _numEnabledAttesters = getNumEnabledAttesters();
    require(_numEnabledAttesters > 1, "Too few enabled attesters");
    
    // Cannot disable if it would reduce below signature threshold
    require(
        _numEnabledAttesters > signatureThreshold,
        "Signature threshold is too low"
    );
    
    require(enabledAttesters.remove(attester), "Attester already disabled");
    emit AttesterDisabled(attester);
}
Safety Checks:
  • At least 1 attester must remain enabled
  • Number of enabled attesters must exceed signature threshold
  • Ensures system remains operational after disabling
Security: Disabling an attester does not invalidate existing attestations signed by that attester. Only future attestations are affected.

Signature Threshold

The signature threshold determines how many attesters must sign each message (m in m-of-n multisig):
function setSignatureThreshold(
    uint256 newSignatureThreshold
) external onlyAttesterManager {
    require(newSignatureThreshold != 0, "Invalid signature threshold");
    
    // Threshold cannot exceed number of enabled attesters
    require(
        newSignatureThreshold <= enabledAttesters.length(),
        "New signature threshold too high"
    );
    
    require(
        newSignatureThreshold != signatureThreshold,
        "Signature threshold already set"
    );
    
    uint256 _oldSignatureThreshold = signatureThreshold;
    signatureThreshold = newSignatureThreshold;
    emit SignatureThresholdUpdated(_oldSignatureThreshold, newSignatureThreshold);
}
Validation Rules:
  • Threshold must be at least 1
  • Threshold cannot exceed number of enabled attesters
  • Threshold must be different from current value
Security Tradeoff:
  • Higher threshold: More secure (more attesters needed to compromise), but slower and less fault-tolerant
  • Lower threshold: Faster and more fault-tolerant, but less secure
Example Configuration:
Enabled Attesters: 5
Signature Threshold: 3
Result: Any 3 of the 5 attesters can sign to authorize minting

Attester Manager Role

The attester manager has exclusive control over the attester set:
function updateAttesterManager(
    address newAttesterManager
) external onlyOwner {
    _setAttesterManager(newAttesterManager);
}

function _setAttesterManager(address _newAttesterManager) internal {
    require(
        _newAttesterManager != address(0),
        "Invalid attester manager address"
    );
    address _oldAttesterManager = _attesterManager;
    _attesterManager = _newAttesterManager;
    emit AttesterManagerUpdated(_oldAttesterManager, _newAttesterManager);
}
Responsibilities:
  • Enable new attesters
  • Disable compromised attesters
  • Adjust signature threshold
Access Control:
  • Only contract owner can change attester manager
  • Attester manager cannot transfer their own role

Signature Verification

The core security mechanism is signature verification in _verifyAttestationSignatures() (see src/roles/Attestable.sol:227):

Attestation Format

Attestations are concatenated 65-byte ECDSA signatures:
[signature1][signature2][signature3]...[signatureN]
│         ││         ││         │     │         │
│   65B   ││   65B   ││   65B   │ ... │   65B   │
│         ││         ││         │     │         │
└─────────┘└─────────┘└─────────┘     └─────────┘
Each 65-byte signature consists of:
  • r (32 bytes): Signature component
  • s (32 bytes): Signature component
  • v (1 byte): Recovery ID
Total Length: 65 * signatureThreshold bytes

Verification Algorithm

function _verifyAttestationSignatures(
    bytes calldata _message,
    bytes calldata _attestation
) internal view {
    // 1. Validate attestation length
    require(
        _attestation.length == signatureLength * signatureThreshold,
        "Invalid attestation length"
    );
    
    // 2. Hash the message
    bytes32 _digest = keccak256(_message);
    
    // 3. Track last attester address (for ordering check)
    address _latestAttesterAddress = address(0);
    
    // 4. Verify each signature
    for (uint256 i = 0; i < signatureThreshold; ++i) {
        // Extract individual signature
        bytes memory _signature = _attestation[
            i * signatureLength : i * signatureLength + signatureLength
        ];
        
        // Recover signer address from signature
        address _recoveredAttester = _recoverAttesterSignature(
            _digest,
            _signature
        );
        
        // 5. Enforce increasing address order (prevents duplicates)
        require(
            _recoveredAttester > _latestAttesterAddress,
            "Invalid signature order or dupe"
        );
        
        // 6. Verify signer is enabled attester
        require(
            isEnabledAttester(_recoveredAttester),
            "Invalid signature: not attester"
        );
        
        _latestAttesterAddress = _recoveredAttester;
    }
}

Verification Steps Explained

Step 1: Length Validation

Ensures attestation contains exactly the required number of signatures:
require(
    _attestation.length == 65 * signatureThreshold,
    "Invalid attestation length"
);
Examples:
  • Threshold = 1: Attestation must be exactly 65 bytes
  • Threshold = 3: Attestation must be exactly 195 bytes
  • Threshold = 5: Attestation must be exactly 325 bytes

Step 2: Message Hashing

bytes32 _digest = keccak256(_message);
The complete message bytes (including all fields) are hashed. Attesters sign this hash, not the raw message.

Step 3-4: Signature Recovery

For each signature, recover the signer’s address using ECDSA:
function _recoverAttesterSignature(
    bytes32 _digest,
    bytes memory _signature
) internal pure returns (address) {
    return ECDSA.recover(_digest, _signature);
}
Uses OpenZeppelin’s ECDSA.recover() which:
  1. Extracts v, r, s from signature
  2. Performs elliptic curve point recovery
  3. Returns the address that created the signature

Step 5: Increasing Address Order

Critical Security Check: Signatures must be ordered by ascending attester address:
require(
    _recoveredAttester > _latestAttesterAddress,
    "Invalid signature order or dupe"
);
Why This Matters:
  • Prevents Duplicate Signatures: Same signature cannot be used twice (address would not increase)
  • Simplifies Verification: O(n) complexity instead of O(n²) for checking duplicates
  • Canonical Format: Only one valid ordering exists for any signature set
Example:
Valid:   [sig_0x1234] [sig_0x5678] [sig_0x9ABC]  ✓
Invalid: [sig_0x5678] [sig_0x1234] [sig_0x9ABC]  ✗ (wrong order)
Invalid: [sig_0x1234] [sig_0x1234] [sig_0x5678]  ✗ (duplicate)

Step 6: Attester Authorization

require(
    isEnabledAttester(_recoveredAttester),
    "Invalid signature: not attester"
);
Verifies the recovered address is currently in the enabled attester set. This prevents:
  • Random addresses from providing signatures
  • Disabled/retired attesters from authorizing transfers

Complete Verification Example

// Example: 3-of-5 multisig (threshold = 3, enabled = 5)

const enabledAttesters = [
  '0x0000...1111',  // Attester 1
  '0x0000...2222',  // Attester 2
  '0x0000...3333',  // Attester 3
  '0x0000...4444',  // Attester 4
  '0x0000...5555'   // Attester 5
];

// Valid attestation: signatures from attesters 1, 3, and 5 (in order)
const validAttestation = 
  signature_from_0x0000_1111 +  // 65 bytes
  signature_from_0x0000_3333 +  // 65 bytes
  signature_from_0x0000_5555;   // 65 bytes
// Total: 195 bytes

// Invalid: wrong order
const invalidAttestation = 
  signature_from_0x0000_3333 +  // ✗ Not in ascending order
  signature_from_0x0000_1111 +
  signature_from_0x0000_5555;

// Invalid: duplicate
const invalidAttestation2 = 
  signature_from_0x0000_1111 +  // ✗ Duplicate signature
  signature_from_0x0000_1111 +
  signature_from_0x0000_3333;

// Invalid: unauthorized signer
const invalidAttestation3 = 
  signature_from_0x0000_1111 +
  signature_from_0x0000_6666 +  // ✗ Not an enabled attester
  signature_from_0x0000_3333;

Attestation Service Architecture

Circle operates the off-chain attestation service:

Service Responsibilities

  1. Event Monitoring: Listen for MessageSent events on all supported chains
  2. Validation:
    • Verify transaction confirmation and finality
    • Check burn amount is within limits
    • Validate destination domain is supported
    • Confirm source contract is legitimate TokenMessenger
  3. Signing: Generate signatures with authorized attester keys
  4. API: Provide attestations via REST API with rate limiting
  5. Observability: Log all attestations for audit trails

Attestation API

Endpoint:
GET https://iris-api.circle.com/attestations/{messageHash}
Request:
curl https://iris-api.circle.com/attestations/0xabc123...
Response - Pending:
{
  "status": "pending"
}
Response - Complete:
{
  "status": "complete",
  "attestation": "0x1234567890abcdef..."
}
Rate Limits:
  • Maximum 1 request per second per IP
  • Exceeding limit returns HTTP 429
Best Practice: Poll with exponential backoff (2s, 4s, 8s, 16s…) to respect rate limits and reduce unnecessary requests.

Security Considerations

Attester Key Management

Critical: Attester private keys are the root of CCTP security
  • Hardware Security Modules (HSMs): Keys stored in tamper-resistant hardware
  • Multi-Party Computation (MPC): Distributed key generation and signing
  • Key Rotation: Periodic rotation of attester keys
  • Separation: Each attester key held by independent party

Threshold Selection

Choosing the right threshold balances security and availability: Example Scenarios:
EnabledThresholdSecurity LevelFault Tolerance
32Moderate1 attester can fail
53High2 attesters can fail
75Very High2 attesters can fail
97Maximum2 attesters can fail
Recommendation: Set threshold to at least floor(n/2) + 1 where n is number of enabled attesters (Byzantine fault tolerance).

Attack Vectors and Mitigations

Replay Attacks

Attack: Reuse valid attestation to mint tokens multiple times Mitigation: Nonce tracking in MessageTransmitter
require(usedNonces[_sourceAndNonce] == 0, "Nonce already used");
usedNonces[_sourceAndNonce] = 1;

Signature Malleability

Attack: Modify signature to create alternative valid signature for same message Mitigation:
  • ECDSA library validates signature format
  • Increasing address order requirement ensures canonical representation

Attester Compromise

Attack: Compromise m attesters to authorize fraudulent mints Mitigation:
  • Geographic distribution of attesters
  • Different security vendors for each attester
  • Regular security audits
  • Ability to quickly disable compromised attesters

Front-Running

Attack: Monitor mempool and submit receiveMessage before intended recipient Mitigation: Use depositForBurnWithCaller() to restrict who can complete transfer

Monitoring and Observability

Key Metrics

Attestation Service:
  • Average attestation time
  • Success/failure rate
  • API request rate and errors
On-Chain:
  • Number of enabled attesters: getNumEnabledAttesters()
  • Current threshold: signatureThreshold
  • Attester manager: attesterManager()

Events

event AttesterEnabled(address indexed attester);
event AttesterDisabled(address indexed attester);
event SignatureThresholdUpdated(uint256 oldThreshold, uint256 newThreshold);
event AttesterManagerUpdated(address indexed previousManager, address indexed newManager);
Monitor these events to track attester set changes and detect anomalies.

Next Steps

Message Flow

See how attestations fit into the complete transfer flow

Quickstart

Build your first integration with attestations

Build docs developers (and LLMs) love