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

# The contract set

Every Gimbal contract on Robinhood Chain, what each one is allowed to do, and the functions, events, flags and roles an integrator builds against.

Gimbal's on-chain code is a Foundry project in the `contracts/` folder of the repository, built for chain ID 4663, Robinhood Chain. This page describes the deployed behaviour. Build steps, the test suite and deployed addresses are on [Build and deploy](/architecture/build-and-deploy.md).

{% hint style="info" %}
Solidity 0.8.26, Cancun EVM, compiled via IR with 100 optimizer runs, on OpenZeppelin 5.2. Each deployment has its source verified on Blockscout. Tests, audit history and operating safeguards are on [Assurance](/architecture/assurance.md).
{% endhint %}

## Inventory

| Contract            | Job                                                                                                                                  | Who can change it                                          |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------- |
| `TermDesk`          | Verifies offers, escrows collateral, pays out principal, keeps the loan and slice ledger, takes repayment, exposes the auction hooks | Nobody, apart from one `wire()` call                       |
| `OfferBook`         | Base of the desk: EIP-712 domain `Gimbal` version `1`, nonce bitmaps, fills per offer digest, ECDSA and EIP-1271 checks              | Nobody                                                     |
| `CollateralAuction` | Descending-price sale, in-kind payouts, closed-market opt-outs, restarts                                                             | Nobody                                                     |
| `RolloverAuction`   | Rising-rate rollover; escrows acceptances until it clears, fails or is withdrawn                                                     | Nobody                                                     |
| `PriceSentinel`     | Chainlink feed and optional Data Streams adapter per token; session, staleness, move cap, pause and sequencer checks                 | Feed configuration by governance                           |
| `RoleRegistry`      | Forwards every role question to the adapter `PolicyBoard` names                                                                      | Adapter swap through the timelock                          |
| `AttestationLedger` | The bundled adapter: EAS-style records written by approved issuers                                                                   | Issuer set through the timelock                            |
| `LenderSlice`       | ERC-721 "Gimbal Lender Slice" (GSLC), one token per slice, transferable only to an eligible lender                                   | Base URI by governance                                     |
| `ParkingAdapter`    | Parks lender USDG in one whitelisted ERC-4626 vault and returns it just in time                                                      | One adapter per vault, fixed at deployment                 |
| `PolicyBoard`       | All parameters, the timelock itself, the guardian's pause switch, the bootstrap flag                                                 | Owner during bootstrap; timelock after `finishBootstrap()` |
| `Treasury`          | Fee income                                                                                                                           | Governance withdraws                                       |

Three libraries sit underneath: `Schema` (types, flags, and the role ids via `Roles`), `OfferHash` (EIP-712 hashing and the rollover key) and `Accrual` (linear per-second interest, never compounded).

## Design choices that shape the rest

* **No proxies, no admin.** `TermDesk`, `OfferBook`, `LenderSlice`, `CollateralAuction` and `RolloverAuction` have no upgrade path and no privileged operator. Improvements ship as a new deployment; a loan opened on the old one finishes there.
* **Parameters and code are separate.** Anything governance can tune lives in `PolicyBoard`, behind a timelock.
* **One market per token pair.** Tier, exposure cap and feed configuration resolve per collateral token, and the loan token must be the one the feed is denominated in.
* **Built for Arbitrum Nitro.** Time is always `block.timestamp`, offer calldata is kept compact, and each entry point can still be reached via the L1 delayed inbox.

## Types, flags and states

`Schema` declares the two signed message types. `OfferHash` encodes them; the type strings match the field order exactly, with `Side` encoded as `uint8`.

```solidity
struct LendOffer {
    address maker;
    Side    side;            // only Lend is accepted
    address collateralToken; // exact token, or the tier sentinel address(uint160(tier))
    address loanToken;
    uint256 principalMin;    // smallest fill, unless the fill drains the offer
    uint256 principalMax;
    uint16  aprBps;
    uint16  maxLtvBps;       // the highest LTV this lender will fund
    uint32  termSeconds;
    uint40  expiry;
    uint256 nonce;           // cancellation handle; a fill leaves it untouched
    bytes32 salt;
    bytes32 requestId;       // 0 standing; a request digest targets one request; rolloverKey(loanId) targets one rollover
    uint8   flags;
}

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

Flag bits on an offer, copied onto the slice it becomes:

| Bit | Constant                            | Effect                                                                                                                             |
| --- | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| 1   | `FLAG_SELF_LIQUIDATE`               | At liquidation, take collateral at the sentinel price instead of sale proceeds, capped at the slice's pro rata share of the escrow |
| 2   | `FLAG_NO_CLOSED_MARKET_LIQUIDATION` | Keep this slice out of any auction opened while the equity market is closed                                                        |
| 4   | `FLAG_PARK_IDLE`                    | Fund fills from, and take repayments into, the vault behind `ParkingAdapter`                                                       |

`LoanStatus` has a `None` value for unused ids and six live states: `Active`, `Refinancing`, `Liquidating`, `Defaulted`, `Repaid` and `Settled`. `Active` spans the term, the grace window, and everything after it until an auction opens. `Session` is `Regular`, `Extended` or `Closed`.

## Roles

A role id is `keccak256("gimbal.role.<NAME>")` for `RELAYER`, `BORROWER`, `LIQUIDATOR`, `LENDER_PROFESSIONAL` and `LENDER_RETAIL`. `RoleRegistry.isLender` is true for either lender role.

## The desk

### Entry points for borrowers and lenders

| Function                                            | What happens                                                                                                                                                                                                                                                       |
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `originate(request, borrowerSig, offers[], sigs[])` | One atomic transaction that fills a request from the offers given, in order. The borrower may call it directly (the signature is then ignored); any other caller needs `RELAYER` plus a valid borrower signature. Offers must land exactly on `request.principal`. |
| `repay(loanId, amount)`                             | Open to anyone, never pausable. Accepted in `Active` and in `Defaulted`. Interest is settled first, principal after, each split across slices by that slice's share of what is owed. Paying the full debt burns the slices and releases the escrow.                |
| `addCollateral(loanId, amount)`                     | Anyone may top up, in every state that accepts repayment and during a rollover.                                                                                                                                                                                    |
| `cancelRequest(request)`                            | Borrower only. Spends the request digest so no relayer holding the signature can fill it.                                                                                                                                                                          |
| `cancel(nonce)`, `cancelWord(wordPos, mask)`        | From `OfferBook`. Retire one nonce, or every nonce a mask selects in a 256-wide word.                                                                                                                                                                              |

### Reads

`getLoan`, `getSlice`, `debtOf` (principal and interest to the current second, each slice floored at its minimum interest), `healthFactor` (WAD; the closed-market haircut is already subtracted when the quote is closed or stale), `ltvBps` (debt over collateral value), `exposure(token)`, and from `OfferBook`: `domainSeparator`, `offerHash`, `requestHash` and `remainingCapacity(offer)`. Two reads that sound like desk functions live elsewhere: `blendedAprBps(loanId)` on `RolloverAuction`, `isPastGrace(loanId)` on `CollateralAuction`.

### What `originate()` checks, in order

1. The origination pause is off; `fillDeadline` lies ahead; principal and collateral are both non-zero.
2. The request digest is unspent. It is spent now, so no replay is possible.
3. If the caller is not the borrower: the caller holds `RELAYER` and the borrower's EIP-712 signature verifies.
4. The borrower holds `BORROWER`.
5. Gates that need no price: the term is allowed; the loan token is on the enabled list and equals `PriceSentinel.quoteToken(collateral)`; the collateral token is enabled too; the fill keeps exposure within the token's cap.
6. `PriceSentinel.refresh()` runs and writes a move-cap checkpoint. A paused market, a zero valuation, a stale price in a live session, or sequencer grace all revert. A stale price in a closed session, like any closed-session quote, is accepted with the haircut.
7. The request's LTV is at or under the tier's `maxLtvBps`, less `closedHaircutBps` when the haircut applies.
8. Per offer: unexpired, nonce live, signature valid for the maker; lend-side; collateral equal to the request's token or the tier sentinel; loan token, term, rate cap, LTV bound and `requestId` all compatible with the request; capacity remaining; fill at or above `principalMin` unless it drains the offer; maker a lender; slice count under `maxSlicesPerLoan`.
9. Value moves: each lender's principal is pulled from their wallet, or through `ParkingAdapter` for a `FLAG_PARK_IDLE` offer, and one slice is minted per offer. Collateral enters escrow, `Treasury` receives the origination fee, the borrower receives the net principal.

### Hooks the auctions alone may call

`beginLiquidation`, `splitForLiquidation`, `settleInKind`, `transferCollateral` and `finalizeLiquidation` serve the collateral auction; `consumeOffer`, `releaseOffer`, `pullFromIdle`, `beginRefinance`, `clearRefinance`, `cancelRefinance` and `markDefaulted` serve the rollover auction. All twelve sit behind `auctionOnly`, which admits the two addresses `wire()` recorded. A thirteenth, `onSliceTransfer`, is reserved for `LenderSlice` and repoints a slice's payee when the token changes hands.

`wire(collateralAuction, rolloverAuction, idleAdapter)` is called once by the owner of `PolicyBoard`. A zero adapter disables parking; a non-zero one must sit on a whitelisted vault.

## CollateralAuction

| Function                                | Behaviour                                                                                                                                                                                                                                                                                                                               |
| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `startAuction(loanId)`                  | Permissionless; the caller becomes the keeper. The loan qualifies with a health factor under 1.0, or when `Defaulted`, or when `Active` past maturity plus the grace window. Refused when liquidations are paused, when the sentinel is paused, inside sequencer grace, or with a stale price in a live session.                        |
| `restart(loanId)`                       | Anyone, once the window has elapsed short of target. Rebuilds the schedule from a fresh quote; proceeds so far carry over and the original keeper keeps the keeper share.                                                                                                                                                               |
| `currentPrice(loanId)`                  | Falls linearly from the quote times (1 + start premium) to the quote times the floor ratio, then holds at the floor. The ratio is `floorBpsClosed` in a closed session and `floorBpsRegular` otherwise.                                                                                                                                 |
| `buy(loanId, collateralAmount, report)` | Needs `LIQUIDATOR`; blocked by the liquidations pause. Clipped to the escrow and to what is still needed to reach the target. When the token has a stream adapter, a report is required and is verified against the feed. Payment goes straight to the desk; the auction settles once proceeds reach the target or the escrow is empty. |

Opening sequence: `refresh` the price; `beginLiquidation`, which abandons any rollover in progress; in a closed session, move every `FLAG_NO_CLOSED_MARKET_LIQUIDATION` slice into a new `Active` loan through `splitForLiquidation` with collateral pro rata, or revert if all slices opted out; pay each `FLAG_SELF_LIQUIDATE` slice in collateral at the checkpointed price, capped at its pro rata share; finalize at once with zero proceeds if nothing is left to sell.

`finalizeLiquidation` sets the penalty at `penaltyBps` of debt and pays out in a fixed order, each tranche limited to what is left: the keeper's penalty portion; lenders' claims in proportion to what each is owed; the lenders' portion of the penalty; the protocol's portion plus the interest share on recovered interest; residual loan token and unsold collateral to the borrower. The loan ends `Settled` whether or not lenders were made whole.

## RolloverAuction

The contract call is `openRefinance`; the docs call the mechanism a rollover auction.

| Function                                                | Behaviour                                                                                                                                                                                                                                                                                                                                                                                      |
| ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `openRefinance(loanId)`                                 | Borrower only, on an `Active` loan whose maturity is still ahead. The opening rate is the blended APR of the live slices; it climbs linearly across `duration` to that figure plus `capSpreadBps`.                                                                                                                                                                                             |
| `accept(loanId, offer, sig)`                            | Anyone can present a lender's signed offer on their behalf. Its APR must be at or under the going rate, and the slice is written at the going rate. The offer must fit the loan's tokens and term, carry `requestId` zero or `rolloverKey(loanId)`, and allow the loan's current LTV. Funds sit in the auction; once acceptances cover debt plus the rollover fee, it clears in the same call. |
| `fail(loanId)`                                          | Anyone, once the window has passed short of target. Acceptors are refunded, their offers regain capacity, and the loan becomes `Defaulted`.                                                                                                                                                                                                                                                    |
| `cancelRefinance(loanId)`                               | Borrower only, at any point before clearing. Every acceptance is refunded and the loan returns to `Active` on its old terms.                                                                                                                                                                                                                                                                   |
| `blendedAprBps`, `currentRate`, `target`, `acceptances` | Reads: the opening rate; the rate now; debt plus fee; the funded acceptances in arrival order.                                                                                                                                                                                                                                                                                                 |

On clearing, `clearRefinance` pays each outgoing slice principal plus interest net of the interest share, sends the rollover fee to `Treasury`, and restarts the loan under the incoming syndicate: fresh start time, same term length, principal equal to old debt plus fee, and a health factor of at least 1.0 or the call reverts. `cancelForLiquidation` is desk-only and unwinds a rollover when a collateral auction opens on the same loan.

## PriceSentinel

`quote(token)` is the read path and never reverts on a stale or paused market. It returns `price`, in loan-token base units for one whole collateral token, plus `updatedAt`, `session`, `paused`, `stale`, `multiplier` (the ERC-8056 `uiMultiplier`, reported for information and never applied, since Chainlink equity feeds already fold it in) and `sequencerGrace`.

`refresh(token)` is the write path, called at origination and when an auction opens or restarts. A gap beyond `moveCapBps` from a checkpoint younger than `moveCapWindow` pauses the market. The pause holds until governance calls `resume(token)`; governance can also `pause(token)` by hand.

The session comes from a market-status source when one is configured, using Chainlink Data Streams codes (1 regular, 2 extended, 5 closed). Without one, a weekday UTC schedule applies, by default 14:30 to 21:00 regular and 09:00 to 01:00 the next day extended. Each session has its own staleness bound. `verifyStreamReport(token, report)` runs the report through the token's adapter, rejects one older than the session bound, and rejects a price further than `streamDivergenceBps` from the feed. Per-token setup is `configureFeed`; the uptime feed is `setSequencerFeed`.

## PolicyBoard

During bootstrap the owner calls setters directly. `finishBootstrap()` ends that for good. Afterwards a setter runs only inside a batch: `schedule(calls, salt, rationale)` queues it, and `execute(calls, salt)` runs it once `delay` has passed and before a fixed `GRACE_PERIOD` of 14 days expires. `cancel(id)` withdraws a queued batch. After bootstrap the delay cannot go under one hour. Ownership moves in two steps, `transferOwnership` then `acceptOwnership`.

`setPaused(newLoans, liquidations)` bypasses the timelock. The owner and the timelock may set either flag either way; the guardian may raise a flag but never clear one.

Each setter ends by emitting `ParamChanged(key, subject, value)`. The board holds: tier configs (max LTV, liquidation LTV, closed-market haircut); token configs (tier, exposure cap); enabled loan tokens; allowed terms; `LoanParams` (slices per loan, sequencer grace, grace window, minimum interest period); `AuctionParams` (duration, start premium, the two floors, penalty, keeper and lender shares); `RefinanceParams` (cap spread, duration); `FeeParams` (origination, interest share, rollover, idle yield share); the vault whitelist; attestation issuers; the eligibility adapter; guardian; delay.

## The rest of the set

* **`AttestationLedger`** is the adapter in service. `attest` and `revoke` are open to approved issuers, any of whom may revoke any record. A record counts while unrevoked, unexpired and from an issuer still approved, so dropping an issuer voids everything it wrote.
* **`LenderSlice`** mints and burns only for the desk. A transfer requires the recipient to pass `requireLender` and notifies the desk before ownership changes. Governance may set `baseURI`.
* **`ParkingAdapter`** tracks each lender's vault shares. Lenders use `deposit`, `withdraw` and `withdrawAll`; the desk uses `withdrawFor` and `depositFor`. Whitelisting is checked on every deposit, so retiring a vault stops inflows without trapping balances; a repayment that cannot be parked goes to the lender's wallet instead.
* **`Treasury`** takes fees by plain transfer and lets only the board's owner `withdraw`.

## Events

| Source              | Events                                                                                                                                                                                                                                                                                                   |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `OfferBook`         | `OfferCancelled`, `OfferWordCancelled`, `OfferFilled`, `OfferReleased`                                                                                                                                                                                                                                   |
| `TermDesk`          | `LoanOriginated`, `SliceCreated`, `CollateralAdded`, `LoanPartiallyRepaid`, `LoanRepaid`, `LoanSplit`, `InKindLiquidation`, `LiquidationStarted`, `LiquidationFinalized`, `RefinanceStarted`, `RefinanceCancelled`, `RefinanceCleared`, `LoanDefaulted`, `RequestCancelled`, `SliceTransferred`, `Wired` |
| `CollateralAuction` | `AuctionStarted`, `AuctionBuy`, `AuctionRestarted`, `AuctionSettled`                                                                                                                                                                                                                                     |
| `RolloverAuction`   | `RefinanceOpened`, `RefinanceAccepted`, `RefinanceClearedEvent`, `RefinanceFailed`, `RefinanceCancelledEvent`                                                                                                                                                                                            |
| `PriceSentinel`     | `FeedConfigured`, `SequencerFeedSet`, `ScheduleSet`, `MarketPausedEvent`, `MarketResumed`, `Checkpointed`                                                                                                                                                                                                |
| `AttestationLedger` | `Attested`, `Revoked`                                                                                                                                                                                                                                                                                    |
| `PolicyBoard`       | `OperationScheduled`, `OperationExecuted`, `OperationCancelled`, `ParamChanged`, `OwnershipTransferStarted`, `OwnershipTransferred`, `GuardianChanged`, `DelayChanged`, `BootstrapFinished`, `EmergencyPause`                                                                                            |
| `LenderSlice`       | `DeskSet`, `BaseURISet`                                                                                                                                                                                                                                                                                  |
| `ParkingAdapter`    | `Deposited`, `Withdrawn`, `DeskSet`, `ParkingSkipped`                                                                                                                                                                                                                                                    |
| `Treasury`          | `Withdrawn`                                                                                                                                                                                                                                                                                              |

## Invariants the set holds

* A loan exists only if every offer in the transaction verified, every lender's principal moved, and the collateral is in escrow. Anything less reverts the whole call.
* A loan can be repaid while `Active` (term, grace window and beyond) and while `Defaulted`. Repayment closes only once a collateral auction opens.
* A shortfall on one slice is that slice's alone. Nothing is socialised across slices or loans.
* Collateral is priced by the feed configured for the exact token in escrow. Wrappers and derived rates are refused.
* Auction proceeds follow one waterfall: keeper share, lender claims, lenders' penalty share, protocol's penalty share, surplus to the borrower.
* Governance keys cannot move escrowed collateral, lender principal or vault balances.

## Bytecode size

`TermDesk` is the largest contract, with runtime bytecode of roughly 24.3 KB, a few hundred bytes under the 24,576-byte EIP-170 ceiling that Arbitrum Nitro enforces. That ceiling is the reason for the 100-run optimizer setting. Porting the offer verifier to Stylus is on the roadmap and would free space.


---

# 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.usegimbal.app/architecture/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.
