#46210 [SC-Insight] Incorrect timestamp comparison in function "beforeCollateralWithdrawal" allows agent to withdraw at last second without being challenged
Submitted on May 26th 2025 at 17:08:55 UTC by @dldLambda for Audit Comp | Flare | FAssets
Report ID: #46210
Report Type: Smart Contract
Report severity: Insight
Target: https://github.com/flare-foundation/fassets/blob/main/contracts/assetManager/library/AgentsExternal.sol
Impacts:
Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield
Description
Brief/Intro
A logic flaw in the withdrawal mechanism allows an agent to finalize a withdrawal without accounting for valid challenges submitted in the final second before the allowed withdrawal timestamp. In particular, due to the use of a non-strict inequality (>=) in the line "require(block.timestamp >= withdrawal.allowedAt, "withdrawal: not allowed yet");" , the last second is unintentionally included in the range allowed for output, which is incorrect.
Vulnerability Details
The bug is contained in the following lines (function "beforeCollateralWithdrawal"):
require(block.timestamp >= withdrawal.allowedAt, "withdrawal: not allowed yet");
In function "announceWithdrawal" variable "withdrawal.allowedAt" is created as follows:
withdrawal.allowedAt = (block.timestamp + settings.withdrawalWaitMinSeconds).toUint64();
This is what is meant by "settings.withdrawalWaitMinSeconds" - this is the period of time during which the agent must wait before he can withdraw funds, this is necessary so that other participants have time to challenge his actions if this agent is malicious:
// Agent has to announce any collateral withdrawal ar vault destroy and then wait for at least
// withdrawalWaitMinSeconds. This prevents challenged agent to remove all collateral before
// challenge can be proved.
// rate-limited
uint64 withdrawalWaitMinSeconds;
The (>=) sign is incorrect - because it includes the last second, when there is a possibility of a challenge, in the allowed range for the agent - a potentially malicious agent can steal a lot of funds if someone tries to stop it at the last second.
Why is this a bug?
Because logically we should wait for the period to expire completely before we can say that the period has expired and some actions are allowed.
This means that we should not do require(block.timestamp >= withdrawal.allowedAt, "withdrawal: not allowed yet"), instead we should do require(block.timestamp > withdrawal.allowedAt, "withdrawal: not allowed yet");.
One might say that this is the intended behavior, however in another function ("announceDestroy") the identical check is used correctly:
uint256 destroyAllowedAt = block.timestamp + settings.withdrawalWaitMinSeconds;
agent.destroyAllowedAt = destroyAllowedAt.toUint64();
and later (in function "destroyAgent")
require(block.timestamp > agent.destroyAllowedAt, "destroy: not allowed yet");
Simple example:
Let the action be allowed to be performed within 1 minute from 14.00.00 to 14.01.00. This means that the first moment when the term expires will be 14.01.01, that is, the next second after that. But according to the logic of the current function, this is not so.
Impact Details
Direct evasion of penalties: Agents can withdraw before valid challenges are finalized.
Bypassing slashing logic: Slashing or fraud proofs become ineffective in last second.
Economic loss: Collateral that should be slashed or penalized is instead withdrawn.
Security model broken: Disputes become unreliable when submitted near the end of the timeout.
References
https://github.com/flare-foundation/fassets/blob/fc727ee70a6d36a3d8dec81892d76d01bb22e7f1/contracts/assetManager/library/AgentsExternal.sol#L58 - announceWithdrawal function
https://github.com/flare-foundation/fassets/blob/fc727ee70a6d36a3d8dec81892d76d01bb22e7f1/contracts/assetManager/library/AgentsExternal.sol#L98 - beforeCollateralWithdrawal function
https://github.com/flare-foundation/fassets/blob/fc727ee70a6d36a3d8dec81892d76d01bb22e7f1/contracts/assetManager/library/AgentsCreateDestroy.sol#L123 - announceDestroy function
https://github.com/flare-foundation/fassets/blob/fc727ee70a6d36a3d8dec81892d76d01bb22e7f1/contracts/assetManager/library/AgentsCreateDestroy.sol#L145C14-L145C26 - destroyAgent function
Proof of Concept
Proof of Concept
My test will show the possibility of withdrawal of funds at the last second of the "deadline".
To check, add and run the following test:
it("collateral withdrawal dldLambda check", async () => {
const agent = await Agent.createTest(context, agentOwner1, underlyingAgent1);
const minter = await Minter.createTest(context, minterAddress1, underlyingMinter1, context.underlyingAmount(10000));
const fullAgentCollateral = toWei(3e8);
await agent.depositCollateralsAndMakeAvailable(fullAgentCollateral, fullAgentCollateral);
await context.updateUnderlyingBlock();
const lots = 3;
const crt = await minter.reserveCollateral(agent.vaultAddress, lots);
const txHash = await minter.performMintingPayment(crt);
const minted = await minter.executeMinting(crt, txHash);
assertWeb3Equal(minted.mintedAmountUBA, context.convertLotsToUBA(lots));
const agentInfo = await agent.checkAgentInfo({ totalVaultCollateralWei: fullAgentCollateral, freeUnderlyingBalanceUBA: minted.agentFeeUBA, mintedUBA: minted.mintedAmountUBA.add(minted.poolFeeUBA) });
const minVaultCollateralRatio = agentInfo.mintingVaultCollateralRatioBIPS;
const vaultCollateralPrice = await context.getCollateralPrice(agent.vaultCollateral());
const lockedCollateral = vaultCollateralPrice.convertUBAToTokenWei(agentInfo.mintedUBA).mul(toBN(minVaultCollateralRatio)).divn(MAX_BIPS);
const withdrawalAmount = fullAgentCollateral.sub(lockedCollateral);
await agent.announceVaultCollateralWithdrawal(withdrawalAmount);
await agent.checkAgentInfo({ reservedUBA: 0, redeemingUBA: 0, announcedVaultCollateralWithdrawalWei: withdrawalAmount });
await deterministicTimeIncrease(context.settings.withdrawalWaitMinSeconds); //<----- this will set the time exactly to the last moment
await agent.withdrawVaultCollateral(withdrawalAmount);
});
And you will see that this test is successful:
simple scenarios - agent manipulating collateral and underlying address
✔ collateral withdrawal dldLambda check (91ms)
1 passing (2s)
✨ Done in 5.51s.
(command - yarn testHH --grep "collateral withdrawal dldLambda check")
Was this helpful?