# The settlement validator (/developers/onchain/settlement-validator)



A route is **mixed** when some legs settle atomically inside the user's own transaction and
some legs are handed to a permissioned batcher that fills an order UTxO later.

Settled leg by leg, such a route has no aggregate guarantee. The Kernel legs land, a
Commitment leg is never filled, and the user ends up below the direct route they were quoted.

This validator holds the route proceeds and releases them only when the user is paid the whole
committed aggregate. That turns "some legs fill and some do not" into "the route fills, or the
user takes everything back after expiry".

## The build [#the-build]

| Item             | Value                                                      |
| ---------------- | ---------------------------------------------------------- |
| Source           | `onchain/validators/settlement.ak`                         |
| Aiken            | v1.1.23                                                    |
| Plutus version   | V3                                                         |
| Standard library | `aiken-lang/stdlib` v3.1.0                                 |
| Property testing | `aiken-lang/fuzz` v2.2.0                                   |
| Compiled size    | 1384 bytes                                                 |
| Script hash      | `12edd011492ecc8953bb5b559b1c25509d5a59477b67d146bef82bf2` |

The blueprint is `onchain/plutus.json`. `cardano-swap deploy show --network <network>` builds
it and prints the hash, the size and the address it would deploy to.

## Where it is deployed [#where-it-is-deployed]

On Cardano preprod:

| Item                   | Value                                                                |
| ---------------------- | -------------------------------------------------------------------- |
| Address                | `addr_test1wqfwm5q3fyhvez2nhdd4txcuy4gf6kjegaak052xhmuzhus9c702t`    |
| Reference script       | `8e066c7dde25ef4b7158e1995e841fbc2a4ee8ee5061f79a55bef23396eb75c0#0` |
| Deployment transaction | `8e066c7dde25ef4b7158e1995e841fbc2a4ee8ee5061f79a55bef23396eb75c0`   |
| Expiry                 | 3600 slots                                                           |

`cardano-swap deploy settlement --network <network> --submit` publishes it and prints the
values to write into that network's configuration. Where a network has not been deployed to,
the configuration holds `null` and reading it throws naming the key and the network, rather
than borrowing another network's value.

## The datum [#the-datum]

```rust
pub type SettlementDatum {
  /// Payment key hash of the user. Only this key can be paid on a claim
  /// and only this key can be refunded after expiry.
  owner: VerificationKeyHash,
  /// The asset the user must be paid in.
  output_asset: AssetClass,
  /// The aggregate the user must receive across the whole transaction.
  /// This is the only amount the validator enforces.
  minimum_output: Int,
  /// The direct-route quote recorded when the route was built.
  /// Deliberately not enforced.
  baseline_output: Int,
  /// POSIX time in milliseconds. The refund path opens at this point.
  expiry_slot: Int,
  /// Leg count of the route, recorded for the same receipt reason.
  expected_legs: Int,
}
```

`baseline_output` and `expected_legs` are carried so the saving claim can be recomputed from
chain data alone, without trusting the quoting service that made the claim in the first place.
The validator does not enforce either.

<Callout type="warn">
  `expiry_slot` holds POSIX time in **milliseconds** despite its name. The validator compares it
  against the script context's lower bound, which the ledger reports in milliseconds. The
  off-chain type names the field `expiryMs` so the unit cannot be misread off the type.
</Callout>

The redeemer is a bare enum, so `Claim` is constructor 0 and `Refund` is constructor 1, both
with no fields.

## The spend path [#the-spend-path]

```rust
validator settlement {
  spend(datum, redeemer, own_ref, self) {
    expect Some(config) = datum
    let own_input = resolve_input(self.inputs, own_ref)

    // exactly one input at this script's payment credential
    expect [_] = list.filter(self.inputs, fn(input) {
      input.output.address.payment_credential == own_input.address.payment_credential
    })

    when redeemer is {
      Claim ->
        paid_to_owner(self.outputs, config.owner, config.output_asset)
          >= config.minimum_output
      Refund -> and {
        starts_at_or_after(self.validity_range, config.expiry_slot),
        fully_returned(own_input.value, returned_to_owner(self.outputs, config.owner)),
      }
    }
  }

  else(_) { fail }
}
```

### Claim [#claim]

The user must be paid at least `minimum_output` of `output_asset` across the transaction.

The commitment is on the **aggregate**, not on any single output. A batcher and a wallet both
split payment over several outputs for reasons that have nothing to do with the route, so
`paid_to_owner` sums the asset across every output whose payment credential is the owner's key
hash. Summing is what makes the guarantee survive that.

### Refund [#refund]

Two conditions, both required.

**The transaction cannot be valid before the expiry.** `starts_at_or_after` reads the validity
range's lower bound and requires it to be at or after `expiry_slot`. A transaction with an
unbounded lower bound proves nothing about the current time and is rejected outright.

**Everything the settlement UTxO held comes back, ADA included.** `fully_returned` walks every
asset in the held value and requires at least that quantity in the merged value paid to the
owner.

Because ADA is included, the refunding transaction has to bring its own fee input. That is the
user's own transaction, so the cost lands where the choice is made, and it keeps the check free
of a fee tolerance that would otherwise be a slow leak.

## The double satisfaction guard [#the-double-satisfaction-guard]

Both redeemers measure value by reading the transaction outputs that pay the owner. Two
settlement UTxOs spent together would each read the same outputs and each conclude, correctly
on its own terms, that it was satisfied. Two UTxOs each owed 100 would both be released by a
single output of 100, and the spender keeps 100.

The fix taken here is a restriction rather than tagging: &#x2A;*a transaction may spend exactly one
input carrying this script's payment credential.**

With a single claimant there is no shared measurement left to double count, and no per-output
bookkeeping is needed, which keeps the reference script small.

The cost is that a user holding two settlement UTxOs claims them in two transactions.
Settlement UTxOs are one per route and short lived, so that cost is rare.

The credential is read from the resolved own input rather than from a parameter, so the
validator stays unparameterised and has one hash.

## What it covers, and what already has a guarantee [#what-it-covers-and-what-already-has-a-guarantee]

A pure Kernel route is already atomic and needs no validator from this project. A pure single
venue Commitment route is already guarded by that venue's own minimum. The gap is the mixed
route, and that is what this validator is for.

Its guarantee, stated exactly: &#x2A;*never worse than the quoted minimum, or recover the pieces.**

If one leg fills and another does not, the refund after expiry returns a mix of input and
output assets rather than the original input, because the filled leg cannot be reversed. That
is why the router prefers a single class route whenever its net output is within
`routing.singleClassPreferenceBps` of a mixed one. A slightly worse price with a clean
guarantee is the better product.

## Tests [#tests]

The validator's own tests are in the same file, built by hand from `transaction.placeholder` so
each test states only the one fact it is about. `packages/tx/src/__tests__/settlement.test.ts`
exercises the off-chain builders against it.

Both paths are confirmed on preprod, each against a twin differing in exactly one variable:

* A claim paying the committed minimum is accepted, and its twin paying one base unit less is
  refused.
* A claim on an under funded commitment is refused.
* A refund one slot before expiry is refused.
* A double satisfaction attempt is refused.

## Related [#related]

<Cards>
  <Card title="Execution classes" href="/developers/concepts/execution-classes" />

  <Card title="Building transactions" href="/developers/internals/transaction-building" />

  <Card title="Guarantees and security" href="/developers/security" />
</Cards>
