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

# Contract reference

The on-chain code lives under `contracts/` in the repository as a Foundry project built for Robinhood Chain (chain ID 4663). This page describes the code as it runs in production; build steps, the test suite and the live addresses are on the [Deployment](/architecture/deployment.md) page.

{% hint style="info" %}
Toolchain: Solidity 0.8.26 targeting the Cancun EVM, compiled via-IR, on OpenZeppelin 5.2. Each deployment has its source verified on Blockscout. Testing, audits and operational controls are covered on the [Security](/architecture/security.md) page.
{% endhint %}

## Principles behind the design

* **The core does not change.** `LoanDesk`, `IntentBook`, `SliceToken`, `CollateralAuction` and `RolloverAuction` carry no proxy and no admin role. Improvements arrive as a new deployment, and loans open on the old one run to maturity there.
* **Configuration sits apart from logic.** Anything that can be tuned is a `PolicyController` value, and the controller is timelocked.
* **Markets do not leak into each other.** A loan is identified by its collateral token together with its loan token. Tier, exposure cap and oracle settings are looked up per token, so no state is shared across markets.
* **Built for an Arbitrum-family chain.** Time is always `block.timestamp`, calldata is kept compact, and every function can be reached through the L1 delayed inbox.

## Inventory

| Contract            | Owns                                                                                                                       | Can it change?                                            |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
| `LoanDesk`          | Offer verification, collateral escrow, principal disbursement, loan and slice records, repayment, restricted auction hooks | Immutable, apart from a one-time `wire()` of the auctions |
| `IntentBook` (base) | EIP-712 domain `Ethosis/1`, nonce bitmaps, partial-fill bookkeeping, ECDSA and EIP-1271 checks                             | Inherited by `LoanDesk`                                   |
| `CollateralAuction` | Dutch auction, in-kind settlement, closed-market opt-outs, hand-off of proceeds                                            | Immutable                                                 |
| `RolloverAuction`   | Rising-rate rollover; escrows acceptances; clears or fails                                                                 | Immutable                                                 |
| `PriceGate`         | Chainlink feeds and streams, wrapped in session, staleness, move-cap, pause and sequencer-uptime guards                    | Feed configuration by governance                          |
| `AccessRegistry`    | Role checks, delegated to whichever adapter `PolicyController` names                                                       | Adapter swappable through the timelock                    |
| `AttestationStore`  | On-chain attestation storage with EAS semantics; issuers whitelisted in `PolicyController`                                 | Issuer set through the timelock                           |
| `SliceToken`        | One ERC-721 per slice; a transfer succeeds only if the recipient is an eligible lender                                     | Immutable; base URI by governance                         |
| `IdleVaultAdapter`  | Allowance-based deposits into, and just-in-time withdrawals from, a single whitelisted ERC-4626 vault                      | Immutable per vault                                       |
| `PolicyController`  | All parameters, the timelock, the guardian pause, bootstrap mode                                                           | Timelocked once `finishBootstrap()` runs                  |
| `Treasury`          | Fee receipts; withdrawal by governance                                                                                     | Governance                                                |

## Message types and loan states

The two signed messages, defined 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;
}
```

A loan is in one of six states: `Active` (an Active loan past maturity is inside its grace window), `Refinancing`, `Liquidating`, `Repaid`, `Settled` or `Defaulted`.

## PolicyController

In bootstrap mode the owner may call any setter directly. `finishBootstrap()` ends that mode for good: afterwards a setter runs only through `schedule(calls, salt, rationale)` followed, once `delay` has elapsed, by `execute(calls, salt)`. The exception is `setPaused(newLoans, liquidations)`, which the guardian or the owner may call at any time. Each setter emits `ParamChanged(key, subject, value)`.

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

## PriceGate

`quote(token)` is the view: price in loan-token units per whole collateral token, `updatedAt`, session, the `paused` and `stale` flags, the ERC-8056 multiplier and `sequencerGrace`. `refresh(token)` is its non-view counterpart, the checkpoint taken at origination: a move beyond the cap pauses the market until governance calls `resume`.

The session is read from a market-status source following the Chainlink Data Streams convention (5 means closed) where one is configured; without one, a UTC weekday schedule decides. Each session has its own staleness bound. `verifyStreamReport` hands the report to a pluggable adapter and rejects it if the reported price diverges from the feed by more than the configured tolerance.

## RolloverAuction

| Function                     | Behaviour                                                                                                                                                                                                                                                   |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `openRefinance(loanId)`      | Only the borrower, and only before maturity. The rate begins at the loan's blended APR and climbs linearly over the duration until it reaches the start rate plus the cap spread.                                                                           |
| `accept(loanId, offer, sig)` | Anyone can hand in a lender's signed offer. Its APR must not exceed the current rate, and the slice is priced at the current rate. Accepted principal is escrowed in the auction; once it covers debt plus the refinance fee the auction clears on its own. |
| `fail(loanId)`               | Anyone, after the window has closed. Acceptors are refunded and the loan becomes `Defaulted`.                                                                                                                                                               |
| `cancelRefinance(loanId)`    | The borrower, provided no acceptance has been recorded yet.                                                                                                                                                                                                 |

When the auction clears, every old slice is paid out in full (principal plus interest, less the interest share), the refinance fee is sent to `Treasury`, and the loan begins again under the new syndicate with a fresh term and a principal equal to the old debt plus the fee. If the resulting health factor would sit below 1.0 the clearing reverts.

## CollateralAuction

| Function                                | Behaviour                                                                                                                                                                                                                             |
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `startAuction(loanId)`                  | Anyone. The loan must have a health factor under 1.0, or be `Defaulted`, or be Active and past its grace window. The call is refused while liquidations are paused, while the oracle is paused, or during the sequencer grace period. |
| `currentPrice(loanId)`                  | Falls linearly from `oracle × (1 + premium)` to `oracle × floor` across the auction duration. In a closed session the floor is the closed-market factor.                                                                              |
| `buy(loanId, collateralAmount, report)` | Caller needs `LIQUIDATOR`. Only enough collateral to cover debt plus penalty is sold. Where a stream adapter is configured for the token the report is required and is cross-checked against the feed.                                |

At the start of an auction in a closed session, any slice whose lender chose `noClosedMarketLiquidation` is carved out, together with its pro-rata collateral, into a new Active loan by `splitForLiquidation`. Should every slice have opted out, the call reverts. Slices flagged `selfLiquidate` are settled in kind at the oracle price, capped at their pro-rata portion of the escrow. When these steps leave nothing to auction, the loan is finalised on the spot.

`finalizeLiquidation` pays out in this sequence: the keeper's share of the penalty; lender claims, pro rata; the lenders' share of the penalty; the protocol's share of the penalty together with the interest share on any interest recovered; and lastly any surplus USDG plus unsold collateral back to the borrower.

## LoanDesk

### Functions for users

| Function                                            | Behaviour                                                                                                                                                                                                                        |
| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `originate(request, borrowerSig, offers[], sigs[])` | One atomic transaction. The borrower calls it directly, or a `RELAYER` calls it carrying the borrower's EIP-712 signature over the request. The request must be filled in full.                                                  |
| `repay(loanId, amount)`                             | Open to any caller. Payment goes to interest first (split pro rata by interest owed) and then to principal (split pro rata by principal). Paying everything burns the slice tokens and returns the collateral. Cannot be paused. |
| `addCollateral(loanId, amount)`                     | Available while the loan is Active or Refinancing.                                                                                                                                                                               |
| `cancel(nonce)` / `cancelWord(wordPos, mask)`       | Void a single offer nonce, or a whole word of nonces at once.                                                                                                                                                                    |

### Read-only functions

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

### What `originate()` verifies, step by step

1. New loans are not paused, the fill deadline has not passed, and no amount is zero.
2. If a relayer is calling: it holds `RELAYER`, and the borrower's signature checks out.
3. The borrower holds `BORROWER`.
4. The term is permitted, the loan token is enabled, the collateral token is enabled, and the exposure cap would not be breached.
5. `PriceGate.refresh()` passes: the market is not paused, and the price is fresh (a stale price is tolerated only in a closed session, where the haircut then applies).
6. The request's LTV is within the tier maximum once any haircut is taken into account.
7. Per offer: unexpired; nonce live; signature valid (ECDSA or EIP-1271); side, token or tier, loan token and term all match; APR within the borrower's cap; lender maximum LTV at least the request LTV; request id matches; capacity and minimum fill satisfied; lender eligible.
8. Funds move: principal is drawn from each lender (or from the idle vault for offers flagged `parkIdle`), collateral goes into escrow, the origination fee goes to `Treasury`, the net principal goes to the borrower, and one slice token is minted per slice.

### Hooks reserved for the auctions

Only the auction contracts fixed by `wire()` may call `consumeOffer`, `beginLiquidation`, `splitForLiquidation`, `settleInKind`, `transferCollateral`, `finalizeLiquidation`, `beginRefinance`, `cancelRefinance`, `clearRefinance`, `markDefaulted` and `pullFromIdle`.

## The supporting contracts

* **`AccessRegistry`** answers the core's role questions by forwarding them to the adapter named in `PolicyController`; changing that adapter is a timelocked operation.
* **`AttestationStore`** is the adapter in use: an on-chain store with EAS semantics that accepts attestations only from issuers whitelisted in `PolicyController`.
* **`SliceToken`** mints one ERC-721 per slice and rejects any transfer to a recipient who is not an eligible lender. Governance may set the base URI; nothing else about it can change.
* **`IdleVaultAdapter`** moves lender balances into a single whitelisted ERC-4626 vault on an allowance basis and pulls them out just in time for a fill. Each vault has its own immutable adapter.
* **`Treasury`** collects every protocol fee; only governance can withdraw.

## Events

| Event                                                                                                                  | Source            |
| ---------------------------------------------------------------------------------------------------------------------- | ----------------- |
| `OfferCancelled`, `OfferWordCancelled`, `OfferFilled`                                                                  | IntentBook        |
| `LoanSettled`, `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  |

## Role identifiers

Roles are `keccak256("ethosis.role.<NAME>")` with `<NAME>` one of `BORROWER`, `LENDER_PROFESSIONAL`, `LENDER_RETAIL`, `LIQUIDATOR` or `RELAYER`. `isLender` returns true for a holder of either lender role.

## Bytecode size

At roughly 24.3 KB of runtime bytecode, `LoanDesk` is the biggest contract in the set and sits within the 24 KB EIP-170 ceiling that Arbitrum Nitro enforces. The build uses 100 optimizer runs to stay under it. A Stylus port of the offer verifier is the roadmap route to more headroom.


---

# 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.ethosis.org/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.
