Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield
Description
Brief/Intro
When a user invokes selfCloseExitTo, the calculation of requiredFAssets is non-trivial, as it depends on the current collateral ratio (CR). For simplicity, let’s assume the current CR is below the healthy threshold. In this case, the formula for requiredFAssets becomes:
The agentBackedFAsset is composed of reservedAMG, mintedAMG, and poolRedeemingAMG.
Importantly, any user can call CollateralReservationsFacet.sol::reserveCollateral() to increase the value of reservedAMG. Since agentBackedFAsset includes reservedAMG, this operation directly increases the agentBackedFAsset value.
As a result, the required amount of f-assets for selfCloseExitTo also increases, potentially making the operation more expensive and allowing malicious actors to exploit the mechanism by inflating the cost for other users.
Vulnerability Details
Firstly CollateralReservationsFacet.sol::reserveCollateral() can increase the value of reservedAMG reserveCollateral()->_reserveCollateral():
And from the selfCloseExitTo()->_selfCloseExitTo()->_getFAssetRequiredToNotSpoilCR():
requiredFAssets is depends on _assetData.agentBackedFAsset
Due to agentBackedFAsset is fetched from assetManager::getFAssetsBackedByPool(agent)->AgentsExternal.getFAssetsBackedByPool():
Impact Details
If user's account can't afford the required fAssets , user have to transfer more fAssets to the protocol. If the max cost fAssets is not checked user may cost more fAssets than expected.
References
Proof of Concept
Proof of Concept
Add the following test to file CollateralPool.ts
Why do i use fAsset.mint ? Cuz from the AssetManagerMock.sol the getFAssetsBackedByPool is total supply :
Add or comment fAsset.mint we can see the output difference is :
function _reserveCollateral(
Agent.State storage _agent,
uint64 _reservationAMG
)
private
{
AssetManagerState.State storage state = AssetManagerState.get();
Minting.checkMintingCap(_reservationAMG); //@audit-info if minting cap is reached, revert
_agent.reservedAMG += _reservationAMG;
state.totalReservedCollateralAMG += _reservationAMG;
}
} else {
// f-asset that preserves pool CR (assume poolNatBalance >= natShare > 0)
// solve (N - n) / (F - f) = N / F get n = N f / F
return _assetData.agentBackedFAsset.mulDiv(_natShare, _assetData.poolNatBalance);
}