Immunefi Audit Competitions
Alchemix V3

58291 sc medium unlike setters collateralization functions alchemistv3 initialize doesnt enforce collateralization invariants allowing to break them

Submitted on Nov 1st 2025 at 01:26:10 UTC by @hunter0xweb3 for Audit Comp | Alchemix V3

Description

Brief/Intro

AlchemistV3 defines multiple collateralization requirements: MinimumCollateralization, GlobalMinimumCollateralization, CollateralizationLowerBound and a relation between them:

globalMinimumCollateralization >= minimumCollateralization >= collateralizationLowerBound >= FIXED_POINT_SCALAR

This relationship is enforced in the setter functions for this variables, however AlchemistV3::initialize allows to break the invariant and start AlchemistV3 in an invariant breakage state

Vulnerability Details

AlchemistV3 defines variables for multiple collateralization requirements:

  • MinimumCollateralization,
  • GlobalMinimumCollateralization
  • CollateralizationLowerBound And also defines its setter functions
    function setMinimumCollateralization(uint256 value) external onlyAdmin {
@>      _checkArgument(value >= FIXED_POINT_SCALAR);
        minimumCollateralization = value;
        emit MinimumCollateralizationUpdated(value);
    }

    /// @inheritdoc IAlchemistV3AdminActions
    function setGlobalMinimumCollateralization(uint256 value) external onlyAdmin {
@>      _checkArgument(value >= minimumCollateralization);
        globalMinimumCollateralization = value;
        emit GlobalMinimumCollateralizationUpdated(value);
    }

    /// @inheritdoc IAlchemistV3AdminActions
    function setCollateralizationLowerBound(uint256 value) external onlyAdmin {
@>      _checkArgument(value <= minimumCollateralization);
@>      _checkArgument(value >= FIXED_POINT_SCALAR);
        collateralizationLowerBound = value;
        emit CollateralizationLowerBoundUpdated(value);
    }

From the setter function it can be seen that the invariant enforced by AlchemistV3 is the following

globalMinimumCollateralization >= minimumCollateralization >= collateralizationLowerBound >= FIXED_POINT_SCALAR

However this invariant is not enforced in AlchemistV3::initialize allowing to use it to break and start contract in an non compliant state:

    function initialize(AlchemistInitializationParams memory params) external initializer {
        //...snippet...
@>      minimumCollateralization = params.minimumCollateralization;
@>      globalMinimumCollateralization = params.globalMinimumCollateralization;
@>      collateralizationLowerBound = params.collateralizationLowerBound;
        //...snippet... 

Impact Details

  • Collateralization requirements invariant breakage
  • AlchemistV3 is unable to perform liquidations
  • Contract failed to fullfil premises

References

https://github.com/alchemix-finance/v3-poc/blob/a192ab313c81ba3ab621d9ca1ee000110fbdd1e9/src/AlchemistV3.sol#L174-L176\\ https://github.com/alchemix-finance/v3-poc/blob/a192ab313c81ba3ab621d9ca1ee000110fbdd1e9/src/AlchemistV3.sol#L292-L312

Proof of Concept

Proof of Concept

The following PoC shows AlchemistV3::initialize() can be used to break the AlchemistV3 collateralization's invariant:

globalMinimumCollateralization >= minimumCollateralization >= collateralizationLowerBound >= FIXED_POINT_SCALAR

enforced by setMinimumCollateralization, setGlobalMinimumCollateralization, and setCollateralizationLowerBound AlchemistV3's functions

To do so, perform the following:

In deployCoreContracts function in src/test/AlchemistV3.t.sol modify AlchemistInitializationParams struct, only the minimumCollateralization, collateralizationLowerBound and globalMinimumCollateralization fields:

    function deployCoreContracts(uint256 alchemistUnderlyingTokenDecimals) public {
        // ...
        AlchemistInitializationParams memory params = AlchemistInitializationParams({
            admin: alOwner,
            debtToken: address(alToken),
            underlyingToken: address(vault.asset()),
            depositCap: type(uint256).max,
            minimumCollateralization: 2_111_111_111_111_111_111, //2.1
            collateralizationLowerBound: 3_111_111_111_111_111_111, // 3.1 collateralization
            globalMinimumCollateralization: 1_111_111_111_111_111_111, // 1.1

Leave the rest of struct with the original values

Create in the same file (src/test/AlchemistV3.t.sol) the following test

    function testInitializeInvariantBreakage() public {
        // Check invariant was break using initialize
        assert(
            alchemist.collateralizationLowerBound() > alchemist.minimumCollateralization() 
        );
        assert(
            alchemist.minimumCollateralization() > alchemist.globalMinimumCollateralization() 
        );
    }

Exec test

forge test --mt testInitializeInvariantBreakage

Observe the AlchemistV3 invariant is bypassed