Steadefi

Steadefi
DeFiHardhatFoundryOracle
35,000 USDC
View results
Submission Details
Severity: medium
Invalid

Loss of precision due to division after multiplication

Summary

Division after multiplication is performed in several functions in contracts GMXManager.sol and GMXWithdraw.sol. This can lead to loss of tokens due to precision loss.

Vulnerability Details

In Solidity, the order of operations matters, especially when it comes to division and multiplication. This is because division and multiplication are not commutative operations, meaning the order in which they are performed can lead to different results.

In functions GMXManager::calcBorrow, GMXManager::calcMinMarketSlippageAmt, GMXManager::calcMinTokensSlippageAmt, GMXWithdraw::withdraw there are multiple instances where multiplication is performed after division. This can potentially lead to precision loss, which is the loss of information due to the approximation of real numbers to a finite number of digits.

In GMXManager.sol:

function calcBorrow(
GMXTypes.Store storage self,
uint256 depositValue
) external view returns (uint256, uint256) {
// Calculate final position value based on deposit value
uint256 _positionValue = depositValue * self.leverage / SAFE_MULTIPLIER;
// Obtain the value to borrow
uint256 _borrowValue = _positionValue - depositValue;
uint256 _tokenADecimals = IERC20Metadata(address(self.tokenA)).decimals();
uint256 _tokenBDecimals = IERC20Metadata(address(self.tokenB)).decimals();
uint256 _borrowLongTokenAmt;
uint256 _borrowShortTokenAmt;
// If delta is long, borrow all in short token
if (self.delta == GMXTypes.Delta.Long) {
_borrowShortTokenAmt = _borrowValue * SAFE_MULTIPLIER
/ GMXReader.convertToUsdValue(self, address(self.tokenB), 10**(_tokenBDecimals))
/ (10 ** (18 - _tokenBDecimals));
}
// If delta is neutral, borrow appropriate amount in long token to hedge, and the rest in short token
if (self.delta == GMXTypes.Delta.Neutral) {
// Get token weights in LP, e.g. 50% = 5e17
(uint256 _tokenAWeight,) = GMXReader.tokenWeights(self);
//@audit multiplication after division
// Get value of long token (typically tokenA)
uint256 _longTokenWeightedValue = _tokenAWeight * _positionValue / SAFE_MULTIPLIER;
// Borrow appropriate amount in long token to hedge
@> _borrowLongTokenAmt = _longTokenWeightedValue * SAFE_MULTIPLIER
/ GMXReader.convertToUsdValue(self, address(self.tokenA), 10**(_tokenADecimals))
/ (10 ** (18 - _tokenADecimals));
// Borrow the shortfall value in short token
@> _borrowShortTokenAmt = (_borrowValue - _longTokenWeightedValue) * SAFE_MULTIPLIER
/ GMXReader.convertToUsdValue(self, address(self.tokenB), 10**(_tokenBDecimals))
/ (10 ** (18 - _tokenBDecimals));
}
return (_borrowLongTokenAmt, _borrowShortTokenAmt);
}
function calcMinMarketSlippageAmt(
GMXTypes.Store storage self,
uint256 depositValue,
uint256 slippage
) external view returns (uint256) {
uint256 _lpTokenValue = self.gmxOracle.getLpTokenValue(
address(self.lpToken),
address(self.tokenA),
address(self.tokenA),
address(self.tokenB),
false,
false
);
// @audit multiplication after devision:
return depositValue
* SAFE_MULTIPLIER
/ _lpTokenValue
@> * (10000 - slippage) / 10000;
}
function calcMinTokensSlippageAmt(
GMXTypes.Store storage self,
uint256 lpAmt,
address minLongToken,
address minShortToken,
uint256 slippage
) external view returns (uint256, uint256) {
uint256 _withdrawValue = lpAmt
* self.gmxOracle.getLpTokenValue(
address(self.lpToken),
address(self.tokenA),
address(self.tokenA),
address(self.tokenB),
false,
false
)
/ SAFE_MULTIPLIER;
(uint256 _tokenAWeight, uint256 _tokenBWeight) = GMXReader.tokenWeights(self);
// @audit multiplication after division
uint256 _minLongTokenAmt = _withdrawValue
* _tokenAWeight / SAFE_MULTIPLIER
@> * SAFE_MULTIPLIER
/ GMXReader.convertToUsdValue(
self,
minLongToken,
10**(IERC20Metadata(minLongToken).decimals())
)
/ (10 ** (18 - IERC20Metadata(minLongToken).decimals()));
// @audit multiplication after division
uint256 _minShortTokenAmt = _withdrawValue
* _tokenBWeight / SAFE_MULTIPLIER
@> * SAFE_MULTIPLIER
/ GMXReader.convertToUsdValue(
self,
minShortToken,
10**(IERC20Metadata(minShortToken).decimals())
)
/ (10 ** (18 - IERC20Metadata(minShortToken).decimals()));
return (
@> _minLongTokenAmt * (10000 - slippage) / 10000,
@> _minShortTokenAmt * (10000 - slippage) / 10000
);
}

In GMXWithdraw.sol:

function withdraw(
GMXTypes.Store storage self,
GMXTypes.WithdrawParams memory wp
) external {
// Sweep any tokenA/B in vault to the temporary trove for future compouding and to prevent
// it from being considered as part of withdrawers assets
if (self.tokenA.balanceOf(address(this)) > 0) {
self.tokenA.safeTransfer(self.trove, self.tokenA.balanceOf(address(this)));
}
if (self.tokenB.balanceOf(address(this)) > 0) {
self.tokenB.safeTransfer(self.trove, self.tokenB.balanceOf(address(this)));
}
self.refundee = payable(msg.sender);
GMXTypes.HealthParams memory _hp;
_hp.equityBefore = GMXReader.equityValue(self);
_hp.lpAmtBefore = GMXReader.lpAmt(self);
_hp.debtRatioBefore = GMXReader.debtRatio(self);
_hp.deltaBefore = GMXReader.delta(self);
GMXTypes.WithdrawCache memory _wc;
_wc.user = payable(msg.sender);
// @audit multiplication after division
_wc.shareRatio = wp.shareAmt
* SAFE_MULTIPLIER
/ IERC20(address(self.vault)).totalSupply();
_wc.lpAmt = _wc.shareRatio
@> * GMXReader.lpAmt(self)
/ SAFE_MULTIPLIER;
_wc.withdrawValue = _wc.lpAmt
@> * self.gmxOracle.getLpTokenValue(
address(self.lpToken),
address(self.tokenA),
address(self.tokenA),
address(self.tokenB),
false,
false
)
/ SAFE_MULTIPLIER;
.
.
.
//@audit multiplication after division
uint256 _lpAmtToRemove = _wc.lpAmt
* (self.leverage - SAFE_MULTIPLIER)
/ self.leverage
@> * 10200 / 10000;
_wc.tokensToUser = _wc.lpAmt - _lpAmtToRemove;
_wc.lpAmt = _lpAmtToRemove;
}
.
.
.
}

Impact

The performed multiplication after division in functions GMXManager::calcBorrow, GMXManager::calcMinMarketSlippageAmt, GMXManager::calcMinTokensSlippageAmt, GMXWithdraw::withdraw can lead to loss of tokens for the users.

The precision loss in these operations is due to the fact that division of integers in Solidity results in an integer, not a fractional number. If the result of the division operation is a fractional number, it is rounded down to the nearest whole number. This means that any fractional part of the result is lost, leading to a loss of precision.

Tools Used

Manual Review, VS Code

Recommendations

Rewrite the above mentioned functions in such a way that the division is performed at the end of the calculations.

Updates

Lead Judging Commences

hans Lead Judge over 1 year ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.