Skip to main content
KeystoneSettlement replaces the earlier SettlementCoordinator + KeystoneEscrow pair with one contract that owns the full on-chain lifecycle. The lifecycle is fixed in code - every settlement runs the same audited machine:
There are no on-chain state graphs, no gates configuration, and no transition transactions. KeyStone’s backend writes exactly two things here: registerSettlement and (operator-only) abort. setRouter is a deploy-time admin step, not part of settlement flow.

Lifecycle

Registration

abortDeadline selects the settlement mode. 0 is single-chain: the settlement executes autonomously on the last deposit, exactly as before. A non-zero value opts into the cross-chain Prepared phase (see Remote mode below): the last deposit flips the settlement to Prepared and the KeystoneRouter drives release or abort. Registering in cross-chain mode requires a configured router; the router address is snapshotted into the settlement at registration, so re-pointing the live router later can never redirect an in-flight settlement. Each LegInput binds everything permanently:
Validation at registration: 1-50 legs, 1-50 non-zero party hashes, future timeout, non-zero tokens/recipients/keys/amounts, deductive fees never exceeding the leg amount, and an isAgent check for ERC-3643 legs (so agent-gated payouts cannot strand funds later). Re-registering an existing id reverts SettlementAlreadyExists.

Deposits: wallet-bound commitment keys

The contract verifies keccak256(abi.encode(msg.sender, depositSecret)) == leg.depositKey - the key commits to BOTH the depositor address and the secret, so a leaked secret is useless from any other wallet. The deposit pulls amount + additive fees via transferFrom and records the depositor for refunds. Deposits are accepted strictly before timeoutAt. Duplicate deposits revert LegAlreadyDeposited; a wrong secret or wrong wallet reverts InvalidDepositKey.

Autonomous execution

If the last outstanding deposit lands while the compliance gate already passes, the settlement executes inline in the same transaction - every leg pays out to its bound recipient, fees transfer, all-or-nothing.
If attestations were still pending at the last deposit, the settlement stays Registered (a closed gate never reverts a deposit) and once the gate clears execute() can be called by the operator or any depositor: every recipient and fee was bound at registration, so the caller only triggers the pre-committed plan, never steers it. (Unrelated third parties are excluded - this is permissioned institutional settlement - but every party with funds at stake can always drive execution itself.) The gate: ComplianceRegistry.areAllPartiesCleared(settlementId, partyHashes) - every party hash bound at registration must be attested Pass.

Remote mode: the Prepared phase

A settlement registered with abortDeadline > 0 never auto-executes. Instead, when every leg is funded and the compliance gate passes, it flips to Prepared - the chain’s promise to hold funds while the KeystoneRouter collects confirmations from every participating chain and lands a release or abort decision.
  • prepare - operator-or-participant (the same gate as execute). Requires all legs deposited, the compliance gate passing, and block.timestamp < timeoutAt. The last depositLeg also flips to Prepared inline when the gate already passes, so an explicit prepare call is only needed when attestations land after the final deposit. Emits SettlementPrepared.
  • executeFromRouter - the only path that pays out a remote-mode settlement. Callable only by the settlement’s snapshotted router, strictly before abortDeadline, and the compliance gate is re-checked at release time - a settlement whose compliance has since failed reverts instead of paying out.
  • abortFromRouter - router-only abort, accepted from Registered (another chain never funded) or Prepared (the coordinator decided abort). Not pausable, and a re-delivered abort can never touch an Executed settlement.
The release window (< abortDeadline) and the Prepared-phase timeout window (>= abortDeadline, claimTimeout by the operator or any depositor) are disjoint by timestamp: at no instant are both release and timeout live, so a cross-chain settlement can never both pay out and refund.

Recovery: abort, timeout, pull refunds

  • abort - operator-only, allowed only while Registered. The race-free runbook against a pending auto-execution is pause first (pause blocks deposits and execution; abort stays pause-exempt), then abort.
  • claimTimeout - operator or any depositor. For a Registered settlement it opens at timeoutAt; for a Prepared (cross-chain) settlement it opens at abortDeadline. Each window is mutually exclusive by timestamp with the corresponding execution window.
  • claimRefund - any depositor (or the operator), per leg, after Aborted or TimedOut. Pays amount + additive fees to the recorded depositor, never an alternate address. Per-leg pull payment means one un-receivable depositor (e.g. a USDC blacklist) can never block other legs’ refunds.
  • refundViaTransfer - operator escape hatch for ERC-3643 legs whose agent role was revoked after deposit; identical guards, standard transfer path.
None of the recovery functions are pausable - a pause must never trap funds past their deadline.

Events

KeyStone’s backend maps SettlementExecuted / SettlementAborted / SettlementTimedOut to the settlement states SETTLED / ROLLED_BACK / TIMED_OUT by event name - see State Machine.

Reads

Roles and pause

Pause blocks registration, deposits, and execution - never abort, timeout, or refunds.

Token support

ERC-20 and ERC-3643 (T-REX security tokens). Fee-on-transfer and rebasing tokens are not supported: the contract pulls and pays exact recorded amounts, and a token that changes the amount in transit would leave a leg short.

ERC-3643 requires agent status, and what that means

A permissioned security token restricts transfers to addresses its own compliance rules allow, and exposes a forcedTransfer function callable only by addresses the token issuer has designated as agents. KeystoneSettlement uses forcedTransfer for every outbound ERC-3643 movement: the payout to each leg recipient and fee recipient, and the standard refund back to a depositor. Deposits are different. depositLeg pulls every leg, ERC-3643 included, through the token’s ordinary transferFrom. Two things follow for an integrator: the depositing wallet must approve the settlement contract for the leg amount plus any additive fee, and the deposit runs the token’s own compliance modules, so that wallet must be permitted to transfer into the settlement contract under the token’s rules. The settlement contract must therefore be an agent of every ERC-3643 token it settles. This is not optional and not deferred. registerSettlement performs a live isAgent(address(this)) check for each ERC-3643 leg and reverts NotTokenAgent if it fails, so a settlement whose payout could later be blocked by missing agent rights cannot be created in the first place. Two consequences worth stating plainly for anyone assessing the contract’s position:
  • Agent status is granted by the token issuer, not claimed by KeyStone. The issuer designates agents on its own token. A settlement contract with no such designation cannot register a leg in that token at all.
  • Agent status is a standing capability, and the contract exposes no way to use it beyond settlement. forcedTransfer is reachable only from the payout and refund paths, each of which sends to an address recorded at registration: a leg’s recipient, a fee recipient bound at registration, or a leg’s recorded depositor. There is no function that transfers a permissioned token to an arbitrary address, and the contract cannot be upgraded to add one.
If the issuer revokes agent status after a deposit, forcedTransfer starts reverting and the ordinary refund path breaks. refundViaTransfer exists for exactly that case: it refunds the same leg to the same recorded depositor using the token’s standard transfer path instead, which re-runs the token’s own compliance modules. It applies to ERC-3643 legs only, reverting NotErc3643Leg otherwise, and it cannot change the destination.