> For the complete documentation index, see [llms.txt](https://docs.trilobyte.finance/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.trilobyte.finance/technical-documentation/collateral-escrow-contract.md).

# Collateral Escrow Contract

A generic, shared escrow that custodies **borrower-posted collateral** against a loan. It is one of Trilobyte's two collateral systems — the other (pool-manager skin-in-the-game collateral, which anchors the junior tranche) lives in [Globals](/technical-documentation/globals-contract.md).

**Source:** `contracts/collateral-escrow/`

## Overview

The escrow custodies pledged collateral of **any** asset that implements the token interface — fungible tokens, RWA certificates via the Stellar Asset Contract, or NFTs. Its only job is `lock` / `release` / `seize`. Valuation, LTV, and what "seize" means by collateral type are policy that lives in configuration around the escrow, not in the contract. It is a single shared contract across every loan, not one per vault.

**Vault-triggered.** Each pledge names its own `controller` — the Trilobyte vault the loan funds against — and **only that controller may release or seize it**. A single shared escrow can therefore back many loans and be authorised once by a permissioned-cert issuer.

**Per-pledge accounting.** Pledges are identified purely by their `pledge_id`, with **no per-token uniqueness constraint**.

{% hint style="danger" %}
**Fixed: the one-pledge-per-token griefing vector.** An earlier design allowed at most one active pledge per token. That let anyone grief the escrow by locking a dust amount of a token against an arbitrary controller — since only that controller could ever clear the slot, and it never would (the dust pledge doesn't match its loan), the token became permanently unusable for every future borrower. Keying purely by `pledge_id` removes that shared choke point: a borrower's own pledge is unaffected by anyone else's.
{% endhint %}

**History kept, never pruned.** `Pledge.state` (`Active` / `Released` / `Seized`) records the outcome instead of deleting the record, so a resolved pledge remains queryable.

## Source Modules

| File          | Purpose                                                                                                             |
| ------------- | ------------------------------------------------------------------------------------------------------------------- |
| `contract.rs` | `__constructor`, `lock`, `release`, `seize`, queries, the `available()` clamp helper, `Upgradeable` impl            |
| `storage.rs`  | `Pledge` / `PledgeState`, pledge counter, per-id pledge records                                                     |
| `errors.rs`   | `#[contracterror]` enum (5 variants: `1,2,4,5,6`)                                                                   |
| `events.rs`   | `PledgeLocked`, `PledgeReleased`, `PledgeSeized`                                                                    |
| `test.rs`     | 13 tests covering lock/release/seize, the griefing regression, clawback robustness, and controller-auth enforcement |

## Data Model

### Pledge

```rust
Pledge {
    borrower: Address,        // Who pledged the collateral
    token: Address,           // The pledged asset
    amount: i128,             // Recorded pledged amount
    recovery_party: Address,  // Receives the asset on seize (default)
    controller: Address,      // The vault allowed to release/seize this pledge
    state: PledgeState,       // Active | Released | Seized
}
```

The record is kept for the pledge's whole lifetime — check `state` to see whether it's still active.

## Functions

| Function               | Signature                                                                         | Auth                                                | Description                                                                                                                                                                                                                                                                                           |
| ---------------------- | --------------------------------------------------------------------------------- | --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `lock`                 | `lock(borrower, token, amount, recovery_party, controller) -> Result<u64, Error>` | `borrower`                                          | Pledges `amount` of `token`, naming the `controller` vault and the `recovery_party`. Verifies the transfer against the escrow's own balance delta — a fee-on-transfer or short-delivering token is rejected with `TransferShortfall` rather than recording an inflated amount. Returns the pledge id. |
| `release`              | `release(pledge_id) -> Result<(), Error>`                                         | pledge `controller`                                 | Return the collateral to the borrower (loan repaid). Marks the pledge `Released` before the token transfer (effects-before-interaction). No-op transfer (but still authorised/checked) if already resolved is rejected with `PledgeNotActive`.                                                        |
| `seize`                | `seize(pledge_id, to) -> Result<(), Error>`                                       | pledge `controller`                                 | Send the collateral to `to` — supplied by the controller at call time, not always the pledge's stored `recovery_party`, since the controller independently validated the recovery party it trusts at disbursement. Marks `Seized` before the transfer.                                                |
| `get_pledge`           | `get_pledge(pledge_id) -> Option<Pledge>`                                         | —                                                   | The pledge record, present for its whole lifetime.                                                                                                                                                                                                                                                    |
| `available_collateral` | `available_collateral(pledge_id) -> i128`                                         | —                                                   | Currently transferable amount: the recorded amount clamped to the escrow's live balance, and 0 once the pledge is no longer Active. Returns 0 if the pledge does not exist.                                                                                                                           |
| `upgrade`              | `upgrade(new_wasm_hash, operator)`                                                | Live Globals admin (or the timelock, once required) | Reads Globals' admin/timelock live — never decodes `GlobalSettings` — see the [Upgrade-path rule](/technical-documentation/deployment.md).                                                                                                                                                            |
| `__constructor`        | `__constructor(globals)`                                                          | —                                                   | Deploy the shared escrow, recording the Globals address so `upgrade` can require its live admin's auth. No other global authority — each pledge still carries its own controller.                                                                                                                     |

{% hint style="warning" %}
`available_collateral` exists because a permissioned RWA certificate can be **clawed back by its issuer while escrowed**. A controller should gate lending on the live `available_collateral`, not the recorded `amount`. The vault's disbursement gate does exactly this. If `seize` finds `amount == 0` (fully clawed back), the pledge is still closed and the event records `amount = 0`.
{% endhint %}

## How the Vault Uses It

1. **Setup** — the loan is created with `collateral_escrow`, `collateral_token`, `collateral_recovery`, and `collateral_min` set on `VaultParams`.
2. **Lock** — the borrower calls `lock(...)` naming the vault as `controller`.
3. **Record** — anyone calls the vault's `record_collateral_pledge(pledge_id)`, once, to link the pledge to the vault ahead of disbursement.
4. **Disbursement gate** — `approve_and_disburse` verifies the pledge backs *this* vault: `controller == vault`, `token == collateral_token`, `borrower == loan borrower`, and both `pledge.amount` and the **live** `available_collateral` are at least `principal × min_borrower_coll_ratio_bps`. Any mismatch → `CollateralPledgeInvalid`.
5. **Repaid** — on `FullyRepaid` the vault calls `escrow.release(pledge_id)` → collateral back to the borrower.
6. **Defaulted** — on `check_default` the vault calls `escrow.seize(pledge_id, recovery_party)` → collateral to the recovery party, ahead of manager collateral in the recovery order.
7. **Cancelled before disbursement** — on funding/approval expiry the vault releases the pledge back to the borrower.

## Events

| Event            | Key Fields                                               | When                         |
| ---------------- | -------------------------------------------------------- | ---------------------------- |
| `PledgeLocked`   | `borrower`, `pledge_id`, `token`, `amount`, `controller` | Collateral locked            |
| `PledgeReleased` | `pledge_id`, `amount`                                    | Released to the borrower     |
| `PledgeSeized`   | `to`, `pledge_id`, `amount`                              | Seized to the recovery party |

## Error Codes

| Code | Name                | Description                                                                                    |
| ---- | ------------------- | ---------------------------------------------------------------------------------------------- |
| 1    | `PledgeNotFound`    | No pledge with that id                                                                         |
| 2    | `InvalidAmount`     | The pledged amount must be positive                                                            |
| 4    | `PledgeNotActive`   | The pledge has already been resolved (released or seized)                                      |
| 5    | `TransferShortfall` | The transfer into the escrow delivered less than requested                                     |
| 6    | `Unauthorized`      | `upgrade` called by someone other than the live Globals admin (or the timelock, once required) |

{% hint style="info" %}
Code 3 is intentionally unused (`TokenAlreadyPledged`, the old one-pledge-per-token error, was removed along with the constraint it guarded).
{% endhint %}

## Storage Design

The pledge counter and per-id pledge records are in **persistent storage** with a TTL clamped to the network maximum (`max_ttl()`). Reads (`get_pledge`) do not bump TTL — reads must not side-effect storage.

*As of September 2026.*
