Skip to content

Resource Management

Overview

The Protocol leverages an expansive locking system from the-compact. We have incorporated Mandates and Solver Payloads to allow Intents to be solved on a single chain without provisioning up front capital as we arbiters can confirm mandates have been met by solvers at execution time, thus solvers may use the swappers locked funds for execution.

As of July 25th the the-compact we are developing on has been forked from Uniswap the-compact v1 branch which has not as yet been deployed.

Summary

The Compact is an ownerless ERC6909 contract that facilitates the voluntary formation and mediation of reusable resource locks. It enables tokens to be credibly committed to be spent in exchange for performing actions across arbitrary, asynchronous environments, and claimed once the specified conditions have been met.

Resource locks are entered into by ERC20 or native token holders (called the depositor). Once a resource lock has been established, the owner of the ERC6909 token representing a resource lock can act as a sponsor and create a compact. A compact is a commitment allowing interested parties to claim their tokens through the sponsor's indicated arbiter. The arbiter is then responsible for processing the claim once it has attested to the specified conditions of the compact having been met.

When depositing into a resource lock, the depositor assigns an allocator and a reset period for that lock. The allocator is tasked with providing additional authorization whenever the owner of the lock wishes to transfer their 6909 tokens, withdraw the underlying locked assets, or sponsor a compact utilizing the lock. Their primary role is essentially to protect claimants—entities that provide proof of having met the conditions and subsequently make a claim against a compact—by ensuring the credibility of commitments, such as preventing "double-spends" involving previously-committed locked balances.

Allocators can be purely onchain abstractions, or can involve hybrid (onchain + offchain) mechanics as part of their authorization procedure. Should an allocator erroneously or maliciously fail to authorize the use of an unallocated resource lock balance, the depositor can initiate a forced withdrawal for the lock in question; after waiting for the reset period indicated when depositing into the lock, they can withdraw their underlying balance at will without the allocator's explicit permission.

Sponsors can also optionally assign an emissary to act as a fallback signer for authorizing claims against their compacts. This is particularly helpful for smart contract accounts or other scenarios where signing keys might change.

The Compact effectively "activates" any deposited tokens to be instantly spent or swapped across arbitrary, asynchronous environments as long as:

  • Claimants are confident that the allocator is sound and will not leave the resource lock underallocated.
  • Sponsors are confident that the allocator will not unduly censor fully allocated requests.
  • Sponsors are confident that the arbiter is sound and will not process claims where the conditions were not successfully met.
  • Claimants are confident that the arbiter is sound and will not fail to process claims where the conditions were successfully met.

Key Concepts

Resource Locks

Resource locks are the fundamental building blocks of The Compact protocol. They are created when a depositor places tokens (either native tokens or ERC20 tokens) into The Compact. Each resource lock has four key properties:

  1. The underlying token held in the resource lock.
  2. The allocator tasked with cosigning on claims against the resource locks (see Allocators).
  3. The scope of the resource lock (either spendable on any chain or limited to a single chain).
  4. The reset period for forcibly exiting the lock (see Forced Withdrawals) and for emissary reassignment timelocks (see Emissaries).

Each unique combination of these four properties is represented by a fungible ERC6909 tokenID. The owner of these ERC6909 tokens can act as a sponsor and create compacts.

The scope, resetPeriod, and the allocatorId (obtained when an allocator is registered) are packed into a bytes12 lockTag. A resource lock's specific ID (the ERC6909 tokenId) is a concatenation of this lockTag and the underlying token address, represented as a uint256 for ERC6909 compatibility. This lockTag is used throughout various interfaces to succinctly identify the parameters of a lock.

Fee-on-Transfer and Rebasing Token Handling:
  • Fee-on-Transfer: The Compact correctly handles fee-on-transfer tokens for both deposits and withdrawals. The amount of ERC6909 tokens minted or burned is based on the actual balance change in The Compact contract, not just the specified amount. This ensures ERC6909 tokens accurately represent the underlying assets.
  • Rebasing Tokens: Rebasing tokens (e.g., stETH) are NOT supported in The Compact V1. Any yield or other balance changes occurring after deposit will not accrue to the depositor's ERC6909 tokens. For such assets, use their wrapped, non-rebasing counterparts (e.g., wstETH) to avoid loss of value.

Allocators

Each resource lock is mediated by an allocator. Their primary responsibilities include:

  1. Preventing Double-Spending: Ensuring sponsors don't commit the same tokens to multiple compacts or transfer away committed funds.
  2. Validating Transfers: Attesting to standard ERC6909 transfers of resource lock tokens (via IAllocator.attest).
  3. Authorizing Claims: Validating claims against resource locks (via IAllocator.authorizeClaim).
  4. Nonce Management: Ensuring nonces are not reused for claims and (optionally) consuming nonces directly on The Compact using consume.

Allocators must be registered with The Compact via __registerAllocator before they can be assigned to locks. They must implement the IAllocator interface and operate under specific trust assumptions.

Arbiters

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

  1. Verify that the specified conditions of the compact have been met (these conditions can be implicitly understood or explicitly defined via witness data).
  2. Process the claim by calling the appropriate function on The Compact (from ITheCompactClaims).
  3. Specify which claimants are entitled to the committed resources and in what form each claimant's portion will be issued (i.e., direct transfer, withdrawal, or conversion) as part of the claim payload.

Often, the entity fulfilling an off-chain condition (like a filler or solver) might interface directly with the arbiter. The trust assumptions around arbiters are critical to understand.

Emissaries

Emissaries provide a fallback verification mechanism for sponsors when authorizing claims. This is particularly useful for:

  1. Smart contract accounts that might update their EIP-1271 signature verification logic.
  2. Accounts using EIP-7702 delegation that leverages EIP-1271.
  3. Situations where the sponsor wants to delegate claim verification to a trusted third party.

A sponsor assigns an emissary for a specific lockTag using assignEmissary. The emissary must implement the IEmissary interface, specifically the verifyClaim function.

To change an emissary after one has been assigned, the sponsor must first call scheduleEmissaryAssignment, wait for the resetPeriod associated with the lockTag to elapse, and then call assignEmissary again with the new emissary's address (or address(0) to remove).

Compacts & EIP-712 Payloads

A compact is the agreement created by a sponsor that allows their locked resources to be claimed under specified conditions. The Compact protocol uses EIP-712 typed structured data for creating and verifying signatures for these agreements.

There are three main EIP-712 payload types a sponsor can sign:

  1. Compact: For single resource lock operations on a single chain.

    // Defined in src/types/EIP712Types.sol
    struct Compact {
        address arbiter;    // The account tasked with verifying and submitting the claim.
        address sponsor;    // The account to source the tokens from.
        uint256 nonce;      // A parameter to enforce replay protection, scoped to allocator.
        uint256 expires;    // The time at which the claim expires.
        bytes12 lockTag;    // A tag representing the allocator, reset period, and scope.
        address token;      // The locked token, or address(0) for native tokens.
        uint256 amount;     // The amount of ERC6909 tokens to commit from the lock.
        // (Optional) Witness data may follow:
        // Mandate mandate;
    }
  2. BatchCompact: For allocating multiple resource locks on a single chain.

    // Defined in src/types/EIP712Types.sol
    struct BatchCompact {
        address arbiter;            // The account tasked with verifying and submitting the claim.
        address sponsor;            // The account to source the tokens from.
        uint256 nonce;              // A parameter to enforce replay protection, scoped to allocator.
        uint256 expires;            // The time at which the claim expires.
        Lock[] commitments;         // The committed locks with lock tags, tokens, & amounts.
        // (Optional) Witness data may follow:
        // Mandate mandate;
    }
     
    struct Lock {
        bytes12 lockTag;    // A tag representing the allocator, reset period, and scope.
        address token;      // The locked token, or address(0) for native tokens.
        uint256 amount;     // The maximum committed amount of tokens.
    }
  3. MultichainCompact: For allocating one or more resource locks across multiple chains.

    // Defined in src/types/EIP712Types.sol
    struct MultichainCompact {
    address sponsor; // The account to source the tokens from.
    uint256 nonce; // A parameter to enforce replay protection, scoped to allocator.
    uint256 expires; // The time at which the claim expires.
    Element[] elements; // Arbiter, chainId, commitments, and mandate for each chain.
    }
     
        // Defined in src/types/EIP712Types.sol
        struct Element {
            address arbiter;            // The account tasked with verifying and submitting the claim.
            uint256 chainId;            // The chainId where the tokens are located.
            Lock[] commitments;         // The committed locks with lock tags, tokens, & amounts.
            // Witness data MUST follow (mandatory for multichain compacts):
            Mandate mandate;
        }
        ```
     
    The `Mandate` struct within these payloads is for [Witness Structure](#witness-structure). The EIP-712 typehash for these structures is constructed dynamically; empty `Mandate` structs result in a typestring without witness data. Witness data is optional _except_ in a `MultichainCompact`; a multichain compact's elements **must** include a witness.

Permit2 Integration Payloads: The Compact also supports integration with Permit2 for gasless deposits, using additional EIP-712 structures for witness data within Permit2 messages:

  • CompactDeposit(bytes12 lockTag,address recipient): For basic Permit2 deposits.
  • Activation(address activator,uint256 id,Compact compact)Compact(...)Mandate(...): Combines deposits with single compact registration.
  • BatchActivation(address activator,uint256[] ids,Compact compact)Compact(...)Mandate(...): Combines deposits with batch compact registration.

CompactCategory Enum: The Compact introduces a CompactCategory enum to distinguish between different types of compacts when using Permit2 integration:

// Defined in src/types/CompactCategory.sol
enum CompactCategory {
    Compact,
    BatchCompact,
    MultichainCompact
}

Witness Structure

The witness mechanism (Mandate struct) allows extending compacts with additional data for specifying conditions or parameters for a claim. The Compact protocol itself doesn't interpret the Mandate's content; this is the responsibility of the arbiter. However, The Compact uses the hash of the witness data and its reconstructed EIP-712 typestring to derive the final claim hash for validation.

Format: The witness is always a Mandate struct appended to the compact.

Compact(..., Mandate mandate)Mandate(uint256 myArg, bytes32 otherArg)

The witnessTypestring provided during a claim should be the arguments inside the Mandate struct (e.g., uint256 myArg,bytes32 otherArg), followed by any nested structs. Note that there are no assumptions made by the protocol about the shape of the Mandate or any nested structs within it.

Nested Structs: EIP-712 requires nested structs to be ordered alphanumerically after the top-level struct in the typestring. We recommend prefixing nested structs with "Mandate" (e.g., MandateCondition) to ensure correct ordering. Failure to do so will result in an invalid EIP-712 typestring.

For example, the correct witness typestring for Mandate(MandateCondition condition,uint256 arg)MandateCondition(bool flag,uint256 val) would be MandateCondition condition,uint256 arg)MandateCondition(bool flag,uint256 val (without a closing parenthesis).

☝️ Note the missing closing parenthesis in the above example. It will be added by the protocol during the dynamic typestring construction, so do not include the closing parenthesis in your witness typestring. This is crucial, otherwise the generated typestring will be invalid.

Registration

As an alternative to sponsors signing EIP-712 payloads, compacts can be registered directly on The Compact contract. This involves submitting a claimHash (derived from the intended compact details) and its typehash. This supports:

  • Sponsors without direct signing capabilities (e.g., DAOs, protocols).
  • Smart wallet / EIP-7702 enabled sponsors with alternative signature logic.
  • Chained deposit-and-register operations.

Registration can be done by the sponsor or a third party (if they provide the sponsor's signature for registerFor type functions, or if they are providing the deposited tokens). Registrations do not expire, and registered compacts cannot be unregistered by the sponsor. Registrations can be invalidated by the allocator consuming the nonce, or by letting them expire. Once a claim is processed for a compact its registration state is cleared.

The current registration status for a given claim can be queried via the ITheCompact.isRegistered function:

bool isRegistered = theCompact.isRegistered(sponsor, claimHash, typehash);

Claimant Processing & Structure

When an arbiter submits a claim, they provide an array of Component structs. Each Component specifies an amount and a claimant.

// Defined in src/types/Components.sol
struct Component {
    uint256 claimant; // The lockTag + recipient of the transfer or withdrawal.
    uint256 amount;   // The amount of tokens to transfer or withdraw.
}

The claimant field encodes both the recipient address (lower 160 bits) and a bytes12 lockTag (upper 96 bits): claimant = (lockTag << 160) | recipient.

This encoding determines how The Compact processes each component of the claim:

  1. Direct ERC6909 Transfer: If the encoded lockTag matches the lockTag of the resource lock being claimed, the amount of ERC6909 tokens is transferred directly to the recipient.
  2. Convert Between Resource Locks: If the encoded lockTag is non-zero and different from the claimed lock's tag, The Compact attempts to convert the claimed resource lock to a new one defined by the encoded lockTag for the recipient. This allows changing allocator, reset period, or scope.
  3. Withdraw Underlying Tokens: If the encoded lockTag is bytes12(0), The Compact attempts to withdraw the underlying tokens (native or ERC20) from the resource lock and send them to the recipient.

Withdrawal Fallback Mechanism: To prevent griefing (e.g., via malicious receive hooks during withdrawals, or relayed claims that intentionally underpay the necessary amount of gas), The Compact first attempts withdrawals with half the available gas. If this fails (and sufficient gas remains above a benchmarked stipend), it falls back to a direct ERC6909 transfer to the recipient. Stipends can be queried via getRequiredWithdrawalFallbackStipends. Benchmarking for these stipends is done via a call to __benchmark post-deployment, which meters cold account access and typical ERC20 and native transfers. This benchmark can be re-run by anyone at any time.

Forced Withdrawals

This mechanism provides sponsors recourse if an allocator becomes unresponsive or censors requests.

  1. Enable: Sponsor calls enableForcedWithdrawal(uint256 id).

  2. Wait: The resetPeriod for that resource lock must elapse.

  3. Withdraw: Sponsor calls forcedWithdrawal(uint256 id, address recipient, uint256 amount) to retrieve the underlying tokens.

The forced withdrawal state can be reversed with disableForcedWithdrawal(uint256 id).

Signature Verification

When a claim is submitted for a non-registered compact (i.e., one relying on a sponsor's signature), The Compact verifies the sponsor's authorization in the following order:

  1. Caller is Sponsor: If msg.sender == sponsor, authorization is granted.
  2. ECDSA Signature: Attempt standard ECDSA signature verification.
  3. EIP-1271 isValidSignature: If ECDSA fails, call isValidSignature on the sponsor's address (if it's a contract) with half the remaining gas.
  4. Emissary verifyClaim: If EIP-1271 fails or isn't applicable, and an emissary is assigned for the sponsor and lockTag, call the emissary's verifyClaim function.

Sponsors cannot unilaterally cancel a signed compact; only allocators can effectively do so by consuming the nonce. This is vital to upholding the equivocation guarantees for claimants.

Trust Assumptions

The Compact protocol operates under a specific trust model where different actors have varying levels of trust requirements:

Sponsor Trust Requirements:
  • Allocators: Sponsors must trust that allocators will not unduly censor valid requests against fully funded locks. However, sponsors retain the ability to initiate forced withdrawals if allocators become unresponsive.
  • Arbiters: Sponsors must trust that arbiters will not process claims where the specified conditions were not met. Arbiters have significant power in determining claim validity.
  • Emissaries: Sponsors must trust that emissaries (if assigned) will not authorize claims maliciously, as emissaries can act as fallback signers when other verification methods fail. Emissaries effectively 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 through double-spending or other allocation failures.
  • Arbiters: Claimants must trust that arbiters will not fail to process claims where conditions were properly met.
  • Emissaries: Claimants must trust that emissaries (if assigned) will faithfully authorize valid claims if the sponsor is able to equivocate, or update their account to revoke their authorization on a previously authorized compact (as is the case with EIP-7702 sponsors and many smart contracts implementing EIP-1271). Therefore, claimants should require the use of one of a small set of known, "canonical" emissaries that enforce delays before allowing key rotation.

Key Events

The Compact emits several events to signal important state changes:

  • Claim(address indexed sponsor, address indexed allocator, address indexed arbiter, bytes32 claimHash, uint256 nonce): Emitted when a claim is successfully processed via ITheCompactClaims functions.
  • NonceConsumedDirectly(address indexed allocator, uint256 nonce): Emitted when an allocator directly consumes a nonce via consume.
  • ForcedWithdrawalStatusUpdated(address indexed account, uint256 indexed id, bool activating, uint256 withdrawableAt): Emitted when enableForcedWithdrawal or disableForcedWithdrawal is called.
  • CompactRegistered(address indexed sponsor, bytes32 claimHash, bytes32 typehash): Emitted when a compact is registered via register, registerMultiple, or combined deposit-and-register functions.
  • AllocatorRegistered(uint96 allocatorId, address allocator): Emitted when a new allocator is registered via __registerAllocator.
  • EmissaryAssigned(address indexed sponsor, bytes12 indexed lockTag, address emissary): Emitted when a sponsor assigns or changes an emissary via assignEmissary.

Standard ERC6909.Transfer events are also emitted for mints, burns, and transfers of resource lock tokens.

Key Data Structures

Many functions in The Compact use custom structs for their calldata. Here are some of the most important ones:

  • For Claims (passed to ITheCompactClaims functions):
    • Claim: For claims involving a single resource lock on a single chain.
      // Defined in src/types/Claims.sol
      struct Claim {
          bytes allocatorData;
          bytes sponsorSignature;
          address sponsor;
          uint256 nonce;
          uint256 expires;
          bytes32 witness;
          string witnessTypestring;
          uint256 id;
          uint256 allocatedAmount;
          Component[] claimants;
      }
    • BatchClaim: For multiple resource locks on a single chain.
    • MultichainClaim: For single resource lock claims on the notarized (i.e., origin) chain of a multichain compact.
    • ExogenousMultichainClaim: For single resource lock claims on an exogenous chain (i.e., any chain other than the notarized chain).
    • BatchMultichainClaim: For multiple resource locks on the notarized chain.
    • ExogenousBatchMultichainClaim: For multiple resource locks on an exogenous chain.
    • BatchClaimComponent: Used within batch claim structs.
      // Defined in src/types/Components.sol
      struct BatchClaimComponent {
          uint256 id;
          uint256 allocatedAmount;
          Component[] portions;
      }
  • For Allocated Transfers (passed to ITheCompact.allocatedTransfer etc.):
    • AllocatedTransfer: For transferring a single ID to multiple recipients with allocator approval.
      // Defined in src/types/Claims.sol
      struct AllocatedTransfer {
          bytes allocatorData;
          uint256 nonce;
          uint256 expires;
          uint256 id;
          Component[] recipients;
      }
    • AllocatedBatchTransfer: For transferring multiple IDs.
  • For Deposits (used with Permit2):
    • DepositDetails: Helper for batch Permit2 deposits.