Skip to main content

Claim Withdrawals from Firelight Vault

This guide demonstrates how to claim pending withdrawals from a Firelight vault. The Firelight vault implements the ERC-4626 standard and uses a period-based logic for withdrawals.

Claiming is the final step in the withdrawal process. After you have created a withdrawal request using withdraw or redeem, you must wait for the period to end and then claim your assets.

Prerequisites

Understanding the Claim Process

The Firelight vault uses a period-based withdrawal system with three steps:

  1. Withdrawal Request: When you call withdraw or redeem, it creates a withdrawal request associated with the current period. Your shares are burned at this point.
  2. Period End: The withdrawal is processed after the current period ends.
  3. Claim Withdrawal: Once the period has ended, you must call claimWithdraw function to transfer the assets to your wallet.

Firelight Vault Claim Script

The following script demonstrates how to claim pending withdrawals from the Firelight vault:

scripts/firelight/claim.ts
/**
* FirelightVault Claim Script
*
* This script claims pending withdrawals from the FirelightVault.
* Withdrawals must be requested first using withdraw/redeem, then claimed after the period ends.
*
* Usage:
* yarn hardhat run scripts/firelight/claim.ts --network coston2
*/

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

export const FIRELIGHT_VAULT_ADDRESS =
"0x91Bfe6A68aB035DFebb6A770FFfB748C03C0E40B";

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

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

// Period to claim (0 means auto-detect claimable periods)
const periodToClaim = 0;

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 logClaimInfo(
account: string,
assetAddress: string,
symbol: string,
assetDecimals: number,
) {
console.log("=== Claim Withdrawals (ERC-4626) ===");
console.log("Sender:", account);
console.log("Vault:", FIRELIGHT_VAULT_ADDRESS);
console.log("Asset:", assetAddress, `(${symbol}, decimals=${assetDecimals})`);
}

async function getClaimableAssets(
vault: IFirelightVaultInstance,
period: bigint,
account: string,
): Promise<bigint | null> {
try {
const withdrawals = bnToBigInt(
await vault.withdrawalsOf(period.toString(), account),
);
if (withdrawals === 0n) return null;

const claimableAssets = await vault.claimWithdraw.call(period.toString(), {
from: account,
});
const assets = bnToBigInt(claimableAssets);
return assets > 0n ? assets : null;
} catch {
return null;
}
}

/**
* Checks if a withdrawal for a specific period has been claimed.
*
* @param vault - The FirelightVault instance
* @param period - The period number to check
* @param account - The account address to check
* @returns true if the withdrawal has been claimed (no pending withdrawals), false if still pending
*/
export async function isWithdrawClaimed(
vault: IFirelightVaultInstance,
period: bigint,
account: string,
): Promise<boolean> {
const withdrawals = bnToBigInt(
await vault.withdrawalsOf(period.toString(), account),
);
return withdrawals === 0n;
}

async function findClaimablePeriods(
vault: IFirelightVaultInstance,
account: string,
currentPeriod: bigint,
) {
const claimablePeriods: { period: bigint; claimableAssets: bigint }[] = [];

for (let period = 0n; period < currentPeriod; period++) {
const claimableAssets = await getClaimableAssets(vault, period, account);
if (claimableAssets) {
claimablePeriods.push({ period, claimableAssets });
}
}

return claimablePeriods;
}

async function logPeriodInfo(vault: IFirelightVaultInstance) {
const currentPeriod = bnToBigInt(await vault.currentPeriod());
const currentPeriodEnd = bnToBigInt(await vault.currentPeriodEnd());

console.log("\n=== Period Info ===");
console.log("Current period:", currentPeriod.toString());
console.log("Current period ends:", formatTimestamp(currentPeriodEnd));

return currentPeriod;
}

function logClaimablePeriods(
claimablePeriods: { period: bigint; claimableAssets: bigint }[],
symbol: string,
assetDecimals: number,
) {
console.log("\n=== Claimable Withdrawals ===");

if (claimablePeriods.length === 0) {
console.log("No claimable withdrawals found.");
return;
}

let totalAssets = 0n;
for (const { period, claimableAssets } of claimablePeriods) {
const formattedAssets = (
Number(claimableAssets) / Math.pow(10, assetDecimals)
).toFixed(assetDecimals);
console.log(
`Period ${period.toString()}: ${claimableAssets.toString()} (${formattedAssets} ${symbol})`,
);
totalAssets += claimableAssets;
}

const formattedTotal = (
Number(totalAssets) / Math.pow(10, assetDecimals)
).toFixed(assetDecimals);
console.log(
`Total claimable: ${totalAssets.toString()} (${formattedTotal} ${symbol})`,
);
}

async function executeClaim(period: bigint) {
const [signer] = await ethers.getSigners();
const vaultContract = new ethers.Contract(
FIRELIGHT_VAULT_ADDRESS,
["function claimWithdraw(uint256 period) external returns (uint256)"],
signer,
);

const tx = await vaultContract.claimWithdraw(period.toString());
const receipt = await tx.wait();
console.log(`Claim tx (period ${period.toString()}):`, receipt.hash);
return receipt;
}

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. Log claim info
logClaimInfo(account, assetAddress, symbol, assetDecimals);

// 5. Get current period info
const currentPeriod = await logPeriodInfo(vault);

// 6. Find claimable periods (only past periods can be claimed)
const claimablePeriods = await findClaimablePeriods(
vault,
account,
currentPeriod,
);

// 7. Log claimable periods
logClaimablePeriods(claimablePeriods, symbol, assetDecimals);

// 8. Execute claims
// If a specific period is set, only claim that one
if (periodToClaim > 0) {
const targetPeriod = BigInt(periodToClaim);
const found = claimablePeriods.find((p) => p.period === targetPeriod);
if (found) {
await executeClaim(targetPeriod);
} else {
console.log(`\nPeriod ${periodToClaim} has no claimable withdrawals.`);
}
} else {
// Claim all claimable periods
for (const { period } of claimablePeriods) {
await executeClaim(period);
}
}
}

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. Log claim info: Displays the claim details, including sender and vault addresses.
  5. Get current period info: Retrieves the current period number and when it ends.
  6. Find claimable periods: Scans all past periods to find any with claimable withdrawals.
  7. Log claimable periods: Displays all periods with pending withdrawals and the total claimable amount.
  8. Execute claims: Claims withdrawals for all claimable periods (or a specific period if configured).

Only withdrawals from completed periods can be claimed. The script automatically detects all claimable periods and processes them.

Running the Script

To run the Firelight vault claim 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. Optionally set periodToClaim to claim a specific period (0 means auto-detect all claimable periods).
  4. Run the script using Hardhat:
npx hardhat run scripts/firelight/claim.ts --network coston2

Output

The script outputs the following information:

=== Claim Withdrawals (ERC-4626) ===
Sender: 0x0d09ff7630588E05E2449aBD3dDD1D8d146bc5c2
Vault: 0x91Bfe6A68aB035DFebb6A770FFfB748C03C0E40B
Asset: 0x0b6A3645c240605887a5532109323A3E12273dc7 (FTestXRP, decimals=6)

=== Period Info ===
Current period: 5
Current period ends: 2025-01-15 12:00:00 UTC

=== Claimable Withdrawals ===
Period 3: 1000000 (1.000000 FTestXRP)
Period 4: 500000 (0.500000 FTestXRP)
Total claimable: 1500000 (1.500000 FTestXRP)
Claim tx (period 3): 0x23d8f34b582ef9935d3e3686a15e7fff34417ac01f68f7f928b14e2b4ef10ba9
Claim tx (period 4): 0x3f1bd8c766852d3b835bcde79f6d8e20afeeb227d737e0ed28d057dc0e6b2ba9

Summary

In this guide, you learned how to claim pending withdrawals from a Firelight vault. Remember that claims can only be made after the withdrawal period has ended, and the script can automatically detect and claim all pending withdrawals across multiple periods.

What's next

To continue your Firelight development journey, you can: