Skip to main content

Redeem Assets from Firelight Vault

Overview

This guide demonstrates how to create a redemption request from a Firelight vault. The Firelight vault implements the ERC-4626 standard and uses a period-based logic for redemptions.

Redeeming shares burns vault shares and allows assets to be withdrawn. Redemptions create a withdrawal request that is processed after the current period ends. The assets are not immediately transferred; they must be claimed once the period has ended.

Prerequisites

Firelight Vault Redeem Script

The following script demonstrates how to create a redemption request from the Firelight vault:

scripts/firelight/redeem.ts
/**
* FirelightVault Redeem Script
*
* This script creates a redemption request from the FirelightVault (ERC-4626).
* Redeem burns shares to withdraw assets. Redemptions are delayed and must be claimed after the period ends.
*/

import { ethers } from "hardhat";
import type { IFirelightVaultInstance } from "../../typechain-types/contracts/firelight/IFirelightVault";
import type { ERC20Instance } from "../../typechain-types/@openzeppelin/contracts/token/ERC20/ERC20";
import { bnToBigInt } from "../utils/core";

export const FIRELIGHT_VAULT_ADDRESS =
"0x91Bfe6A68aB035DFebb6A770FFfB748C03C0E40B";

export const IFirelightVault = artifacts.require("IFirelightVault");

const sharesToRedeem = 1; // Number of shares to redeem

// @ts-expect-error - Type definitions issue, but works at runtime
const IERC20 = artifacts.require(
"@openzeppelin/contracts/token/ERC20/ERC20.sol:ERC20",
);

async function getAccount() {
const [signer] = await ethers.getSigners();
return { signer, account: signer.address };
}

async function getVaultAndAsset() {
const vault = (await IFirelightVault.at(
FIRELIGHT_VAULT_ADDRESS,
)) as IFirelightVaultInstance;
const assetAddress = await vault.asset();
const assetToken = (await IERC20.at(assetAddress)) as ERC20Instance;
return { vault, assetAddress, assetToken };
}

async function getAssetInfo(assetToken: ERC20Instance) {
const symbol = await assetToken.symbol();
const assetDecimals = (await assetToken.decimals()).toNumber();
return { symbol, assetDecimals };
}

function logRedeemInfo(
account: string,
assetAddress: string,
symbol: string,
assetDecimals: number,
sharesAmount: bigint,
) {
console.log("=== Redeem (ERC-4626) ===");
console.log("Sender:", account);
console.log("Vault:", FIRELIGHT_VAULT_ADDRESS);
console.log("Asset:", assetAddress, `(${symbol}, decimals=${assetDecimals})`);
console.log(
"Shares to redeem:",
sharesAmount.toString(),
`(= ${sharesToRedeem} share${sharesToRedeem > 1 ? "s" : ""})`,
);
}

async function validateRedeem(
vault: IFirelightVaultInstance,
account: string,
sharesAmount: bigint,
) {
const maxRedeem = bnToBigInt(await vault.maxRedeem(account));
console.log("Max redeem:", maxRedeem);
if (sharesAmount > maxRedeem) {
console.error(
`Cannot redeem ${sharesAmount.toString()} shares. Max allowed: ${maxRedeem.toString()}`,
);
process.exit(1);
}
}

async function checkUserBalance(
vault: IFirelightVaultInstance,
account: string,
sharesAmount: bigint,
assetDecimals: number,
) {
const userBalance = await vault.balanceOf(account);
const formattedUserBalance = (
Number(userBalance.toString()) / Math.pow(10, assetDecimals)
).toFixed(assetDecimals);
console.log(
"User balance (shares):",
userBalance.toString(),
`(= ${formattedUserBalance} shares)`,
);
if (bnToBigInt(userBalance) < sharesAmount) {
console.error(
`Insufficient balance. Need ${sharesAmount.toString()} shares, have ${userBalance.toString()}`,
);
process.exit(1);
}
}

async function executeRedeem(
vault: IFirelightVaultInstance,
sharesAmount: bigint,
account: string,
) {
const redeemTx = await vault.redeem(
sharesAmount.toString(),
account,
account,
{ from: account },
);
console.log("Redeem tx:", redeemTx.tx);
}

async function main() {
// 1. Get the account
const { account } = await getAccount();

// 2. Get the vault and asset token
const { vault, assetAddress, assetToken } = await getVaultAndAsset();

// 3. Get asset info (symbol, decimals)
const { symbol, assetDecimals } = await getAssetInfo(assetToken);

// 4. Calculate the shares amount to redeem
const sharesAmount = BigInt(sharesToRedeem * 10 ** assetDecimals);

// 5. Log redeem info
logRedeemInfo(account, assetAddress, symbol, assetDecimals, sharesAmount);

// 6. Validate the redeem (check max redeem)
await validateRedeem(vault, account, sharesAmount);

// 7. Check user balance
await checkUserBalance(vault, account, sharesAmount, assetDecimals);

// 8. Execute the redemption
await executeRedeem(vault, sharesAmount, account);
}

main().catch((error) => {
console.error(error);
process.exitCode = 1;
});

Script Breakdown

The main() function executes the following steps:

  1. Get the account: Retrieves the signer account from the Hardhat environment.
  2. Get the vault and asset token: Connects to the vault contract and retrieves the underlying asset token.
  3. Get asset info: Fetches the asset token's symbol and decimals.
  4. Calculate the shares amount to redeem: Converts the desired shares into the correct units based on decimals.
  5. Log redeem info: Displays the redemption details, including sender, vault, and shares.
  6. Validate the redeem: Checks if the amount exceeds the maximum allowed.
  7. Check user balance: Verifies the user has sufficient shares.
  8. Execute the redemption: Calls redeem() to create a redemption request for the current period.

Understanding Redemption Process

The Firelight vault uses a period-based redemption system:

  1. Redemption Request: When you call redeem, it creates a redemption request associated with the current period.
  2. Period End: The redemption is processed after the current period ends.
  3. Claim Redemption: Once the period has ended, you must claim the redemption to receive your assets.

This delayed redemption mechanism is part of the vault's period-based logic and helps manage liquidity and ensure fair distribution of rewards.

Running the Script

To run the Firelight vault redeem script:

  1. Ensure you have the Flare Hardhat Starter Kit set up.
  2. Update the FIRELIGHT_VAULT_ADDRESS constant with the correct vault address for your network.
  3. Adjust the SHARES_TO_REDEEM constant to the desired number of shares.
  4. Ensure your account has sufficient vault shares to cover the redemption.
  5. Run the script using Hardhat:
npx hardhat run scripts/firelight/redeem.ts --network coston2

Output

The script outputs the following information:

=== Redeem (ERC-4626) ===
Sender: 0x0d09ff7630588E05E2449aBD3dDD1D8d146bc5c2
Vault: 0x91Bfe6A68aB035DFebb6A770FFfB748C03C0E40B
Asset: 0x0b6A3645c240605887a5532109323A3E12273dc7 (FTestXRP, decimals=6)
Shares to redeem: 1000000 (= 1 share)
Max redeem: 1000061
User balance (shares): 1000061 (= 1.000061 shares)
Redeem tx: 0x3f1bd8c766852d3b835bcde79f6d8e20afeeb227d737e0ed28d057dc0e6b2ba9

Difference Between Redeem and Withdraw

The Firelight vault provides two ways to remove assets:

  • redeem: You specify the number of shares to redeem, and the vault calculates how many assets you'll receive.
  • withdraw: You specify the amount of assets to withdraw, and the vault calculates how many shares need to be burned based on the current exchange rate.

Both functions follow the ERC-4626 standard and create withdrawal requests that must be claimed after the period ends.

Summary

In this guide, you learned how to create a redemption request from a Firelight vault by specifying the number of shares to redeem. Remember that redemptions are delayed and must be claimed after the current period ends.

What's next

To continue your Firelight development journey, you can: