paralock-evm 0.1.1

EVM layer of Paralock: alloy clients for the ParalockHTLC and ParalockAdaptorEscrow escrows (lock, claim, refund, outcomes), escrow ids and claim digests, Tempo fee-token transactions, a verified network registry with the public testnet deployments, and a local deployment helper.
Documentation
//! Typed ABI bindings (`alloy::sol!`) for the Paralock contracts and the token interfaces the clients use.
//!
//! The interfaces are written out by hand so they carry documentation, and are checked against the compiled
//! artifacts (`artifacts/*.json`, the same files as `packages/evm/artifacts`) and the selectors in
//! `spec/vectors/evm.json` by the crate's tests: every function, event and error of the artifacts is here with the
//! same selector, and nothing else.
//!
//! The generated items follow `alloy::sol!` conventions: `ParalockHTLC::lockCall` encodes a call,
//! `ParalockHTLC::Locked` decodes an event, `ParalockHTLC::ParalockHTLCErrors` decodes any of the contract's custom
//! errors, and so on.
#![allow(missing_docs)] // helper types generated by `sol!` (call/return structs, selector enums) carry no docs
#![allow(clippy::too_many_arguments)] // the generated `Locked` event constructor takes one argument per field

use alloy::sol;

sol! {
    /// `ParalockHTLC`: hash-time-locked escrow (htlc mode). See `contracts/src/ParalockHTLC.sol`.
    ///
    /// Storage is one slot per hashlock: `0` (never used), `keccak256(abi.encode(swap))` while active, or `SETTLED`
    /// (`0x…01`) forever after a claim or refund.
    #[derive(Debug, PartialEq, Eq)]
    interface ParalockHTLC {
        /// Every parameter of a swap. Its keccak256 commitment is all the contract stores.
        struct Swap {
            /// sha256 of the 32-byte secret; unique key of the swap.
            bytes32 hashlock;
            /// TIP-20 (or ERC-20) token held in escrow.
            address token;
            /// Party that locks the funds and is refunded after expiry.
            address sender;
            /// Party paid on claim.
            address recipient;
            /// Token amount in base units.
            uint256 amount;
            /// Unix timestamp (seconds) from which the swap can no longer be claimed, only refunded.
            uint64 expiry;
        }

        /// Funds were locked. Carries every `Swap` field so the struct can be rebuilt from the log alone.
        event Locked(
            bytes32 indexed hashlock,
            address indexed sender,
            address indexed recipient,
            address token,
            uint256 amount,
            uint64 expiry
        );
        /// The swap was claimed and the recipient paid. `preimage` unlocks the Zcash leg.
        event Claimed(bytes32 indexed hashlock, bytes32 preimage);
        /// The swap expired and the sender was refunded.
        event Refunded(bytes32 indexed hashlock);

        /// `msg.sender` is not the swap's sender.
        error NotSender();
        /// The hashlock is zero.
        error InvalidHashlock();
        /// The amount is zero.
        error InvalidAmount();
        /// The recipient is the zero address.
        error InvalidRecipient();
        /// The expiry is not in the future.
        error InvalidExpiry();
        /// The hashlock was already used by an active or settled swap.
        error HashlockUsed(bytes32 hashlock);
        /// No swap was ever locked under this hashlock.
        error SwapNotFound(bytes32 hashlock);
        /// The swap under this hashlock was already claimed or refunded.
        error SwapSettled(bytes32 hashlock);
        /// The provided swap parameters do not match the locked swap.
        error SwapMismatch(bytes32 hashlock);
        /// sha256(preimage) is not the swap's hashlock.
        error InvalidPreimage();
        /// Claim attempted at or after expiry.
        error SwapExpired(uint64 expiry);
        /// Refund attempted before expiry.
        error SwapNotExpired(uint64 expiry);
        /// The token call failed without a reason, or returned false.
        error TokenTransferFailed(address token);
        /// The escrow received a different amount than requested (fee-on-transfer or rebasing token).
        error UnsupportedToken(address token);

        /// Lock `s.amount` of `s.token` under `s.hashlock` (caller must be `s.sender` and have approved the escrow).
        function lock(Swap calldata s) external;
        /// Pay `s.recipient` by revealing the secret. Callable by anyone before `s.expiry`.
        function claim(Swap calldata s, bytes32 preimage) external;
        /// Return the funds to `s.sender`. Callable by anyone from `s.expiry` on.
        function refund(Swap calldata s) external;
        /// Raw state of a hashlock: 0 if unused, the swap commitment if active, `SETTLED` once claimed/refunded.
        function stateOf(bytes32 hashlock) external view returns (bytes32);
        /// Lifecycle status of a hashlock (`Status` enum: 0 empty, 1 active, 2 settled).
        function statusOf(bytes32 hashlock) external view returns (uint8);
        /// True if a swap is locked under `hashlock` and not yet claimed or refunded.
        function isActive(bytes32 hashlock) external view returns (bool);
        /// The value stored for an active swap: keccak256 of its ABI encoding.
        function commitment(Swap calldata s) external pure returns (bytes32);
        /// State value of a hashlock whose swap was claimed or refunded.
        function SETTLED() external view returns (bytes32);
    }
}

sol! {
    /// `ParalockAdaptorEscrow`: signature-locked escrow (adaptor mode). See `contracts/src/ParalockAdaptorEscrow.sol`.
    ///
    /// Released by a raw ECDSA signature from the swap's one-time `signer` over `claimDigest(id)`; storage is
    /// `id = keccak256(abi.encode(swap)) => Status` (0 empty, 1 active, 2 settled).
    #[derive(Debug, PartialEq, Eq)]
    interface ParalockAdaptorEscrow {
        /// Every parameter of a swap. Its keccak256 hash is the swap id.
        struct Swap {
            /// TIP-20 (or ERC-20) token held in escrow.
            address token;
            /// Party that locks the funds and is refunded after expiry.
            address sender;
            /// Party paid on claim.
            address recipient;
            /// Address whose signature over `claimDigest(id)` releases the funds.
            address signer;
            /// Token amount in base units.
            uint256 amount;
            /// Unix timestamp (seconds) from which the swap can no longer be claimed, only refunded.
            uint64 expiry;
            /// Random value that makes the id unique and unlinkable across swaps.
            bytes32 salt;
        }

        /// Funds were locked. Carries every `Swap` field so the struct can be rebuilt from the log alone.
        event Locked(
            bytes32 indexed id,
            address indexed sender,
            address indexed recipient,
            address token,
            address signer,
            uint256 amount,
            uint64 expiry,
            bytes32 salt
        );
        /// The swap was claimed and the recipient paid. `(r, s)` is the completed adaptor signature.
        event Claimed(bytes32 indexed id, uint8 v, bytes32 r, bytes32 s);
        /// The swap expired and the sender was refunded.
        event Refunded(bytes32 indexed id);

        /// `msg.sender` is not the swap's sender.
        error NotSender();
        /// The amount is zero.
        error InvalidAmount();
        /// The recipient is the zero address.
        error InvalidRecipient();
        /// The signer is the zero address.
        error InvalidSigner();
        /// The expiry is not in the future.
        error InvalidExpiry();
        /// A swap with this id was already locked (it is active or settled).
        error SwapExists(bytes32 id);
        /// No swap was ever locked under this id (unknown or tampered parameters).
        error SwapNotFound(bytes32 id);
        /// The swap was already claimed or refunded.
        error SwapSettled(bytes32 id);
        /// Claim attempted at or after expiry.
        error SwapExpired(uint64 expiry);
        /// Refund attempted before expiry.
        error SwapNotExpired(uint64 expiry);
        /// The signature does not recover to the swap's signer.
        error InvalidSignature();
        /// The signature is not canonical: `s` is in the upper half of the curve order, or `v` is not 27/28.
        error MalleableSignature();
        /// The token call failed without a reason, or returned false.
        error TokenTransferFailed(address token);
        /// The escrow received a different amount than requested (fee-on-transfer or rebasing token).
        error UnsupportedToken(address token);

        /// Lock `s.amount` of `s.token` for `s.recipient`; returns the swap id.
        function lock(Swap calldata s) external returns (bytes32 id);
        /// Pay `s.recipient` with `s.signer`'s signature over `claimDigest(id)`. Callable by anyone before expiry.
        function claim(Swap calldata s, uint8 v, bytes32 r, bytes32 sigS) external;
        /// Return the funds to `s.sender`. Callable by anyone from `s.expiry` on.
        function refund(Swap calldata s) external;
        /// The 32-byte digest `s.signer` signs (raw ECDSA, no prefix) to release swap `id`.
        function claimDigest(bytes32 id) external view returns (bytes32);
        /// The id of a swap: keccak256 of its ABI encoding.
        function idOf(Swap calldata s) external pure returns (bytes32);
        /// Lifecycle status of a swap id (`Status` enum: 0 empty, 1 active, 2 settled).
        function statusOf(bytes32 id) external view returns (uint8);
        /// True if `id` is locked and not yet claimed or refunded.
        function isActive(bytes32 id) external view returns (bool);
        /// Domain tag of the claim digest: `keccak256("ParalockAdaptorClaim")`.
        function CLAIM_TYPEHASH() external view returns (bytes32);
    }
}

sol! {
    /// `MockUSD`: the 6-decimal "USDC" stand-in for local devnets (only the deployer can mint).
    #[derive(Debug, PartialEq, Eq)]
    interface MockUSD {
        /// Tokens moved.
        event Transfer(address indexed from, address indexed to, uint256 value);
        /// An allowance was set.
        event Approval(address indexed owner, address indexed spender, uint256 value);

        /// Only the deployer may mint.
        error NotMinter();
        /// The sender's balance is too low.
        error InsufficientBalance();
        /// The spender's allowance is too low.
        error InsufficientAllowance();
        /// Transfer or mint to the zero address.
        error ZeroAddress();

        /// Token name.
        function name() external view returns (string);
        /// Token symbol (`USDC`).
        function symbol() external view returns (string);
        /// Token decimals (6).
        function decimals() external view returns (uint8);
        /// The only account allowed to mint (the deployer).
        function minter() external view returns (address);
        /// Total minted.
        function totalSupply() external view returns (uint256);
        /// Balance of an account.
        function balanceOf(address account) external view returns (uint256);
        /// Allowance of `spender` over `owner`'s tokens.
        function allowance(address owner, address spender) external view returns (uint256);
        /// Mint `amount` to `to` (deployer only).
        function mint(address to, uint256 amount) external;
        /// Set `spender`'s allowance.
        function approve(address spender, uint256 amount) external returns (bool);
        /// Transfer tokens.
        function transfer(address to, uint256 amount) external returns (bool);
        /// Transfer tokens on behalf of `from`.
        function transferFrom(address from, address to, uint256 amount) external returns (bool);
    }
}

sol! {
    /// The ERC-20 surface the clients use (TIP-20 tokens implement it too).
    #[derive(Debug, PartialEq, Eq)]
    interface IERC20 {
        /// Balance of an account.
        function balanceOf(address account) external view returns (uint256);
        /// Allowance of `spender` over `owner`'s tokens.
        function allowance(address owner, address spender) external view returns (uint256);
        /// Set `spender`'s allowance to exactly `amount`.
        function approve(address spender, uint256 amount) external returns (bool);
        /// Token decimals.
        function decimals() external view returns (uint8);
        /// Token symbol.
        function symbol() external view returns (string);
    }
}

sol! {
    /// Tempo's TIP-20 factory precompile (`0x20Fc…0000`), as used by the local deployment helper.
    #[derive(Debug, PartialEq, Eq)]
    interface ITIP20Factory {
        /// The factory refused an address in the reserved range.
        error AddressReserved();
        /// The address is not in the reserved range.
        error AddressNotReserved();
        /// The quote token is not a valid TIP-20.
        error InvalidQuoteToken();
        /// A token already exists at the derived address.
        error TokenAlreadyExists(address token);

        /// Create a TIP-20 at an address derived from `(msg.sender, salt)`.
        function createToken(
            string name,
            string symbol,
            string currency,
            address quoteToken,
            address admin,
            bytes32 salt
        ) external returns (address);
        /// True if `token` is a TIP-20.
        function isTIP20(address token) external view returns (bool);
        /// The address `createToken` would use for `(sender, salt)`.
        function getTokenAddress(address sender, bytes32 salt) external pure returns (address);
    }
}

sol! {
    /// The TIP-20 issuer surface used by the local deployment helper (roles and minting).
    #[derive(Debug, PartialEq, Eq)]
    interface ITIP20Issuer {
        /// The caller lacks the required role.
        error Unauthorized();

        /// The role allowed to mint.
        function ISSUER_ROLE() external view returns (bytes32);
        /// Grant `role` to `account` (admin only).
        function grantRole(bytes32 role, address account) external;
        /// Mint `amount` to `to` (issuer only).
        function mint(address to, uint256 amount) external;
    }
}

sol! {
    /// The TIP-20 custom errors a token call bubbled up through an escrow can revert with (Tempo's `ITIP20`).
    #[derive(Debug, PartialEq, Eq)]
    interface ITIP20Errors {
        /// Balance too low.
        error InsufficientBalance(uint256 available, uint256 required, address token);
        /// Allowance too low.
        error InsufficientAllowance();
        /// Minting would exceed the supply cap.
        error SupplyCapExceeded();
        /// Invalid supply cap.
        error InvalidSupplyCap();
        /// Invalid payload.
        error InvalidPayload();
        /// The token's transfer policy (TIP-403) forbids the transfer.
        error PolicyForbids();
        /// Invalid recipient.
        error InvalidRecipient();
        /// The token is paused.
        error ContractPaused();
        /// Invalid currency.
        error InvalidCurrency();
        /// Invalid quote token.
        error InvalidQuoteToken();
        /// Invalid amount.
        error InvalidAmount();
        /// No opted-in supply.
        error NoOptedInSupply();
        /// The caller lacks the required role.
        error Unauthorized();
        /// The address is protected.
        error ProtectedAddress();
        /// Not a valid token.
        error InvalidToken();
        /// The token is not initialized.
        error Uninitialized();
        /// Invalid transfer policy id.
        error InvalidTransferPolicyId();
    }
}

sol! {
    /// Tempo's fee manager precompile (`0xfeEC…0000`): an account's preferred fee token for plain (non-`0x76`)
    /// transactions.
    #[derive(Debug, PartialEq, Eq)]
    interface ITipFeeManager {
        /// The account's preferred fee token (zero if none is set).
        function userTokens(address user) external view returns (address);
        /// Set the caller's preferred fee token.
        function setUserToken(address token) external;
    }
}