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:
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:
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
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.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 withabortDeadline > 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 asexecute). Requires all legs deposited, the compliance gate passing, andblock.timestamp < timeoutAt. The lastdepositLegalso flips toPreparedinline when the gate already passes, so an explicitpreparecall is only needed when attestations land after the final deposit. EmitsSettlementPrepared.executeFromRouter- the only path that pays out a remote-mode settlement. Callable only by the settlement’s snapshotted router, strictly beforeabortDeadline, 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 fromRegistered(another chain never funded) orPrepared(the coordinator decided abort). Not pausable, and a re-delivered abort can never touch anExecutedsettlement.
< 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 whileRegistered. 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 aRegisteredsettlement it opens attimeoutAt; for aPrepared(cross-chain) settlement it opens atabortDeadline. Each window is mutually exclusive by timestamp with the corresponding execution window.claimRefund- any depositor (or the operator), per leg, afterAbortedorTimedOut. Paysamount + additive feesto 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.
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 aforcedTransfer 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.
forcedTransferis 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.
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.