code4rena-2023-06-angle-protocol-m01

[M-01] LibHelpers.piecewiseLinear will revert when the value is less than the first element of the array

보고서

Summary

예외상황 처리가 부족하여 언더플로우가 발생, redeem 이 실패할 수 있다.

Keyword

integer underflow

Vulnerability

LibHelpers.piecewiseLinear 함수는 다음과 같다.

findLowerBoundfindLowerBound(true, xArray, 1, x)는 오름차순으로 정렬된 xArray 배열에서, x보다 값이 큰 첫번째 인덱스를 리턴한다.

이 때, x가 모든 값보다 작다면 어떻게 되는가? LibHelpers.findLowerBound는 0을 리턴한다.

그리고 yArray[indexLowerBound] + ((yArray[indexLowerBound + 1] - yArray[indexLowerBound]) * int64(x - xArray[indexLowerBound])) / int64(xArray[indexLowerBound + 1] - xArray[indexLowerBound]); 를 계산하면서 x - xArray[indexLowerBound]에서 언더플로우가 발생하여 revert 될 것이다.

    function piecewiseLinear(uint64 x, uint64[] memory xArray, int64[] memory yArray) internal pure returns (int64) {
        uint256 indexLowerBound = findLowerBound(true, xArray, 1, x);
        if (indexLowerBound == xArray.length - 1) return yArray[xArray.length - 1];
        return
            yArray[indexLowerBound] +
            ((yArray[indexLowerBound + 1] - yArray[indexLowerBound]) * int64(x - xArray[indexLowerBound])) /
            int64(xArray[indexLowerBound + 1] - xArray[indexLowerBound]);
    }

LibHelpers.piecewiseLinear 함수는 어디에서 사용되는가? Redeemer._quoteRedemptionCurve에서, 담보가 부족할 때 패널티(penaltyFactor)를 적용한다. penaltyFactor를 계산하기 위해 uint64(LibHelpers.piecewiseLinear(collatRatio, xRedemptionCurveMem, yRedemptionCurveMem))를 호출한다.

    function _quoteRedemptionCurve(
        uint256 amountBurnt
    )
        internal
        view
        returns (address[] memory tokens, uint256[] memory balances, uint256[] memory subCollateralsTracker)
    {
        ...
        if (collatRatio < BASE_9) {
            uint64[] memory xRedemptionCurveMem = ts.xRedemptionCurve;
            penaltyFactor = uint64(LibHelpers.piecewiseLinear(collatRatio, xRedemptionCurveMem, yRedemptionCurveMem));
        }
 
        uint256 balancesLength = balances.length;
        for (uint256 i; i < balancesLength; ++i) {
            balances[i] = collatRatio >= BASE_9
                ? (amountBurnt * balances[i] * (uint64(yRedemptionCurveMem[yRedemptionCurveMem.length - 1]))) /
                    (stablecoinsIssued * collatRatio)
                : (amountBurnt * balances[i] * penaltyFactor) / (stablecoinsIssued * BASE_9);
        }
    }

그렇다면 xxArray의 모든 값보다 작아지는 게 일어날 수 있는 일일까?

uint64(LibHelpers.piecewiseLinear(collatRatio, xRedemptionCurveMem, yRedemptionCurveMem))에서 ts.xRedemptionCurvexArray 파라미터로 들어간다. 이 값은 관리자(guardian)이 setRedemptionCurveParams 함수를 호출해 설정한다. xFee 가 바로 xArray 값이다.

    function setRedemptionCurveParams(uint64[] memory xFee, int64[] memory yFee) external onlyGuardian {
        LibSetters.setRedemptionCurveParams(xFee, yFee);
    }

이 값이 설정되기 전 LibSetters.checkFees 함수를 통해 값이 유효한지 확인한다. checkFees 호출시 액션은 ActionType.Redeem로 하드코딩되어 있으므로, 실질적으로 xFee의 크기와 관련된 조건은 상한이 BASE_9임을 제외하면 따로 없다. _quoteRedemptionCurve에서 LibHelpers.piecewiseLinear 를 호출하는 경우는 collatRatio < BASE_9 때이다. 따라서 collatRatio(=x) < xRedemptionCurveMem[0](=xArray[0]) 보다 작은 경우도 존재할 수도 있다고 본다.

    function setRedemptionCurveParams(uint64[] memory xFee, int64[] memory yFee) internal {
        TransmuterStorage storage ts = s.transmuterStorage();
        LibSetters.checkFees(xFee, yFee, ActionType.Redeem);
        ts.xRedemptionCurve = xFee;
        ts.yRedemptionCurve = yFee;
        emit RedemptionCurveParamsSet(xFee, yFee);
    }
 
    function checkFees(uint64[] memory xFee, int64[] memory yFee, ActionType action) internal view {
        uint256 n = xFee.length;
        if (n != yFee.length || n == 0) revert InvalidParams();
        if (
            ...
            (action == ActionType.Redeem && (xFee[n - 1] > BASE_9 || yFee[n - 1] < 0 || yFee[n - 1] > int256(BASE_9)))
        ) revert InvalidParams();
 
        for (uint256 i = 0; i < n - 1; ++i) {
            if (
                ...
                (action == ActionType.Redeem && (xFee[i] >= xFee[i + 1] || yFee[i] < 0 || yFee[i] > int256(BASE_9)))
            ) revert InvalidParams();
        }
 
        // If a mint or burn fee is negative, we need to check that accounts atomically minting
        // (from any collateral) and then burning cannot get more than their initial value
        if (yFee[0] < 0) {
            if (!LibDiamond.isGovernor(msg.sender)) revert NotGovernor(); // Only governor can set negative fees
            TransmuterStorage storage ts = s.transmuterStorage();
            address[] memory collateralListMem = ts.collateralList;
            uint256 length = collateralListMem.length;
            if (action == ActionType.Mint) {
                // This can be mathematically expressed by `(1-min_c(burnFee_c))<=(1+mintFee[0])`
                for (uint256 i; i < length; ++i) {
                    int64[] memory burnFees = ts.collaterals[collateralListMem[i]].yFeeBurn;
                    if (burnFees[0] + yFee[0] < 0) revert InvalidNegativeFees();
                }
            }
            if (action == ActionType.Burn) {
                // This can be mathematically expressed by `(1-burnFee[0])<=(1+min_c(mintFee_c))`
                for (uint256 i; i < length; ++i) {
                    int64[] memory mintFees = ts.collaterals[collateralListMem[i]].yFeeMint;
                    if (yFee[0] + mintFees[0] < 0) revert InvalidNegativeFees();
                }
            }
        }
    }

Impact

언더플로우로 인해 redeem 이 실패한다.

Mitigation

xxArray의 모든 값보다 작은 경우를 따로 핸들링하여 언더플로우를 피한다.

    function piecewiseLinear(uint64 x, uint64[] memory xArray, int64[] memory yArray) internal pure returns (int64) {
        uint256 indexLowerBound = findLowerBound(true, xArray, 1, x);
-       if (indexLowerBound == xArray.length - 1) return yArray[xArray.length - 1];
+       if (indexLowerBound == 0 && x < xArray[0]) return yArray[0];
+       else if (indexLowerBound == xArray.length - 1) return yArray[xArray.length - 1];
        return
            yArray[indexLowerBound] +
            ((yArray[indexLowerBound + 1] - yArray[indexLowerBound]) * int64(x - xArray[indexLowerBound])) /
            int64(xArray[indexLowerBound + 1] - xArray[indexLowerBound]);
    }

tags: bughunting, angle protocol, smart contract, solidity, stablecoin, integer overflow underflow, severity medium