Kind Aqua Ostrich
Medium
Unchecked increment of nextLoanId of contract PredictDotLoan.sol
could cause an overflow.
In Solidity, unchecked arithmetic (i.e., not checking for overflow or underflow) was historically a vulnerability before Solidity 0.8.x, where arithmetic operations would automatically revert in case of overflow/underflow. However, unchecked arithmetic can still be a risk in specific cases or deliberately used for gas optimization.
In the contract, the following piece of code contains an unchecked increment of nextLoanId
: https://github.com/sherlock-audit/2024-09-predict-fun/blob/main/predict-dot-loan/contracts/PredictDotLoan.sol#L312-L314
The goal of an unchecked increment is typically to save gas, as checking for overflow costs extra gas. However, if nextLoanId
were to reach the maximum value for uint256
(which is very large), the next increment would cause it to wrap around to zero, resulting in a loan ID collision or unexpected behavior.
Once nextLoanId
wraps around to zero (i.e., it overflows), the next loan issued will have an ID of 0
, which could potentially overwrite an existing loan with ID 0
or introduce an incorrect state in the protocol.
If nextLoanId
is used in mappings or arrays as a unique identifier for loans, an overflow could cause loans to share the same ID, leading to broken logic and potential financial risk.
In systems that rely on unique IDs for tracking, an overflow could corrupt data, making it difficult to track or manage loans properly.
Let`s reffer to first PoC:
- We initialize
nextLoanId
to the maximum value(type(uint256).max)
to demonstrate the overflow condition in the PoC. - The
createLoan
function incrementsnextLoanId
unchecked, meaning that after the first call,nextLoanId
will wrap around to zero. - This leads to a collision of loan IDs, which could result in a loan overwriting an existing loan.
No response
Let`s reffer to second PoC:
- After the first
createLoan()
call, a loan is created with ID2^256 - 1
(the maximum value foruint256
). - The second
createLoan()
call causes thenextLoanId
to overflow to0
, and this new loan will overwrite the existing loan with ID0
. - When you try to retrieve the loan with ID
0
, it will return the details of the second loan, which means that the first loan (with ID0
) has been overwritten or mismanaged.
- The primary impact of this vulnerability is that loan IDs could collide due to overflow, leading to existing loans being overwritten. In financial systems, this can cause significant issues in tracking debt obligations, repayments, and liquidations.
- When loans are overwritten, users might lose their loan data, or the system might incorrectly assume certain loans exist or do not exist. This can result in borrowers losing their collateral or lenders being unable to collect repayments.
- If an attacker can manipulate the system to force loan ID collisions, they could disrupt the proper functioning of the lending protocol, creating DoS (Denial of Service) attacks or draining funds through incorrect loan operations.
- As loans are overwritten or mismanaged, the overall state of the protocol becomes corrupt. Auditors and users will find it difficult to rely on the integrity of the system, reducing trust in the protocol.
FIRST PoC
// Simplified contract snippet showing unchecked increment
contract LoanContract {
uint256 public nextLoanId = type(uint256).max; // Start at the max value to force overflow
mapping(uint256 => Loan) public loans;
struct Loan {
address borrower;
uint256 amount;
bool active;
}
function createLoan(uint256 amount) public {
unchecked {
++nextLoanId; // Unchecked increment that could overflow
}
loans[nextLoanId] = Loan({
borrower: msg.sender,
amount: amount,
active: true
});
}
function getLoan(uint256 loanId) public view returns (Loan memory) {
return loans[loanId];
}
}
SECOND PoC
// Deploy the contract and interact with it in a test or using a Solidity script
// Step 1: Create a loan with max loanId
loanContract.createLoan(100); // Creates loan with ID 2^256 - 1
// Step 2: Call the function again, causing the nextLoanId to overflow to 0
loanContract.createLoan(200); // Creates loan with ID 0 (overwrites existing loan)
// Step 3: Retrieve the loan with ID 0
Loan memory loan = loanContract.getLoan(0);
console.log("Loan Borrower:", loan.borrower);
console.log("Loan Amount:", loan.amount); // Displays incorrect or overwritten loan data
The simplest way to prevent this vulnerability is to remove the unchecked block around the nextLoanId
increment. Solidity’s default behavior in version 0.8.x will automatically revert if an overflow occurs.
function createLoan(uint256 amount) public {
nextLoanId++;
loans[nextLoanId] = Loan({
borrower: msg.sender,
amount: amount,
active: true
});
}
Although Solidity 0.8.x already checks for overflow by default, explicitly using SafeMath can make the code clearer and ensure that future versions of the compiler are handled safely.
Ensure that nextLoanId
starts at a reasonable value (e.g., 0 or 1), and avoid setting it to an extremely high value that would approach the uint256
limit. Additionally, perform checks to ensure that nextLoanId
does not overflow before each increment.
If loan ID uniqueness is critical, consider using a more sophisticated identifier scheme, such as combining the loan ID with a timestamp or address to ensure uniqueness, even if overflow occurs.