> For the complete documentation index, see [llms.txt](https://docs.usefarlight.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.usefarlight.com/architecture/smart-contracts.md).

# The contract set

The full Farlight contract set on Robinhood Chain, contract by contract, with the functions, events and message types that integrators build against.

All on-chain code sits in `contracts/` inside the repository, a Foundry project compiled for Robinhood Chain (chain ID 4663). What follows describes the contracts exactly as they behave in production. For build instructions, the test suite and the deployed addresses, go to the [Deployment](/architecture/deployment.md) page.

{% hint style="info" %}
The contracts are written in Solidity 0.8.26 for the Cancun EVM, compiled through the IR pipeline, and build on OpenZeppelin 5.2. Source code for every deployment is verified on Blockscout. For the test suite, the audit record and the operational controls, see [Verification, audits and safeguards](/architecture/security.md).
{% endhint %}

## Four design decisions

* **An immutable core.** There is no proxy and no admin role on `LoanDesk`, `IntentBook`, `SliceToken`, `CollateralAuction` or `RolloverAuction`. When the protocol improves, a new deployment goes out, and any loan opened on the previous one finishes its term there.
* **Parameters live away from code.** Every value that governance can adjust is stored in `PolicyController`, and the controller sits behind a timelock.
* **Markets are isolated.** A loan is keyed by its collateral token and its loan token together. Tier, exposure cap and oracle configuration are all resolved per token, and no state crosses from one market into another.
* **Written for an Arbitrum-family chain.** Time is `block.timestamp` throughout, calldata stays small, and every function remains reachable through the L1 delayed inbox.

## Inventory

| Contract            | Responsible for                                                                                                         | Mutability                                                      |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| `LoanDesk`          | Verifying offers, escrowing collateral, paying out principal, loan and slice records, repayment, the auction-only hooks | Immutable, except for a single `wire()` that fixes the auctions |
| `IntentBook` (base) | The EIP-712 domain `Farlight/1`, nonce bitmaps, partial-fill accounting, ECDSA and EIP-1271 verification                | Inherited by `LoanDesk`                                         |
| `CollateralAuction` | The Dutch auction, in-kind settlement, closed-market opt-outs, forwarding of proceeds                                   | Immutable                                                       |
| `RolloverAuction`   | The rising-rate rollover; holds acceptances in escrow; clears or fails                                                  | Immutable                                                       |
| `PriceGate`         | Chainlink feeds and streams behind session, staleness, move-cap, pause and sequencer-uptime guards                      | Feed configuration by governance                                |
| `AccessRegistry`    | Role lookups, forwarded to the adapter that `PolicyController` designates                                               | Adapter swappable through the timelock                          |
| `AttestationStore`  | Storing attestations on-chain with EAS semantics; issuers are whitelisted in `PolicyController`                         | Issuer set through the timelock                                 |
| `SliceToken`        | One ERC-721 for each slice; transfers complete only to an eligible lender                                               | Immutable; base URI by governance                               |
| `IdleVaultAdapter`  | Allowance-based deposits into one whitelisted ERC-4626 vault, and just-in-time withdrawals out of it                    | Immutable per vault                                             |
| `PolicyController`  | Every parameter, the timelock, the guardian pause, bootstrap mode                                                       | Timelocked once `finishBootstrap()` runs                        |
| `Treasury`          | Fee receipts; withdrawal by governance                                                                                  | Governance                                                      |

## Signed messages and loan states

Two message types are signed off-chain. Both are declared in the `Schema` library and hashed by `IntentHash`:

```solidity
struct LendOffer {
    address maker;
    Side    side;            // Lend only in v1
    address collateralToken; // token, or tier sentinel address(uint160(tier)) for standing offers
    address loanToken;
    uint256 principalMin;
    uint256 principalMax;
    uint16  aprBps;
    uint16  maxLtvBps;
    uint32  termSeconds;
    uint40  expiry;
    uint256 nonce;
    bytes32 salt;
    bytes32 requestId;       // 0 = standing; request hash = targeted; refinanceKey(loanId) = rollover
    uint8   flags;           // 1 selfLiquidate, 2 noClosedMarketLiquidation, 4 parkIdle
}

struct BorrowRequest {
    address borrower;
    address collateralToken;
    address loanToken;
    uint256 collateralAmount;
    uint256 principal;
    uint16  maxAprBps;
    uint32  termSeconds;
    uint40  fillDeadline;
    bytes32 salt;
}
```

Every loan occupies exactly one of six states: `Active`, `Refinancing`, `Liquidating`, `Repaid`, `Settled` or `Defaulted`. An `Active` loan whose maturity has passed is, by definition, inside its grace window.

## PolicyController

While bootstrap mode is on, the owner can invoke any setter directly. Calling `finishBootstrap()` switches that off permanently. From then on a setter can only run through `schedule(calls, salt, rationale)` and, after `delay` has passed, `execute(calls, salt)`. One call sits outside the timelock: `setPaused(newLoans, liquidations)`, which the guardian or the owner may invoke at any moment. Every setter emits `ParamChanged(key, subject, value)`.

The controller stores: the tier configurations; each token's tier and exposure cap; which loan tokens are enabled; the permitted terms; the loan parameters (minimum interest period, grace window, sequencer grace); the auction parameters (start premium, regular and closed-market floors, duration, penalty, keeper and lender shares); the refinance parameters (cap spread, duration); the fee parameters; the vault whitelist; the attestation issuers; and the eligibility adapter.

## PriceGate

The read path is `quote(token)`. It returns the price in loan-token units per whole collateral token, `updatedAt`, the session, the `paused` and `stale` flags, the ERC-8056 multiplier and `sequencerGrace`. The write path is `refresh(token)`, called at origination to take a checkpoint. A move beyond the cap pauses the market, and it stays paused until governance calls `resume`.

Where a market-status source is configured, the session comes from it, following the Chainlink Data Streams convention in which 5 means closed. Where none is configured, a UTC weekday schedule applies instead. Every session carries its own staleness bound. `verifyStreamReport` passes a report through a pluggable adapter and rejects it whenever the reported price sits further from the feed than the configured tolerance allows.

## RolloverAuction

| Function                     | Behaviour                                                                                                                                                                                                                                                                                              |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `openRefinance(loanId)`      | Restricted to the borrower, and only while the loan is still before maturity. The rate starts at the loan's blended APR and rises linearly across the duration to the start rate plus the cap spread.                                                                                                  |
| `accept(loanId, offer, sig)` | Any caller may submit a lender's signed offer. The offer's APR may not be above the current rate, and the slice is priced at the current rate. Accepted principal stays in escrow inside the auction; as soon as it covers the debt plus the refinance fee, the auction clears without further action. |
| `fail(loanId)`               | Any caller, once the window has elapsed. Acceptors get their principal back and the loan moves to `Defaulted`.                                                                                                                                                                                         |
| `cancelRefinance(loanId)`    | The borrower, so long as no acceptance has yet been recorded.                                                                                                                                                                                                                                          |

On clearing, each old slice receives principal plus interest, less the interest share; the refinance fee goes to `Treasury`; and the loan restarts under the new syndicate, with a fresh term and a principal equal to the old debt plus the fee. The clearing reverts if the resulting health factor would be below 1.0.

## CollateralAuction

| Function                                | Behaviour                                                                                                                                                                                                                                        |
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `startAuction(loanId)`                  | Any caller. The loan must have a health factor below 1.0, or be `Defaulted`, or be Active with its grace window behind it. The call is rejected while liquidations are paused, while the oracle is paused, or during the sequencer grace period. |
| `currentPrice(loanId)`                  | Declines linearly from `oracle × (1 + premium)` to `oracle × floor` over the auction duration. When the session is closed, the closed-market factor is the floor.                                                                                |
| `buy(loanId, collateralAmount, report)` | Requires `LIQUIDATOR`. Sells only as much collateral as covers debt plus penalty. If a stream adapter is configured for the token, the report is mandatory and is cross-checked against the feed.                                                |

When an auction begins during a closed session, `splitForLiquidation` carves out every slice whose lender set `noClosedMarketLiquidation`, along with its pro-rata collateral, into a separate Active loan. If every slice has opted out, the call reverts. Slices carrying `selfLiquidate` settle in kind at the oracle price, up to their pro-rata portion of the escrow. If nothing remains to auction after these steps, the loan is finalised immediately.

`finalizeLiquidation` distributes in a fixed order: first the keeper's share of the penalty; then lender claims, pro rata; then the lenders' share of the penalty; then the protocol's share of the penalty, together with the interest share on whatever interest was recovered; and finally any surplus USDG and unsold collateral back to the borrower.

## LoanDesk

### User-facing functions

| Function                                            | Behaviour                                                                                                                                                                                                                 |
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `originate(request, borrowerSig, offers[], sigs[])` | A single atomic transaction. Either the borrower calls it directly, or a `RELAYER` calls it while carrying the borrower's EIP-712 signature over the request. Partial fills of the request are not accepted.              |
| `repay(loanId, amount)`                             | Any caller. Interest is paid first, split pro rata by interest owed; principal follows, split pro rata by principal. A full repayment burns the slice tokens and releases the collateral. This function cannot be paused. |
| `addCollateral(loanId, amount)`                     | Allowed while the loan is Active or Refinancing.                                                                                                                                                                          |
| `cancel(nonce)` / `cancelWord(wordPos, mask)`       | Invalidate one offer nonce, or an entire word of nonces in one call.                                                                                                                                                      |

### Views

`getLoan`, `getSlice`, `debtOf` (principal plus interest owed, after the minimum interest floor is applied), `healthFactor` (a WAD value that already includes the closed-market haircut), `ltvBps` (debt over collateral value), `blendedAprBps`, `isPastGrace`, `remainingCapacity(offer)`, `offerHash`, `requestHash`, `domainSeparator` and `exposure(token)`.

### The checks inside `originate()`

1. New loans are not paused, the fill deadline is still ahead, and no amount is zero.
2. When a relayer is the caller: it holds `RELAYER`, and the borrower's signature verifies.
3. The borrower holds `BORROWER`.
4. The term is one of the permitted terms, the loan token is enabled, the collateral token is enabled, and filling the request would keep exposure under the cap.
5. `PriceGate.refresh()` succeeds: the market is not paused and the price is fresh (a stale price passes only in a closed session, and the haircut then applies).
6. The request's LTV, after any haircut, is within the tier maximum.
7. For each offer: it is unexpired; its nonce is live; its signature verifies (ECDSA or EIP-1271); its side, token or tier, loan token and term all match; its APR is within the borrower's cap; its lender maximum LTV is at least the request LTV; its request id matches; capacity and minimum fill are satisfied; the lender is eligible.
8. Value moves: principal is pulled from each lender (or from the idle vault where an offer carries `parkIdle`), collateral enters escrow, the origination fee goes to `Treasury`, the net principal reaches the borrower, and one slice token is minted for each slice.

### Hooks callable only by the auctions

`consumeOffer`, `beginLiquidation`, `splitForLiquidation`, `settleInKind`, `transferCollateral`, `finalizeLiquidation`, `beginRefinance`, `cancelRefinance`, `clearRefinance`, `markDefaulted` and `pullFromIdle` accept calls exclusively from the auction contracts set by `wire()`.

## Supporting contracts

* **`AccessRegistry`** takes the core's role questions and forwards them to the adapter that `PolicyController` names. Swapping the adapter is a timelocked operation.
* **`AttestationStore`** is the adapter currently in use: an on-chain store with EAS semantics that accepts attestations from whitelisted issuers in `PolicyController` and from no one else.
* **`SliceToken`** issues one ERC-721 per slice and refuses any transfer whose recipient is not an eligible lender. Governance can set the base URI; nothing else is adjustable.
* **`IdleVaultAdapter`** moves lender balances into one whitelisted ERC-4626 vault on an allowance basis and withdraws them just in time for a fill. Every vault gets its own immutable adapter.
* **`Treasury`** receives every protocol fee, and only governance can withdraw from it.

## Events

| Event                                                                                                                  | Source            |
| ---------------------------------------------------------------------------------------------------------------------- | ----------------- |
| `OfferCancelled`, `OfferWordCancelled`, `OfferFilled`                                                                  | IntentBook        |
| `LoanOriginated`, `SliceCreated`                                                                                       | LoanDesk          |
| `CollateralAdded`                                                                                                      | LoanDesk          |
| `LoanPartiallyRepaid`, `LoanRepaid`                                                                                    | LoanDesk          |
| `LoanSplit`, `InKindLiquidation`                                                                                       | LoanDesk          |
| `LiquidationStarted`, `LiquidationFinalized`                                                                           | LoanDesk          |
| `RefinanceStarted`, `RefinanceCancelled`, `RefinanceCleared`                                                           | LoanDesk          |
| `LoanDefaulted`, `Wired`                                                                                               | LoanDesk          |
| `AuctionStarted`, `AuctionBuy`, `AuctionSettled`                                                                       | CollateralAuction |
| `RefinanceOpened`, `RefinanceAccepted`, `RefinanceClearedEvent`, `RefinanceFailed`, `RefinanceCancelledEvent`          | RolloverAuction   |
| `FeedConfigured`, `Checkpointed`, `MarketPausedEvent`, `MarketResumed`, `SequencerFeedSet`, `ScheduleSet`              | PriceGate         |
| `Attested`, `Revoked`                                                                                                  | AttestationStore  |
| `OperationScheduled`, `OperationExecuted`, `OperationCancelled`, `ParamChanged`, `EmergencyPause`, `BootstrapFinished` | PolicyController  |
| `Deposited`, `Withdrawn`                                                                                               | IdleVaultAdapter  |

## Roles

A role identifier is `keccak256("farlight.role.<NAME>")`, where `<NAME>` is `BORROWER`, `LENDER_PROFESSIONAL`, `LENDER_RETAIL`, `LIQUIDATOR` or `RELAYER`. `isLender` is true for anyone who holds either of the two lender roles.

## Bytecode size

`LoanDesk` is the largest contract in the set at roughly 24.3 KB of runtime bytecode, inside the 24 KB EIP-170 ceiling that Arbitrum Nitro applies. Staying under that ceiling is why the build uses 100 optimizer runs. The roadmap route to more headroom is a Stylus port of the offer verifier.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.usefarlight.com/architecture/smart-contracts.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
