Skip to main content
Burn claim tokens to receive the underlying collateral on a 1:1 basis. Permissionless — no approval, whitelist, or expiry window.

When Can You Redeem?

Redemption becomes available the moment block.timestamp >= series.unlockTime. You can check this on-chain by reading the series’ unlockTime field. Attempting to call redeem before the unlock time will revert with NotYetUnlocked().

Function Signatures

The venue provides two redemption entry points:

Simple Redemption

function redeem(bytes32 seriesId, uint256 amount) external;
Burns amount claim tokens from msg.sender and transfers the underlying collateral. Applies a default minAssetReceived = 1.

Redemption with Minimum Output

function redeemWithMinimum(
    bytes32 seriesId,
    uint256 amount,
    uint256 minAssetReceived
) external;
Same as redeem, but reverts with SlippageExceeded() if the net asset tokens received by the caller are less than minAssetReceived. Use this when redeeming fee-on-transfer asset tokens.

Parameters

seriesId
bytes32
required
The deterministic series identifier, computed as keccak256(abi.encode(asset, unlockTime)). You can find this from the SeriesCreated event or compute it off-chain using the asset address and unlock time. The venue also provides a helper: seriesIdFor(asset, unlockTime).
amount
uint256
required
The number of claim tokens to burn in this redemption. Must be greater than zero and must not exceed your current claim token balance. You can redeem your full balance in one call or redeem incrementally across multiple calls — partial redemption is fully supported.
minAssetReceived
uint256
The minimum net asset tokens that must arrive in your wallet after any transfer fees. If the delivered amount falls below this threshold, the transaction reverts with SlippageExceeded(). Only available in redeemWithMinimum.

1:1 Redemption

Canopy enforces a strict 1:1 backing ratio between claim tokens and escrowed collateral. When you redeem, exactly amount units of the underlying asset are released from escrow:
series.backing -= amount;
The claim tokens are burned and the corresponding collateral is transferred directly to your wallet. For standard ERC-20 asset tokens, you receive exactly amount tokens. For fee-on-transfer tokens, you receive amount minus the transfer fee — use redeemWithMinimum to protect against this.
This is not a proportional share calculation — it is a direct 1:1 exchange. One claim token always redeems for one unit of the underlying asset (minus any outbound transfer fee on the asset token itself).

Events

event Redeemed(
    bytes32 indexed seriesId,
    address indexed holder,
    uint256 amount,
    uint256 assetReceived
);
assetReceived is the net quantity of underlying collateral transferred to the redeemer. For standard tokens this equals amount; for fee-on-transfer tokens it may be less.

Revert Conditions

ConditionError
series.claimToken == address(0) (series does not exist)SeriesUnknown()
block.timestamp < series.unlockTimeNotYetUnlocked()
amount == 0InvalidParams()
Caller does not hold enough claim tokensERC-20 burn revert
Net asset received < minAssetReceivedSlippageExceeded()

Example: Checking Unlock Status and Redeeming

import { ethers } from "ethers";

const VENUE_ADDRESS = "0xYourVenueAddress";
const venue = new ethers.Contract(VENUE_ADDRESS, VENUE_ABI, signer);

// Compute seriesId
const seriesId = await venue.seriesIdFor(ASSET_ADDRESS, unlockTime);

// Step 1: Check that the series has unlocked
const seriesData = await venue.series(seriesId);
const now = Math.floor(Date.now() / 1000);

if (now < Number(seriesData.unlockTime)) {
  throw new Error(
    `Not yet unlocked. Unlocks at ${new Date(Number(seriesData.unlockTime) * 1000).toISOString()}`
  );
}

// Step 2: Read your claim token balance
const claimToken = new ethers.Contract(seriesData.claimToken, ERC20_ABI, signer);
const myAddress = await signer.getAddress();
const balance = await claimToken.balanceOf(myAddress);
if (balance === 0n) {
  throw new Error("No claim tokens to redeem.");
}

// Step 3: Redeem the full balance
const tx = await venue.redeem(seriesId, balance);
const receipt = await tx.wait();
console.log("Redeemed in tx:", receipt.hash);
import { ethers } from "ethers";

const venue = new ethers.Contract(VENUE_ADDRESS, VENUE_ABI, signer);
const seriesId = await venue.seriesIdFor(ASSET_ADDRESS, unlockTime);

const claimToken = new ethers.Contract(CLAIM_TOKEN_ADDRESS, ERC20_ABI, signer);
const balance = await claimToken.balanceOf(myAddress);
const redeemAmount = balance / 2n; // redeem half your position

// Use redeemWithMinimum for fee-on-transfer asset tokens
const minExpected = redeemAmount * 99n / 100n; // allow 1% fee
const tx = await venue.redeemWithMinimum(seriesId, redeemAmount, minExpected);
await tx.wait();
// Remaining half of claim tokens stay in your wallet for later redemption

Finding the SeriesId and Claim Token Address

You can find the claim token address and series information using these methods:
  1. From the contract — Call seriesIdFor(asset, unlockTime) to compute the series ID, then series(seriesId) to read the claimToken address.
  2. From the event log — Listen for SeriesCreated events. The event includes both the seriesId and the deployed claimToken address.
  3. Off-chain computation — Compute keccak256(abi.encode(asset, unlockTime)) using ethers.js: ethers.solidityPackedKeccak256(["address", "uint64"], [asset, unlockTime]).
If you transferred some or all of your claim tokens to another wallet before the unlock time, only the tokens currently held in msg.sender’s wallet can be redeemed in that call. The receiving wallet must call redeem itself to convert those tokens into collateral. Claim tokens are fully portable ERC-20 tokens — whoever holds them at redemption time receives the collateral.