Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 70 additions & 1 deletion contracts/FirelightVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,18 @@ import {Checkpoints} from "./lib/Checkpoints.sol";

/**
* @title FirelightVault
* @notice Upgradeable ERC4626-compatible vault
* @notice Upgradeable ERC4626-compatible vault with delayed withdrawals.
*
* @dev FirelightVault is an ERC4626 vault that intentionally deviates from the standard.
* It overrides `withdraw` and `redeem` to implement delayed withdrawals.
* Instead of transferring assets immediately, these functions create a withdrawal request,
* which must be completed later via `claimWithdraw` after a set delay.
*
* The standard `Withdraw` event is not emitted. Instead, `WithdrawRequest` and `CompleteWithdraw`
* are used to track the withdrawal process.
*
* Off-chain and on-chain tools must account for this custom flow and event structure.
*
* @custom:security-contact securityreport@firelight.finance
*/
contract FirelightVault is
Expand Down Expand Up @@ -292,6 +303,64 @@ contract FirelightVault is
return currentEnd + periodConfigurationAtTimestamp(currentEnd).duration;
}

/**
* @notice Returns the maximum amount of the underlying asset that can be deposited into the Vault for the receiver,
* through a deposit call.
* @param receiver The address of the deposit receiver.
* @return amount Maximum amount of assets that can be deposited.
*/
function maxDeposit(address receiver) public view override returns (uint256 amount) {
uint256 assets = totalAssets();
if (isBlocklisted[receiver] || paused() || assets > depositLimit) {
return 0;
} else {
return depositLimit - assets;
}
}

/**
* @notice Returns the maximum amount of the Vault shares that can be minted for the receiver, through a mint call.
* @param receiver The address of the mint receiver.
* @return amount Maximum amount of shares that can be minted.
*/
function maxMint(address receiver) public view override returns (uint256 amount) {
uint256 shares = totalSupply();
uint256 sharesLimit = convertToShares(depositLimit);
if (isBlocklisted[receiver] || paused() || shares > sharesLimit) {
return 0;
} else {
return sharesLimit - shares;
}
}

/**
* @notice Returns the maximum amount of the underlying asset that can be withdrawn from the owner balance in the
* Vault, through a withdraw call.
* @param owner The owner of the assets.
* @return amount Maximum amount of assets that can be withdrawn.
*/
function maxWithdraw(address owner) public view override returns (uint256 amount) {
if (isBlocklisted[owner] || paused()) {
return 0;
} else {
return _convertToAssets(balanceOf(owner), Math.Rounding.Floor);
}
}

/**
* @notice Returns the maximum amount of Vault shares that can be redeemed from the owner balance in the Vault,
* through a redeem call.
* @param owner The owner of the shares.
* @param amount Maximum amount of shares that can be redeemed.
*/
function maxRedeem(address owner) public view override returns (uint256 amount) {
if (isBlocklisted[owner] || paused()) {
return 0;
} else {
return balanceOf(owner);
}
}

/**
* @notice Returns the total assets in the vault excluding those marked for withdrawal.
* @return The total assets held by the vault.
Expand Down
16 changes: 14 additions & 2 deletions test/blocklist.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,18 @@ describe('Blocklist test', function() {
expect(status).to.equal(true)
})

it('returns correct values for maxDeposit, maxMint, maxWithdraw and maxRedeem if the user is blocklisted', async () => {
const max_deposit = await firelight_vault.maxDeposit(users[0].address),
max_mint = await firelight_vault.maxMint(users[0].address),
max_withdraw = await firelight_vault.maxWithdraw(users[0].address),
max_redeem = await firelight_vault.maxRedeem(users[0].address)

expect(max_deposit).to.be.equal(0n)
expect(max_mint).to.be.equal(0n)
expect(max_withdraw).to.be.equal(0n)
expect(max_redeem).to.be.equal(0n)
})

it('reverts if blocklister tries to blocklist a user that is already blocklisted', async () => {
const blocklist = firelight_vault.connect(blocklister).addToBlocklist(users[0].address)
await expect(blocklist).to.be.revertedWithCustomError(firelight_vault, 'BlocklistedAddress')
Expand Down Expand Up @@ -86,12 +98,12 @@ describe('Blocklist test', function() {
await expect(deposit_attempt).to.be.revertedWithCustomError(firelight_vault, 'BlocklistedAddress')
})

it('reverts if a user attempts to redem from a blocklisted user', async () => {
it('reverts if a user attempts to redeem from a blocklisted user', async () => {
const redeem_attempt = firelight_vault.connect(users[1]).redeem(DEPOSIT_AMOUNT, users[1].address, users[0].address)
await expect(redeem_attempt).to.be.revertedWithCustomError(firelight_vault, 'BlocklistedAddress')
})

it('reverts if a user attempts to redem to a blocklisted user', async () => {
it('reverts if a user attempts to redeem to a blocklisted user', async () => {
const redeem_attempt = firelight_vault.connect(users[1]).redeem(DEPOSIT_AMOUNT, users[0].address, users[2].address)
await expect(redeem_attempt).to.be.revertedWithCustomError(firelight_vault, 'BlocklistedAddress')
})
Expand Down
59 changes: 55 additions & 4 deletions test/deposit_and_withdraw.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,24 +35,40 @@ describe('Deposit and Withdraw test', function() {
expect(deposit_limit.toString()).to.equal(TARGET_DEPOSIT_LIMIT)
})

it('returns correct values for maxDeposit and maxMint', async () => {
const max_deposit = await firelight_vault.maxDeposit(users[0].address),
shares_preview = await firelight_vault.previewDeposit(max_deposit),
max_mint = await firelight_vault.maxMint(users[0].address)

expect(max_deposit).to.be.equal(TARGET_DEPOSIT_LIMIT)
expect(max_mint).to.be.equal(shares_preview)
})

it('deposits tokens and receives the expected amount of shares', async () => {
const shares_preview = await firelight_vault.previewDeposit(DEPOSIT_AMOUNT),
depositTrx = firelight_vault.connect(users[0]).deposit(DEPOSIT_AMOUNT, users[0])
deposit_tx = firelight_vault.connect(users[0]).deposit(DEPOSIT_AMOUNT, users[0])

await expect(depositTrx).to.emit(firelight_vault, 'Deposit').withArgs(
await expect(deposit_tx).to.emit(firelight_vault, 'Deposit').withArgs(
users[0].address, users[0].address, DEPOSIT_AMOUNT, shares_preview
)

const shares = await firelight_vault.balanceOf(users[0].address)
expect(shares.toString()).to.equal(DEPOSIT_AMOUNT)

const max_deposit = await firelight_vault.maxDeposit(users[0].address),
max_deposit_shares = await firelight_vault.previewDeposit(max_deposit),
max_mint = await firelight_vault.maxMint(users[0].address)

expect(max_deposit).to.be.equal(TARGET_DEPOSIT_LIMIT - DEPOSIT_AMOUNT)
expect(max_mint).to.be.equal(max_deposit_shares)
})

it('mints shares and deducts the expected amount of tokens', async () => {
const prev_token_bal = await token_contract.balanceOf(users[1]),
assets_preview = await firelight_vault.previewMint(DEPOSIT_AMOUNT),
mintedTrx = firelight_vault.connect(users[1]).mint(DEPOSIT_AMOUNT, users[1])
mint_tx = firelight_vault.connect(users[1]).mint(DEPOSIT_AMOUNT, users[1])

await expect(mintedTrx).to.emit(firelight_vault, 'Deposit').withArgs(
await expect(mint_tx).to.emit(firelight_vault, 'Deposit').withArgs(
users[1].address, users[1].address, assets_preview, DEPOSIT_AMOUNT
)

Expand All @@ -61,6 +77,13 @@ describe('Deposit and Withdraw test', function() {

const assets = await token_contract.balanceOf(users[1].address)
expect(assets).to.equal(prev_token_bal - DEPOSIT_AMOUNT)

const max_deposit = await firelight_vault.maxDeposit(users[0].address),
max_deposit_shares = await firelight_vault.previewDeposit(max_deposit),
max_mint = await firelight_vault.maxMint(users[0].address)

expect(max_deposit).to.be.equal(TARGET_DEPOSIT_LIMIT - DEPOSIT_AMOUNT * 2n)
expect(max_mint).to.be.equal(max_deposit_shares)
})

it('reverts when user tries to request withdraw with more than what it owns', async () => {
Expand All @@ -73,12 +96,27 @@ describe('Deposit and Withdraw test', function() {
await expect(withdraw_request).to.be.revertedWithCustomError(firelight_vault, 'InsufficientShares')
})

it('returns correct values for maxWithdraw and maxRedeem', async () => {
const max_withdraw = await firelight_vault.connect(users[0]).maxWithdraw(users[0].address),
max_withdraw_shares = await firelight_vault.connect(users[0]).previewWithdraw(max_withdraw),
max_redeem = await firelight_vault.connect(users[0]).maxRedeem(users[0].address)

expect(max_withdraw).to.be.equal(DEPOSIT_AMOUNT)
expect(max_redeem).to.be.equal(max_withdraw_shares)
})

it('reverts when trying to complete the withdraw before the next period', async() => {
const receipt = await (await firelight_vault.connect(users[0]).withdraw(DEPOSIT_AMOUNT, users[0].address, users[0].address)).wait()
withdraw_period = receipt.logs[1].args[3]

const withdraw_attempt = firelight_vault.connect(users[0]).claimWithdraw(withdraw_period)
await expect(withdraw_attempt).to.be.revertedWithCustomError(firelight_vault, 'InvalidPeriod')

const max_withdraw = await firelight_vault.connect(users[0]).maxWithdraw(users[0].address),
max_redeem = await firelight_vault.connect(users[0]).maxRedeem(users[0].address)

expect(max_withdraw).to.be.equal(0n)
expect(max_redeem).to.be.equal(0n)
})

it('reads the user\'s pending withdrawals', async () => {
Expand Down Expand Up @@ -112,4 +150,17 @@ describe('Deposit and Withdraw test', function() {
const complete_withdraw = firelight_vault.connect(users[0]).claimWithdraw(withdraw_period + 1n)
await expect(complete_withdraw).to.be.revertedWithCustomError(firelight_vault, 'NoWithdrawalAmount')
})

it('decreases the deposit limit below total value', async () => {
await firelight_vault.connect(limit_updater).updateDepositLimit(INITIAL_DEPOSIT_LIMIT)

const deposit_limit = await firelight_vault.depositLimit()
expect(deposit_limit.toString()).to.equal(INITIAL_DEPOSIT_LIMIT)

const max_deposit = await firelight_vault.maxDeposit(users[0].address),
max_mint = await firelight_vault.maxMint(users[0].address)

expect(max_deposit).to.be.equal(0n)
expect(max_mint).to.be.equal(0n)
})
})
12 changes: 12 additions & 0 deletions test/pause.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@ describe('Pause test', function() {
expect(await firelight_vault.paused()).to.equal(true)
})

it('returns correct values for maxDeposit, maxMint, maxWithdraw and maxRedeem when the contract is paused', async () => {
const max_deposit = await firelight_vault.maxDeposit(users[0].address),
max_mint = await firelight_vault.maxMint(users[0].address),
max_withdraw = await firelight_vault.maxWithdraw(users[0].address),
max_redeem = await firelight_vault.maxRedeem(users[0].address)

expect(max_deposit).to.be.equal(0n)
expect(max_mint).to.be.equal(0n)
expect(max_withdraw).to.be.equal(0n)
expect(max_redeem).to.be.equal(0n)
})

it('reverts when trying to deposit if the contract is paused', async () => {
const deposit_attempt = firelight_vault.connect(users[0]).deposit(DEPOSIT_AMOUNT / 2n, users[0].address)
await expect(deposit_attempt).to.be.revertedWithCustomError(firelight_vault, 'EnforcedPause')
Expand Down