58544 sc critical it is possible to underflow on sync making positions bricked forever

Submitted on Nov 3rd 2025 at 07:06:27 UTC by @farismaulana for Audit Comp | Alchemix V3arrow-up-right

  • Report ID: #58544

  • Report Type: Smart Contract

  • Report severity: Critical

  • Target: https://github.com/alchemix-finance/v3-poc/blob/immunefi_audit/src/AlchemistV3.sol

  • Impacts:

    • Permanent freezing of funds

    • Protocol insolvency

Description

Brief/Intro

account.rawLocked is recalculated every time the _sync function invoked. it is crucial for the value to be updated because it is used to calculate how much collateral need to be removed every _sync . however there are no mechanism to make the collateral to remove from this calculation to always cap to account.collateralBalance . given how a combination of poke, redeem and yield token price drop would cause the collateral to remove bigger than account.collateralBalance leading to underflow.

Vulnerability Details

the issue is there are no cap mechanism to prevent the collateralToRemove to be maxed at account.collateralBalance

    function _sync(uint256 tokenId) internal {
        Account storage account = _accounts[tokenId];

        // Collateral to remove from redemptions and fees
@>      uint256 collateralToRemove = PositionDecay.ScaleByWeightDelta(account.rawLocked, _collateralWeight - account.lastCollateralWeight);
@>      account.collateralBalance -= collateralToRemove;
...
        // Update locked collateral
@>      account.rawLocked = convertDebtTokensToYield(account.debt) * minimumCollateralization / FIXED_POINT_SCALAR;

note that the account.rawLocked is recalculated at the end of _sync, this would cause an issue if the yield token price drop, making the account.debt would need more rawLocked amount than before.

but the above are not enough to be an issue. there are second parameter that affect how much collateralToRemove, which is _collateralWeight that is updated on redeem function.

the issue to inflate this collateralToRemove to be bigger than account.collateralBalance can be achieved by:

  1. minimum two redemption position on transmuter

  2. yield token price drop

  3. claim matured redemption → increase system _collateralWeight

  4. poke a tokenId → using updated _collateralWeight remove the collateral, recalculate account.rawLocked using price drop of yield token, this would result in increased rawLocked amount needed.

  5. claim another matured redemption → increase system _collateralWeight

  6. any _sync interaction would revert underflow because now the result of PositionDecay.ScaleByWeightDelta(account.rawLocked, _collateralWeight - account.lastCollateralWeight would be greater than account.collateralBalance

Impact Details

there are multiple impact of this issue:

  1. permanent account freezing

  2. unable to recover bad debt via repay/liquidate (or any function that have _sync )

  3. protocol insolvency, unliquidated bad debt drains value of the entire system. the debt remains and the collateral is frozen while cant be seized.

  4. the positions that bricked cant be recovered even if the price of yield increase, making the issue permanent once happen

References

https://github.com/alchemix-finance/v3-poc/blob/a192ab313c81ba3ab621d9ca1ee000110fbdd1e9/src/AlchemistV3.sol#L1042-L1086

https://github.com/alchemix-finance/v3-poc/blob/a192ab313c81ba3ab621d9ca1ee000110fbdd1e9/src/AlchemistV3.sol#L634

Proof of Concept

Proof of Concept

add the test to src/test/AlchemistV3.t.sol:

run with forge test --mt test_underflowOnSync and the repay call would fail of underflow.

note: we can also simulate yield price became even higher than mint time (2x price increase) by uncomment the total supply mock lines but it would not help the position that is bricked as it would still underflow.

at the end of the test, when _sync is invoked inside repay, the value of collateralToRemove is 1.422e20 while the account.collateralBalance is left with 5e19 .

we can log the value by creating an emit event and emit it inside the _sync for further analysis:

the trace:

Was this helpful?