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

# Smart contracts

The Syndromics contracts live in the `contracts/` directory of the repository as a Foundry project targeting Robinhood Chain (chain ID 4663). This page describes the implementation as deployed; the [Deployment](/architecture/deployment.md) page covers building, testing and the production addresses.

{% hint style="info" %}
Solidity 0.8.26, Cancun EVM, via-IR, OpenZeppelin 5.2. Source is verified on Blockscout for every deployment, and the security programme is described on the [Security](/architecture/security.md) page.
{% endhint %}

## Design stance

* **Immutable core.** `LoanSettlement`, `OfferBook`, `PositionNFT` and both auctions have no proxy and no admin. Improvements ship as new deployments; existing loans run to maturity on the old one.
* **Parameters separated from logic.** Everything tunable lives in `ParamController` behind a timelock.
* **Isolation.** A loan is keyed to its collateral token and loan token; tier, cap and oracle configuration are read per token. Nothing is shared between markets.
* **Arbitrum-aware.** `block.timestamp` for every time calculation, compact calldata, every function reachable through the L1 delayed inbox.

## Contract map

| Contract                   | Responsibility                                                                                                         | Mutability                               |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- |
| `LoanSettlement`           | Verifies offers, escrows collateral, disburses principal, tracks loans and slices, repayment, restricted auction hooks | Immutable; one-time `wire()` of auctions |
| `OfferBook` (base)         | EIP-712 domain `Syndromics/1`, nonce bitmaps, partial-fill accounting, ECDSA and EIP-1271 verification                 | Inherited by `LoanSettlement`            |
| `LiquidationAuction`       | Dutch auction, in-kind settlement, closed-market exclusions, proceeds hand-off                                         | Immutable                                |
| `RefinanceAuction`         | Rising-rate rollover; escrows acceptances; clears or fails                                                             | Immutable                                |
| `OracleRouter`             | Chainlink feeds and streams with session, staleness, move-cap, pause and sequencer-uptime guards                       | Feed config by governance                |
| `EligibilityRegistry`      | Role checks; delegates to the adapter named in `ParamController`                                                       | Adapter swappable through timelock       |
| `NativeAttestationAdapter` | On-chain attestation store with EAS semantics; issuers whitelisted in `ParamController`                                | Issuer set through timelock              |
| `PositionNFT`              | One ERC-721 per slice; transfer requires the recipient to be an eligible lender                                        | Immutable; base URI by governance        |
| `IdleCapitalAdapter`       | Allowance-based deposit and just-in-time withdrawal from one whitelisted ERC-4626 vault                                | Immutable per vault                      |
| `ParamController`          | All parameters, timelock, guardian pause, bootstrap mode                                                               | Timelocked after `finishBootstrap()`     |
| `FeeCollector`             | Receives fees; withdrawal by governance                                                                                | Governance                               |

## Core types

```solidity
struct Offer {
    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 Request {
    address borrower;
    address collateralToken;
    address loanToken;
    uint256 collateralAmount;
    uint256 principal;
    uint16  maxAprBps;
    uint32  termSeconds;
    uint40  fillDeadline;
    bytes32 salt;
}
```

Loan status: `Active` (past maturity means the grace window), `Refinancing`, `Liquidating`, `Repaid`, `Settled`, `Defaulted`.

## LoanSettlement

### User functions

| Function                                         | Notes                                                                                                                                                                   |
| ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `settle(request, borrowerSig, offers[], sigs[])` | Atomic. Called by the borrower, or by a `RELAYER` with the borrower's EIP-712 signature over the request. Requires a full fill.                                         |
| `repay(loanId, amount)`                          | Anyone may repay. Interest first (pro rata by interest due), then principal (pro rata by principal). Full repayment burns NFTs and releases collateral. Never pausable. |
| `addCollateral(loanId, amount)`                  | Allowed while Active or Refinancing.                                                                                                                                    |
| `cancel(nonce)` / `cancelWord(wordPos, mask)`    | Cancel one offer nonce or a whole word of nonces.                                                                                                                       |

### Views

`getLoan`, `getSlice`, `debtOf` (principal and interest due, honouring the minimum interest floor), `healthFactor` (WAD, with the closed-market haircut applied), `ltvBps` (debt over collateral value), `blendedAprBps`, `isPastGrace`, `remainingCapacity(offer)`, `offerHash`, `requestHash`, `domainSeparator`, `exposure(token)`.

### Settlement checks, in order

1. New loans not paused; fill deadline not passed; non-zero amounts.
2. Relayer path: caller holds `RELAYER`, borrower signature valid.
3. Borrower holds `BORROWER`.
4. Term allowed, loan token enabled, collateral token enabled, exposure cap not exceeded.
5. `OracleRouter.refresh()`: market not paused; price not stale unless the session is closed (haircut applies then).
6. LTV within the tier maximum after any haircut.
7. Per offer: expiry, nonce not cancelled, signature (ECDSA or EIP-1271), side, token or tier match, loan token, term, APR at or under the borrower's cap, lender max LTV at or above the request LTV, request id, remaining capacity, minimum fill; lender is an eligible lender.
8. Principal pulled from each lender (or from the idle vault when `parkIdle` is set), collateral escrowed, origination fee to the collector, net principal to the borrower, one position NFT per slice.

### Auction hooks (restricted to the wired auction contracts)

`consumeOffer`, `beginLiquidation`, `splitForLiquidation`, `settleInKind`, `transferCollateral`, `finalizeLiquidation`, `beginRefinance`, `cancelRefinance`, `clearRefinance`, `markDefaulted`, `pullFromIdle`.

## LiquidationAuction

| Function                                | Notes                                                                                                                                                                                   |
| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `startAuction(loanId)`                  | Anyone. Requires health factor below 1.0, or `Defaulted`, or Active past the grace window. Blocked by the liquidation pause, an oracle pause, and the sequencer grace period.           |
| `currentPrice(loanId)`                  | Linear decline from `oracle × (1 + premium)` to `oracle × floor` over the duration; floor uses the closed-market factor when the session is closed.                                     |
| `buy(loanId, collateralAmount, report)` | Requires `LIQUIDATOR`. Sells only what is needed to reach debt plus penalty. If a stream adapter is configured for the token, a report is mandatory and cross-checked against the feed. |

On start, in a closed session, slices whose lenders set `noClosedMarketLiquidation` are moved with their pro-rata collateral into a new Active loan by `splitForLiquidation`; if every slice opted out the call reverts. Slices with `selfLiquidate` are settled in kind at the oracle price, capped at their pro-rata share of the escrow. If nothing is left to auction, the loan finalises immediately.

Proceeds order in `finalizeLiquidation`: keeper share of the penalty, lender claims pro rata, lender penalty share, protocol penalty share (plus the interest share on recovered interest), then surplus USDG and any unsold collateral to the borrower.

## RefinanceAuction

| Function                     | Notes                                                                                                                                                                                                                                                  |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `openRefinance(loanId)`      | Borrower only, before maturity. Rate starts at the blended APR and rises linearly to start plus the cap spread over the duration.                                                                                                                      |
| `accept(loanId, offer, sig)` | Anyone may submit a lender's signed offer. Offer APR must be at or below the current rate; the slice is created at the current rate. Acceptances are escrowed in the auction. Clears automatically when acceptances reach debt plus the refinance fee. |
| `fail(loanId)`               | After the window, anyone. Refunds acceptors and marks the loan `Defaulted`.                                                                                                                                                                            |
| `cancelRefinance(loanId)`    | Borrower, only while no acceptance exists.                                                                                                                                                                                                             |

On clearing, old slices are repaid in full (principal plus interest, net of the interest share), the refinance fee goes to the collector, and the loan restarts with the new syndicate, new principal equal to old debt plus fee, and a fresh term. The clearing reverts if the health factor would be below 1.0.

## OracleRouter

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

Session comes from a market-status source (Chainlink Data Streams convention, 5 = closed) when configured, otherwise from a weekday schedule in UTC. Staleness bounds are per session. `verifyStreamReport` delegates to a pluggable adapter and rejects divergence beyond the configured tolerance.

## ParamController

Bootstrap mode lets the owner call setters directly. `finishBootstrap()` is irreversible; afterwards every setter runs only through `schedule(calls, salt, rationale)` and `execute(calls, salt)` after `delay`. The guardian (or owner) may call `setPaused(newLoans, liquidations)` at any time. Every setter emits `ParamChanged(key, subject, value)`.

Parameters: tier configs, per-token tier and exposure cap, enabled loan tokens, allowed terms, loan params (minimum interest period, grace window, sequencer grace), auction params (start premium, regular and closed floors, duration, penalty, keeper and lender shares), refinance params (cap spread, duration), fee params, whitelisted vaults, attestation issuers, eligibility adapter.

## Events

| Event                                                                                                                  | Emitted by               |
| ---------------------------------------------------------------------------------------------------------------------- | ------------------------ |
| `OfferCancelled`, `OfferWordCancelled`, `OfferFilled`                                                                  | OfferBook                |
| `LoanSettled`, `SliceCreated`                                                                                          | LoanSettlement           |
| `CollateralAdded`                                                                                                      | LoanSettlement           |
| `LoanPartiallyRepaid`, `LoanRepaid`                                                                                    | LoanSettlement           |
| `LoanSplit`, `InKindLiquidation`                                                                                       | LoanSettlement           |
| `LiquidationStarted`, `LiquidationFinalized`                                                                           | LoanSettlement           |
| `RefinanceStarted`, `RefinanceCancelled`, `RefinanceCleared`                                                           | LoanSettlement           |
| `LoanDefaulted`, `Wired`                                                                                               | LoanSettlement           |
| `AuctionStarted`, `AuctionBuy`, `AuctionSettled`                                                                       | LiquidationAuction       |
| `RefinanceOpened`, `RefinanceAccepted`, `RefinanceClearedEvent`, `RefinanceFailed`, `RefinanceCancelledEvent`          | RefinanceAuction         |
| `FeedConfigured`, `Checkpointed`, `MarketPausedEvent`, `MarketResumed`, `SequencerFeedSet`, `ScheduleSet`              | OracleRouter             |
| `Attested`, `Revoked`                                                                                                  | NativeAttestationAdapter |
| `OperationScheduled`, `OperationExecuted`, `OperationCancelled`, `ParamChanged`, `EmergencyPause`, `BootstrapFinished` | ParamController          |
| `Deposited`, `Withdrawn`                                                                                               | IdleCapitalAdapter       |

## Roles

`keccak256("syndromics.role.<NAME>")` for `BORROWER`, `LENDER_PROFESSIONAL`, `LENDER_RETAIL`, `LIQUIDATOR`, `RELAYER`. `isLender` is true for either lender role.

## Sizes

`LoanSettlement` is the largest contract at about 24.3 KB of runtime bytecode, inside the 24 KB EIP-170 limit 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.syndromics.xyz/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.
