Faulty Division Operation
Overview
- Severity: Medium
- Confidence: Medium
- Affected Versions: All
What is the Faulty Division Operation vulnerability?
While newer versions of the Solidity language support fixed-point mathematical operations, many calculations in Ethereum (including for units of ether itself) are done using integer arithmetic. This means that any division operation has the potential to lead to imprecise results in cases where the inputs are variable. Rounding errors may ultimately lead to unintended calculations and even exploits of smart contract logic as a result.
A real-world example: Midas Capital
On June 18, 2023, the Midas Capital protocol was exploited due to a bug in a redemption calculation that used imprecise division. An attacker was able to use the manner in which the calculations were made to subvert the intent of the protocol and redeem more tokens than they were entitled to, resulting in an approximate loss of $600,000.
Further reading: Midas Capital Hack Analysis
Technical example of vulnerable code
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
contract VulnerableContract {
mapping(address => uint256) public balances;
mapping(address => uint8) public bonusFactor;
int256 public interestRatio = 10;
function deposit() external payable {
balances[msg.sender] += msg.value;
}
function withdrawWithInterest() external {
uint256 interest = balances[msg.sender] / ratio;
uint256 amountToSend = balances[msg.sender] + (interest * bonusFactor[msg.sender]);
balances[msg.sender] = 0;
payable(msg.sender).transfer(amountToSend);
}
// ... Additional functions, logic, and other parts of the contract ...
}
In the above example, the contract VulnerableContract
exposes a function named withdrawWithInterest()
that is intended to send the caller their deposited balance plus interest, with an additional bonus computed based on the bonsuFactor
mapping (e.g. for an increasing bonus the longer that the deposit has been held). However, the calculation of the bonus is performed using integer division before multiplication, which could lead to an unnecessary loss of precision and result in the user receiving less than expected.
If an adversary is aware of this loss of precision in the calculation performed by withdrawWithInterest()
, they could deposit strategic amounts that would lead to greater interest payments than the average uninformed user might receive. This could be used to drain the contract of its ether balance, or simply to gain an advantage over other users.
Technical example of how to fix the vulnerability
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
contract VulnerableContract {
mapping(address => uint256) public balances;
mapping(address => uint8) public bonusFactor;
int256 public interestRatio = 10;
function deposit() external payable {
balances[msg.sender] += msg.value;
}
function withdrawWithInterest() external {
uint256 interestWithBonus = balances[msg.sender] / (ratio * bonusFactor[msg.sender]);
uint256 amountToSend = balances[msg.sender] + interestWithBonus;
balances[msg.sender] = 0;
payable(msg.sender).transfer(amountToSend);
}
// ... Additional functions, logic, and other parts of the contract ...
}
In the updated calculation, the multiplcation is performed before the division, reducing the loss of precision in the calculations and thus the impact of the exploit described above. While rounding errors will still occur due to the nature of integer division, the impact in this case will be lessened due to the reverse order of the operations.