#47108 [SC-High] selfCloseExitTo() can cause users to receive partial payments without validation, leading to permanent asset loss

Submitted on Jun 9th 2025 at 01:55:29 UTC by @rilwan99 for Audit Comp | Flare | FAssets

  • Report ID: #47108

  • Report Type: Smart Contract

  • Report severity: High

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

  • Impacts:

    • Permanent freezing of unclaimed yield

Description

Brief/Intro

The selfCloseExitTo() function in CollateralPool.sol allows users to exit the pool and redeem FAssets for agent vault collateral, but fails to validate that the agent vault has sufficient collateral balance before proceeding. When the agent vault has insufficient funds, users receive only partial payments while their CPT tokens are fully burned, resulting in permanent and unrecoverable asset loss with no transaction revert or compensation mechanism.

Vulnerability Details

The vulnerability exists in the _selfCloseExitTo() function when users choose to redeem FAssets for agent vault collateral (_redeemToCollateral = true). The function calls assetManager.redeemFromAgentInCollateral() without validating the agent vault's collateral balance:

// In CollateralPool.sol _selfCloseExitTo()
if (requiredFAssets < assetManager.lotSize() || _redeemToCollateral) {
    // @audit BUG No check on agent vault balance -> Can lead to user receiving a loss
    assetManager.redeemFromAgentInCollateral(
        agentVault, _recipient, requiredFAssets);
}

This leads to a call chain: _selfCloseExitTo()RedemptionRequests.redeemFromAgentInCollateral()Agents.payoutFromVault(), where the critical flaw occurs:

// In Agents.sol payoutFromVault()
function payoutFromVault(
    Agent.State storage _agent,
    address _receiver,
    uint256 _amountWei
) internal returns (uint256 _amountPaid) {
    CollateralTypeInt.Data storage collateral = getVaultCollateral(_agent);
    // @audit This allows partial payments without reverting
    _amountPaid = Math.min(_amountWei, collateral.token.balanceOf(address(vault)));
    vault.payout(collateral.token, _receiver, _amountPaid);
}

The Math.min() logic ensures that if the vault has insufficient collateral, only the available amount is paid out, with no revert or compensation mechanism. The user's CPT tokens are still burned via token.burn(msg.sender, _tokenShare, false) regardless of whether they received their full entitlement.

Contrast with underlying asset redemption: The code properly handles insufficient capacity for underlying asset redemptions by scaling down the entire operation proportionally and emitting an IncompleteSelfCloseExit event. However, no such protection exists for vault collateral redemptions.

// Proper handling for underlying assets
uint256 maxAgentRedemption = assetManager.maxRedemptionFromAgent(agentVault);
if (maxAgentRedemption < requiredFAssets) {
    requiredFAssets = maxAgentRedemption;
    natShare = _getNatRequiredToNotSpoilCR(assetData, requiredFAssets);
    _tokenShare = assetData.poolTokenSupply.mulDiv(natShare, assetData.poolNatBalance);
    emit IncompleteSelfCloseExit(_tokenShare, requiredFAssets);
}

Impact Details

Severity: High - Direct permanent asset loss for users

Financial Impact:

This vulnerability results in permanent loss of funds for CPT holders. The maximum loss per transaction is capped at the difference between the value of FAssets being redeemed and the agent vault's available collateral balance.

Permanent Nature of Loss:

Once CPT tokens are burned, they cannot be recovered, leaving users with no means of compensation or protection. The burned tokens represent the user's proportional ownership in the collateral pool, and their destruction is irreversible regardless of whether the user received their full entitlement. Protocol Responsibility:

This is NOT a user error. The responsibility lies with the protocol to ensure that when users' CPT tokens are burned, they receive equivalent value in return. The protocol should either:

  • Validate sufficient agent vault balance before proceeding, or

  • Scale down the CPT token burn proportionally to match the available payout

References

https://github.com/flare-labs-ltd/fassets/blob/main/contracts/assetManager/library/Agents.sol

Proof of Concept

Proof of Concept

Assume the following: Collateral Pool State:

  • Total collateral: 500 NAT

  • Total CPT supply: 100 tokens

  • Alice holds 50 CPT tokens (50% of pool)

  • Pool has accumulated 50 FAssets in fees

  • Alice has earned 10 FAssets in fees

Market Conditions:

  • NAT (FLR) price: $2

  • FAsset (XRP) price: $3

  • USDC price: $1

Agent State:

  • Agent is backing 300 FAssets ($900 total value)

  • Agent vault contains: 400 USDC ($400)

  • Pool collateral: 500 NAT ($1,000)

Collateral Ratios (Both Below Required):

  • Vault CR: $400 USDC / $900 FAssets = 0.44 (Below required 1.5)

  • Pool CR: $1,000 NAT / $900 FAssets = 1.11 (Below required 2.0)

Step 1: Alice initiates selfCloseExitTo

  • Alice calls selfCloseExitTo() to exit with her 50 CPT tokens (50% of entire pool)

  • She sets _redeemToCollateral = true (wants USDC from agent vault)

Step 2: Protocol calculates FAsset redemption requirement

  • Alice's exit would remove 250 NAT ($500) from pool

  • New pool collateral after Alice's exit: 250 NAT ($500)

  • To maintain the same pool CR of 1.11, new FAsset backing should be: $500 / 1.11 = $450

  • Required FAsset redemption: $900 - $450 = $450 worth of FAssets

  • offset by FAsset fees earned by Alice: $450 - $30 = $420

  • Converting to USDC needed: $420 worth of FAssets = 420 USDC required from agent vault

Step 3: Alice receives less than required

  • Alice requires: $420 from agent vault

  • Agent vault contains: Only 400 USDC

  • Shortfall: $420 - $400 = $20 USD deficit

Was this helpful?