> ## Documentation Index
> Fetch the complete documentation index at: https://docs.canopy.deal/llms.txt
> Use this file to discover all available pages before exploring further.

# Canopy Data Structures: Offer and Series Struct Reference

## Offer Struct

Created when a seller calls `createOffer`. Fields evolve as the offer is filled or cancelled. Records persist after the offer is consumed for historical queries.

<ParamField path="seller" type="address">
  The wallet address that created the offer and deposited the collateral. This is the address that receives quote token payments when the offer is filled and that is authorized to cancel the offer.
</ParamField>

<ParamField path="asset" type="address">
  The ERC-20 token contract address of the asset being sold. This is the token that the seller has deposited as collateral and that buyers (or claim token holders) ultimately receive.
</ParamField>

<ParamField path="quote" type="address">
  The ERC-20 token contract address accepted as payment. Quote tokens are never held by the venue — they are transferred directly to the seller and fee recipient at fill time.
</ParamField>

<ParamField path="allowedBuyer" type="address">
  The address permitted to fill this offer. When set to `address(0)`, the offer is public and any address may fill it. When set to a specific address, the offer is private and only that address can execute a fill.
</ParamField>

<ParamField path="feeRecipient" type="address">
  The address that receives the protocol fee for fills against this offer. Snapshotted from the venue's `feeRecipient` at offer creation time so that later changes to the venue's fee recipient do not affect existing offers.
</ParamField>

<ParamField path="assetRemaining" type="uint256">
  The amount of escrowed asset collateral that has not yet been filled or cancelled. This value starts at the actual amount received by the venue (which may differ from the requested amount for fee-on-transfer tokens) and decreases as fills occur. When it reaches `0`, the offer becomes inactive.
</ParamField>

<ParamField path="pricePerUnit" type="uint256">
  The quote-token cost per one whole unit of the asset (i.e., per `10^assetDecimals` of the asset token). See the [Price Computation](#price-computation) section below for how fill costs are calculated.
</ParamField>

<ParamField path="expiry" type="uint64">
  A Unix timestamp after which the offer can no longer be filled. The offer reverts any fill attempt where `block.timestamp >= expiry`. Expiry must be in the future at creation time.
</ParamField>

<ParamField path="unlockTime" type="uint64">
  A Unix timestamp controlling the settlement mode:

  * **`0`** — Spot offer. Assets transfer directly to the buyer at fill time.
  * **Non-zero** — Deferred (forward) offer. Assets remain in escrow until the unlock time; buyers receive ERC-20 claim tokens at fill time and redeem them for the underlying collateral after `block.timestamp >= unlockTime`. Must satisfy `unlockTime >= expiry`.
</ParamField>

<ParamField path="assetDecimals" type="uint8">
  The decimal precision of the asset token, cached at offer creation. Used in the price calculation formula to convert `pricePerUnit` to the correct quote amount.
</ParamField>

<ParamField path="feeBps" type="uint16">
  The fee rate in basis points, snapshotted from the venue's `feeBps` at offer creation time. This means the fee rate is locked for the lifetime of the offer, even if the admin changes the venue-level fee later.
</ParamField>

<ParamField path="assetFilled" type="uint256">
  The cumulative amount of asset that has been filled so far across all partial fills. Used in the cumulative price calculation to ensure consistent pricing regardless of fill sequence.
</ParamField>

<ParamField path="quotePaid" type="uint256">
  The cumulative quote tokens paid across all fills. Used with `assetFilled` in the cumulative `mulDiv` formula to ensure mathematically precise split-fill pricing.
</ParamField>

<ParamField path="active" type="bool">
  `true` while the offer is open for fills. Set to `false` when either the offer is fully filled (`assetRemaining == 0`) or the seller cancels.
</ParamField>

## Series Struct

A `Series` record is created automatically the first time a deferred offer is filled for a novel `(asset, unlockTime)` pair. All deferred offers that share these two parameters contribute to the same series and share the same claim token contract.

<ParamField path="asset" type="address">
  The ERC-20 asset token address that defines this series.
</ParamField>

<ParamField path="unlockTime" type="uint64">
  The unlock timestamp shared by all offers in this series. Claim tokens for this series become redeemable once `block.timestamp >= unlockTime`.
</ParamField>

<ParamField path="claimToken" type="address">
  The address of the ERC-20 claim token contract deployed for this series by the venue on first fill. Buyers who fill deferred offers in this series receive tokens at this address, and redemption burns tokens from this contract.
</ParamField>

<ParamField path="backing" type="uint256">
  The total amount of asset collateral currently backing outstanding claim tokens for this series. This counter increases when fills mint claim tokens and decreases when holders redeem. The protocol enforces that `claimToken.totalSupply() == series.backing` as an invariant.
</ParamField>

## SeriesId Derivation

The `seriesId` is computed deterministically from the two fields that define a series:

```solidity theme={null}
bytes32 seriesId = keccak256(abi.encode(asset, unlockTime));
```

<Note>
  The series key uses `abi.encode` (not `abi.encodePacked`) and is derived from **only** the asset address and unlock time — not the quote token. This means all deferred fills for the same asset with the same unlock time share a single fungible claim token, regardless of which quote token was used for payment.
</Note>

A convenience function is available on the contract:

```solidity theme={null}
function seriesIdFor(address asset, uint64 unlockTime) external pure returns (bytes32);
```

## OfferId

Offer IDs are **sequential unsigned integers** (`uint256`), assigned from a `nextOfferId` counter that starts at `0` and increments by one for each new offer. This means:

* The first offer created on the venue has `offerId = 0`, the second has `offerId = 1`, and so on.
* You can enumerate all offers by iterating from `0` to `nextOfferId - 1`.
* You can predict the next offer ID by reading the `nextOfferId` public variable.

```solidity theme={null}
uint256 public nextOfferId;
```

## Price Computation

The `pricePerUnit` field represents the quote-token cost per one whole unit of the asset token (i.e., per `10^assetDecimals` base units). When computing the quote cost for a fill, the contract uses a **cumulative** pricing approach with full-precision integer arithmetic:

```solidity theme={null}
uint256 newFilled = assetFilled + assetUnits;
uint256 cumulativeQuote = Math.mulDiv(newFilled, pricePerUnit, 10 ** assetDecimals);
uint256 quoteCost = cumulativeQuote - quotePaid;
```

This cumulative approach ensures that split fills produce exactly the same total quote cost as a single fill of the same total size — there is no rounding drift regardless of how many partial fills occur or in what order.

<Note>
  The venue provides a view function `quoteCostFor(offerId, assetUnits)` that computes the exact quote cost for a prospective fill without sending a transaction. Use this to preview costs before submitting fills.
</Note>

### Fee Calculation

The protocol fee is computed on the quote cost and paid by the buyer on top of the seller's proceeds:

```solidity theme={null}
uint256 fee = Math.mulDiv(quoteCost, feeBps, 10_000);
```

The buyer's total outlay is `quoteCost + fee` (plus any transfer fees if either token is fee-on-transfer). The fee is transferred directly to the offer's snapshotted `feeRecipient`.
