The Compact Protocol

repository·main·Indexed 41 days ago

https://github.com/uniswap/the-compact

An ownerless ERC6909 contract that facilitates the voluntary formation and mediation of reusable resource locks. It enables users to commit ERC20 or native tokens as collateral or payment in asynchronous environments, utilizing arbiters to process claims and allocators to manage trust and prevent double-spending.

Tokens
8.9K
Snippets
14
Records
41
Agent score
51%

What's inside The Compact

  1. What is The Compact?

    main

    The Compact is an ownerless ERC6909 contract designed to facilitate the voluntary formation and mediation of reusable resource locks. It allows tokens (ERC20 or native) to be committed to a lock, enabling them to be spent in exchange for performing actions across asynchronous environments. Once conditions are met, these tokens can be claimed via a compact through a specified arbiter.

    Key actors include:

    • Depositor: The token holder who enters a resource lock.
    • Sponsor: The owner of the ERC6909 token representing a resource lock who creates a compact.
    • Arbiter: The entity responsible for processing claims after attesting that compact conditions are met.
    • Allocator: An entity assigned by the depositor to provide authorization for lock operations (transfers, withdrawals, or sponsoring compacts) and protect claimants from double-spends.
    • Claimant: An entity that provides proof of meeting compact conditions to claim tokens.
    • Emissary: An optional fallback signer assigned by a sponsor to authorize claims.
  2. Implement or Use Arbiters

    main

    Arbiters are responsible for verifying and submitting claims. When a sponsor creates a compact, they designate an arbiter to:

    1. Verify that compact conditions are met (via witness data).
    2. Process the claim by calling functions on ITheCompactClaims.
    3. Specify claimants and their entitlement (direct transfer, withdrawal, or conversion) in the claim payload.
  3. Understand Resource Locks

    main

    Resource locks are the fundamental building blocks of The Compact. They are created when a depositor places tokens (native or ERC20) into The Compact. Each lock is represented by a fungible ERC6909 tokenId and is defined by four properties:

    1. Underlying token: The asset held in the lock.
    2. Allocator: The entity responsible for cosigning claims and preventing double-spending.
    3. Scope: Whether the lock is spendable on any chain or limited to a single chain.
    4. Reset period: The duration required for forced withdrawals or emissary reassignment timelocks.

    Technical Details:

    • The scope, resetPeriod, and allocatorId are packed into a bytes12 lockTag.
    • The ERC6909 tokenId is a uint256 concatenation of the lockTag and the underlying token address.
    • Fee-on-Transfer tokens: Supported. Minting/burning is based on actual balance changes.
    • Rebasing tokens (e.g., stETH): NOT supported in V1. Use wrapped versions (e.g., wstETH) to avoid value loss.
  4. How Resource Locks and Compacts work together

    main

    The Compact ecosystem relies on a hierarchy of abstractions to manage asynchronous commitments:

    1. Resource Lock: Established by a depositor who commits ERC20 or native tokens. The depositor assigns an allocator and a reset period to the lock.
    2. Compact: Created by a sponsor (the owner of the ERC6909 tokens representing the lock). A compact is a commitment that allows claimants to claim tokens through an arbiter.
    3. Authorization Flow: The allocator ensures the credibility of the lock by authorizing the sponsor's ability to use the tokens. This prevents double-spending of locked balances.
    4. Claim Flow: A claimant proves they met the compact's conditions, and the arbiter processes the claim to release the tokens.

    Trust Assumptions for Users:

    • Claimants must trust that the allocator is sound (won't underallocate) and the arbiter is sound (won't process invalid claims or fail to process valid ones).
    • Sponsors must trust that the allocator won't censor requests and the arbiter is sound (won't process invalid claims).
  5. Implement or Use Allocators

    main

    Allocators mediate resource locks. They must be registered with The Compact via __registerAllocator before assignment.

    Responsibilities:

    • Preventing double-spending of committed tokens.
    • Validating ERC6909 transfers via IAllocator.attest.
    • Authorizing claims via IAllocator.authorizeClaim.
    • Managing nonces (optionally consuming them via consume).

    Allocators must implement the IAllocator interface.

  6. Understand the Compact protocol trust model

    main

    The Compact protocol operates with varying trust requirements for different actors. Understanding these is critical for both Sponsors and Claimants to manage risk.

    • Allocators: Sponsors must trust that allocators will not censor valid requests against fully funded locks. Sponsors can initiate forced withdrawals if allocators are unresponsive.
    • Arbiters: Sponsors must trust that arbiters will not process claims where specified conditions were not met.
    • Emissaries: Sponsors must trust that emissaries (if assigned) will not authorize claims maliciously. Emissaries have the same authorization power as the sponsor for claim verification.

    Claimant Trust Requirements

    • Allocators: Claimants must trust that allocators are sound and will not allow resource locks to become underfunded (e.g., via double-spending).
    • Arbiters: Claimants must trust that arbiters will not fail to process claims where conditions were properly met.
    • Emissaries: Claimants must trust that emissaries will faithfully authorize valid claims if the sponsor is able to equivocate or revoke authorization (e.g., via EIP-7702 or EIP-1271). It is recommended to use a small set of known, "canonical" emissaries that enforce delays before allowing key rotation.
  7. Forced Withdrawals for Resource Locks

    main

    If an allocator fails to authorize the use of an unallocated resource lock balance (either erroneously or maliciously), the depositor has a safety mechanism called a forced withdrawal.

    To perform a forced withdrawal:

    1. The depositor must wait for the reset period that was specified when the tokens were originally deposited into the lock.
    2. After the reset period has elapsed, the depositor can withdraw their underlying balance at will, bypassing the need for the allocator's explicit permission.
  8. Allocator Workflow: Implementing Infrastructure

    main

    Allocators are responsible for resource lock integrity and claim authorization.

    1. Registration

    Register via __registerAllocator to obtain an allocatorId. Registration is permitted if:

    • The caller is the allocator address.
    • The address contains code.
    • A valid create2 deployment proof is provided.

    Create2 Proof Format: An 85-byte proof: 0xff ++ factory ++ salt ++ initcode hash.

    2. Implementation Requirements

    Deploy a contract implementing IAllocator:

    • attest: Called during ERC6909 transfers; must verify safety and return IAllocator.attest.selector.
    • authorizeClaim / isClaimAuthorized: Validates claims against sponsor balances and nonces. authorizeClaim must return IAllocator.authorizeClaim.selector for on-chain validation.

    3. Optional Features

    • Off-chain Logic: Use off-chain systems to track balances, validate requests, and generate allocatorData (signatures).
    • Nonce Consumption: Proactively invalidate compacts using consume on The Compact contract.
  9. Register Compacts Directly

    main

    Instead of signing EIP-712 payloads, compacts can be registered on-chain. This is useful for DAOs, protocols, or smart wallets that cannot perform standard ECDSA signing.

    Process: Submit a claimHash and its typehash to the contract.

    Key Properties:

    • Registrations do not expire.
    • Registrations cannot be unregistered by the sponsor.
    • Registrations are invalidated if the allocator consumes the nonce or if the compact expires.
    • Once a claim is processed, the registration state is cleared.

    Querying Status: Use ITheCompact.isRegistered(sponsor, claimHash, typehash) to check if a compact is registered.

  10. Development and testing guide for The Compact

    main

    Testing Framework

    • Foundry: The primary tool for testing and development.
    • Hardhat: A small number of optional Hardhat tests are available.

    Test Categories

    • Unit Tests: Located in test/unit/ for individual component logic.
    • Integration Tests: Located in test/integration/ for end-to-end workflows.
    • Benchmark Tests: Used for measuring withdrawal costs.
    • Permit2 Tests: Covers Permit2 integration and witness data handling.

    Important Development Notes

    • Solady Integration: The contract uses Solady's ERC6909 implementation. Be cautious of storage convention differences between The Compact and Solady to avoid collisions.
    • Gas Optimization: The codebase uses extensive assembly and custom errors for maximum gas efficiency.
    • Documentation: All external functions include NatSpec documentation.
    • Permit2: Extensive integration with Permit2 enables gasless transactions with support for custom witness data.
  11. Construct Witness Typestrings for Mandates

    main

    The Mandate struct allows extending compacts with custom data. The protocol uses the hash of this data for validation.

    Crucial Rule for Typestrings: When providing the witnessTypestring during a claim, include the arguments inside the Mandate struct, but do not include the closing parenthesis. The protocol appends it dynamically.

    Example: If your mandate is Mandate(uint256 myArg, bytes32 otherArg)MandateCondition(bool flag, uint256 val):

    • Correct Typestring: MandateCondition condition,uint256 arg)MandateCondition(bool flag,uint256 val
    • Incorrect: MandateCondition condition,uint256 arg)MandateCondition(bool flag,uint256 val) (with trailing parenthesis)

    Best Practice: To ensure correct alphanumeric ordering required by EIP-712, prefix nested structs with Mandate (e.g., MandateCondition).

  12. Assign and Update Emissaries

    main

    Emissaries provide fallback verification for sponsors, useful for smart contract accounts (EIP-1271) or delegating verification to a third party.

    Workflow:

    1. Assign: Use assignEmissary to set an emissary for a specific lockTag.
    2. Update: To change an emissary, you cannot simply overwrite it. You must:
      • Call scheduleEmissaryAssignment.
      • Wait for the resetPeriod associated with the lockTag to elapse.
      • Call assignEmissary again with the new address (or address(0) to remove).

    Emissaries must implement IEmissary.verifyClaim.