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
- contracts/transmuter/facets/Redeemer.sol#L156-L157
- contracts/transmuter/libraries/LibSetters.sol#L230-L240
- contracts/transmuter/libraries/LibHelpers.sol#L77-L80
LibHelpers.piecewiseLinear 함수는 다음과 같다.
findLowerBound는 findLowerBound(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);
}
}그렇다면 x가 xArray의 모든 값보다 작아지는 게 일어날 수 있는 일일까?
uint64(LibHelpers.piecewiseLinear(collatRatio, xRedemptionCurveMem, yRedemptionCurveMem))에서 ts.xRedemptionCurve가 xArray 파라미터로 들어간다. 이 값은 관리자(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
x가 xArray의 모든 값보다 작은 경우를 따로 핸들링하여 언더플로우를 피한다.
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