> ## 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.

# Creating Offers: Collateral, Pricing, and Parameters

Approve the asset token, then call `createOffer` to escrow collateral and register the offer atomically.

## Prerequisites

Before calling `createOffer`, you must grant the OTCVenue contract an ERC-20 allowance for at least `assetAmount` of the token you intend to sell. Without this approval the contract's `transferFrom` call will revert.

```solidity theme={null}
IERC20(asset).approve(VENUE_ADDRESS, assetAmount);
```

<Note>
  For fee-on-transfer tokens, approve the **nominal** amount you want to deposit. The venue measures the actual balance change after the transfer and escrows only what it receives. The `assetAmount` you pass sets the ceiling, not the guaranteed escrow balance.
</Note>

## Function Signature

```solidity theme={null}
function createOffer(
    address asset,
    address quote,
    uint256 assetAmount,
    uint256 pricePerUnit,
    uint64 expiry,
    uint64 unlockTime,
    address allowedBuyer
) external returns (uint256 offerId);
```

## Parameters

<ParamField path="asset" type="address" required>
  The ERC-20 token you are selling. This token must be approved for transfer to the venue contract before calling `createOffer`. Cannot be `address(0)` and cannot be the same as `quote`.
</ParamField>

<ParamField path="quote" type="address" required>
  The ERC-20 token you will accept as payment. Buyers must hold and approve this token to fill your offer. Cannot be `address(0)`.
</ParamField>

<ParamField path="assetAmount" type="uint256" required>
  The total amount of collateral to deposit, in the asset token's native decimals. Must be greater than zero. For standard ERC-20 tokens this equals the amount escrowed. For fee-on-transfer tokens, the actual escrowed amount may be less because the contract records the net balance received.
</ParamField>

<ParamField path="pricePerUnit" type="uint256" required>
  The quote-token cost per one whole unit of the asset token. One "whole unit" equals `10^assetDecimals` base units. Must be greater than zero.

  For example, to sell a token with 18 decimals at a price of 0.43 USDC (6 decimals) per token:

  * One whole unit = `10^18` base units of the asset
  * `pricePerUnit` = `430000` (0.43 × 10^6 USDC base units)

  The quote amount charged for any fill is calculated as:

  ```
  quoteCost = mulDiv(assetUnits, pricePerUnit, 10^assetDecimals)
  ```
</ParamField>

<ParamField path="expiry" type="uint64" required>
  Unix timestamp after which the offer can no longer be filled. Must be in the future (`expiry > block.timestamp`). For deferred offers, `expiry` must also be less than or equal to `unlockTime`.
</ParamField>

<ParamField path="unlockTime" type="uint64" required>
  Controls the settlement mode of the offer:

  * **`0`** — Spot offer. Assets transfer directly to the buyer at fill time.
  * **Non-zero Unix timestamp** — 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 the unlock timestamp. Must be in the future and must satisfy `expiry <= unlockTime`.
</ParamField>

<ParamField path="allowedBuyer" type="address" required>
  Controls who can fill the offer:

  * **`address(0)`** — Public offer; any address may fill.
  * **Specific address** — Private bilateral offer; only that address may fill.
</ParamField>

## Return Value

<ResponseField name="offerId" type="uint256">
  A sequential identifier for the newly created offer, assigned from the venue's `nextOfferId` counter. Store this value — you will need it to fill, query, or cancel the offer.
</ResponseField>

## Events

The venue emits the following event upon successful offer creation:

```solidity theme={null}
event OfferCreated(
    uint256 indexed offerId,
    address indexed seller,
    address indexed asset,
    address quote,
    uint256 requestedAssetAmount,
    uint256 escrowedAssetAmount,
    uint256 pricePerUnit,
    uint64 expiry,
    uint64 unlockTime,
    address allowedBuyer
);
```

## Validation Rules

`createOffer` will revert with `InvalidParams()` if any of the following are true:

* `asset` is `address(0)`
* `quote` is `address(0)`
* `asset == quote`
* `assetAmount == 0`
* `pricePerUnit == 0`
* `expiry <= block.timestamp` (expired or current block)
* `unlockTime != 0` and `unlockTime <= block.timestamp` (deferred but already past)
* `unlockTime != 0` and `expiry > unlockTime` (fills accepted after unlock)
* Asset token's `decimals() > 77`

Additionally, the call reverts with `ZeroReceived()` if the actual amount received by the venue after the transfer is zero.

## Example: Creating a Public Spot Offer

The following TypeScript snippet uses ethers.js v6 to approve collateral and create a public spot offer selling 500,000 TECH tokens at 0.43 USDC each.

<CodeGroup>
  ```typescript ethers.js v6 theme={null}
  import { ethers } from "ethers";

  const VENUE_ADDRESS = "0xYourVenueAddress";
  const TECH_ADDRESS  = "0xTechTokenAddress";
  const USDC_ADDRESS  = "0xUsdcTokenAddress";

  const assetAmount = ethers.parseUnits("500000", 18);

  // Price: 0.43 USDC per token. USDC has 6 decimals, so:
  // pricePerUnit = 0.43 * 10^6 = 430000
  const pricePerUnit = 430000n;

  // Expiry: 7 days from now
  const expiry = BigInt(Math.floor(Date.now() / 1000) + 7 * 86400);

  // Step 1: Approve collateral transfer
  const assetToken = new ethers.Contract(TECH_ADDRESS, ERC20_ABI, signer);
  await assetToken.approve(VENUE_ADDRESS, assetAmount);

  // Step 2: Create a public spot offer
  const venue = new ethers.Contract(VENUE_ADDRESS, VENUE_ABI, signer);
  const tx = await venue.createOffer(
    TECH_ADDRESS,                    // asset
    USDC_ADDRESS,                    // quote
    assetAmount,                     // assetAmount
    pricePerUnit,                    // pricePerUnit (0.43 USDC)
    expiry,                          // expiry: 7 days from now
    0n,                              // unlockTime: 0 = immediate spot
    ethers.ZeroAddress               // allowedBuyer: open to everyone
  );

  const receipt = await tx.wait();
  console.log("Offer created. Transaction:", receipt.hash);
  ```

  ```typescript Private Bilateral Offer theme={null}
  import { ethers } from "ethers";

  const BUYER_ADDRESS = "0xBuyerWalletAddress";

  // Same approve step as above, then:
  const tx = await venue.createOffer(
    TECH_ADDRESS,
    USDC_ADDRESS,
    ethers.parseUnits("500000", 18),
    430000n,                           // pricePerUnit
    BigInt(Math.floor(Date.now() / 1000) + 7 * 86400), // expiry
    0n,                                // unlockTime: spot
    BUYER_ADDRESS                      // allowedBuyer: only this address can fill
  );
  const receipt = await tx.wait();
  ```

  ```typescript Deferred Forward Offer theme={null}
  import { ethers } from "ethers";

  const now = Math.floor(Date.now() / 1000);
  const expiry = BigInt(now + 30 * 86400);      // accept fills for 30 days
  const unlockTime = BigInt(now + 90 * 86400);   // claims redeemable after 90 days

  const tx = await venue.createOffer(
    TECH_ADDRESS,
    USDC_ADDRESS,
    ethers.parseUnits("500000", 18),
    430000n,
    expiry,
    unlockTime,                       // deferred: claims unlock in 90 days
    ethers.ZeroAddress                // public
  );
  const receipt = await tx.wait();
  ```
</CodeGroup>

<Tip>
  For private bilateral offers, set `allowedBuyer` to the buyer's wallet address instead of `address(0)`. Only that address will be able to call `fill` on your offer — all other callers will revert with `NotAllowedBuyer()`.
</Tip>

<Warning>
  Once `createOffer` succeeds, the collateral is escrowed in the venue contract and is no longer in your wallet. You can only recover it by calling `cancel`, which returns the **unfilled portion** of the escrow. Any portion that has already been filled is settled and cannot be reversed.
</Warning>

<Note>
  The fee rate and fee recipient are snapshotted into the offer at creation time. Even if the admin changes the venue's fee rate later, your offer's fills will always use the rate that was active when you created the offer.
</Note>
