sherlock-2024-01-flat-money-m01
[M-01] Fees are ignored when checks skew max in Stable Withdrawal / Leverage Open / Leverage Adjust
Summary
담보 토큰 출금, 레버리지 오픈, 레버리지 조정 시, skew가 최대에 도달하는지 확인할 때 (볼트에 재예치되는)수수료를 무시하고 계산한다. 안전한 상황에도 트랜잭션을 revert 시킨다.
Keyword
arithmetic error
Vulnerability
사용자가 스테이블 LP로 출금하면 볼트에 남아있는 총 스테이블 담보의 양이 업데이트 된다. 그런 다음 출금 수수료를 계산하고, 시스템이 너무 롱포지션에 치우치지 않도록 checkSkewMax 함수를 호출한다.
function executeWithdraw(
address _account,
uint64 _executableAtTime,
FlatcoinStructs.AnnouncedStableWithdraw calldata _announcedWithdraw
) external whenNotPaused onlyAuthorizedModule returns (uint256 _amountOut, uint256 _withdrawFee) {
uint256 withdrawAmount = _announcedWithdraw.withdrawAmount;
uint32 maxAge = _getMaxAge(_executableAtTime);
uint256 stableCollateralPerShareBefore = stableCollateralPerShare(maxAge);
_amountOut = (withdrawAmount * stableCollateralPerShareBefore) / (10 ** decimals());
// Unlock the locked LP tokens before burning.
// This is because if the amount to be burned is locked, the burn will fail due to `_beforeTokenTransfer`.
_unlock(_account, withdrawAmount);
_burn(_account, withdrawAmount);
@> vault.updateStableCollateralTotal(-int256(_amountOut));
uint256 stableCollateralPerShareAfter = stableCollateralPerShare(maxAge);
// Check that there is no significant impact on stable token price.
// This should never happen and means that too much value or not enough value was withdrawn.
if (totalSupply() > 0) {
if (
stableCollateralPerShareAfter < stableCollateralPerShareBefore - 1e6 ||
stableCollateralPerShareAfter > stableCollateralPerShareBefore + 1e6
) revert FlatcoinErrors.PriceImpactDuringWithdraw();
// Apply the withdraw fee if it's not the final withdrawal.
@> _withdrawFee = (stableWithdrawFee * _amountOut) / 1e18;
// additionalSkew = 0 because withdrawal was already processed above.
@> vault.checkSkewMax({additionalSkew: 0});
} else {
// Need to check there are no longs open before allowing full system withdrawal.
uint256 sizeOpenedTotal = vault.getVaultSummary().globalPositions.sizeOpenedTotal;
if (sizeOpenedTotal != 0) revert FlatcoinErrors.MaxSkewReached(sizeOpenedTotal);
if (stableCollateralPerShareAfter != 1e18) revert FlatcoinErrors.PriceImpactDuringFullWithdraw();
}
emit FlatcoinEvents.Withdraw(_account, _amountOut, withdrawAmount);
}executeWithdraw 함수가 끝나면 다시 볼트 담보로 withdrawFee 만큼을 업데이트 한다. (출금 수수료는 볼트에 다시 예치) 그리고 키퍼에게 수수료를 전달하고, (amountOut - totalFee) 만큼의 담보가 사용자에게 전달된다.
function _executeStableWithdraw(address account) internal returns (uint256 amountOut) {
...
@> (amountOut, withdrawFee) = IStableModule(vault.moduleAddress(FlatcoinModuleKeys._STABLE_MODULE_KEY))
.executeWithdraw(account, order.executableAtTime, stableWithdraw);
@> uint256 totalFee = order.keeperFee + withdrawFee;
// Make sure there is enough margin in the position to pay the keeper fee and withdrawal fee
if (amountOut < totalFee) revert FlatcoinErrors.NotEnoughMarginForFees(int256(amountOut), totalFee);
// include the fees here to check for slippage
@> amountOut -= totalFee;
if (amountOut < stableWithdraw.minAmountOut)
revert FlatcoinErrors.HighSlippage(amountOut, stableWithdraw.minAmountOut);
// Settle the collateral
@> vault.updateStableCollateralTotal(int256(withdrawFee)); // pay the withdrawal fee to stable LPs
@> vault.sendCollateral({to: msg.sender, amount: order.keeperFee}); // pay the keeper their fee
@> vault.sendCollateral({to: account, amount: amountOut}); // transfer remaining amount to the trader
emit FlatcoinEvents.OrderExecuted({account: account, orderType: order.orderType, keeperFee: order.keeperFee});
}executeWithdraw 에서 checkSkewMax 를 호출할 때, 재예치되는 출금 수수료를 고려하지 않았다. 출금으로 인한 담보 업데이트 → checkSkewMax → 출금 수수료 재예치 담보 업데이트 한다. 즉, checkSkewMax 에서는 출금 수수료로 인한 담보 증가액은 고려하지 않는다. 즉 실제보다 skew 가 높게 측정되므로, checkSkewMax 에서 트랜잭션이 취소될 수 있다.
다음과 같은 상황을 생각해보자.
skewFractionMax는 120%,stableWithdrawFee는 1% 라고 가정하자.- 앨리스는 100개의 담보를 예치하고 밥은 크기 100의 레버리지 포지션을 연다.
- 현재 볼트에 100개의 담보가 있고, skew는 0, skew 비율은 100% 이다.
- 앨리스가 16.8개의 담보를 출금하려고 하고, 출금 수수료는 0.168이다. 출금 후 볼트에 83.368개의 담보가 있을 것으로 예상되므로,
skewFraction은 119.5%로skewFractionMax보다 작다. - 그러나 프로토콜이 skew max를 확인할 때 withdrawFee를 포함하지 않고 계산하므로,
skewFraction이 120.19%로skewFractionMax보다 높기 때문에 출금 트랜잭션이 취소된다.
동일한 이슈가 레버리지 오픈과 레버리지 조정 기능에도 존재한다. skew max를 계산할 때 tradeFee 가 무시된다.
Impact
스테이블 출금/레버리지 오픈/레버리지 조정을 잘못 revert 한다.
Mitigation
skew max를 계산할 때 볼트에 재예치되는 수수료를 고려한다.
tags: bughunting, flat money, smart contract, solidity, solo issue, arithmetic error, severity medium