https://github.com/hackenproof-public/reliq-protocol
The borrowMore() function in ReliqHYPE.sol incorrectly overwrites the numberOfDays field with the remaining loan tenure instead of preserving the original loan duration. This causes permanent data corruption that cascades through subsequent operations like extendLoan(), leading to incorrect tenure calculations and loss of historical loan data.
The borrowMore() function calculates the remaining loan tenure and then incorrectly stores this value in the numberOfDays field, overwriting the original loan duration:
// Line 373-374: Calculate remaining tenure
uint256 nextMidnight = getMidnightTimestamp(block.timestamp);
uint256 newBorrowTenure = (endDate - nextMidnight) / 1 days;
// Lines 401-406: BUG - overwrites original numberOfDays with remaining days
Loans[msg.sender] = Loan({
collateral: netUserCollateral,
borrowed: netUserBorrow,
endDate: endDate,
numberOfDays: newBorrowTenure // Should preserve original, not overwrite
});
The numberOfDays field is meant to track the original loan duration for historical and accounting purposes. However, borrowMore() replaces it with the remaining days until maturity, causing:
numberOfDays now get the wrong valueborrowMore() compound the problemLoans[user].numberOfDays = 180borrowMore()
newBorrowTenure = (endDate - nextMidnight) / 1 days = 90Loans[user].numberOfDays = 90 (CORRUPTED - was 180)The extendLoan() function reads and updates numberOfDays:
// Line 586: Reads the potentially corrupted value
uint256 _loanTenure = Loans[msg.sender].numberOfDays;
// Line 606: Updates based on corrupted value
Loans[msg.sender].numberOfDays = _loanTenure + numberOfDays;
If borrowMore() was called before extendLoan(), the tenure tracking will be incorrect.
borrowMore(), then extends:
numberOfDays is corrupted from 100 to 50borrowMore() calls progressively corrupt the dataWhile this might seem like a minor data inconsistency, it has real implications:
numberOfDays will have corrupted dataThis is not an active attack vector but rather a functional correctness issue that:
borrowMore()test/POC/M01_NumberOfDaysCorruption.t.sol
forge test --match-path test/POC/M01_NumberOfDaysCorruption.t.sol -vv
// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;
import "forge-std/Test.sol";
import {ReliqHYPE} from "../../src/ReliqHYPE.sol";
import {InterestManager} from "../../src/InterestManager.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MockERC20 is ERC20 {
constructor() ERC20("Mock Backing Token", "MBK") {}
function mint(address to, uint256 amt) external {
_mint(to, amt);
}
}
contract POC_M01_NumberOfDaysCorruption is Test {
ReliqHYPE reHYPE;
InterestManager interestManager;
MockERC20 backing;
address owner = address(this);
address treasury = makeAddr("treasury");
address user = makeAddr("user");
function setUp() public {
backing = new MockERC20();
reHYPE = new ReliqHYPE(IERC20(address(backing)));
interestManager = new InterestManager(1000);
reHYPE.setTreasuryAddress(treasury);
reHYPE.setInterestManager(address(interestManager));
backing.mint(owner, 1_000_000 ether);
backing.approve(address(reHYPE), type(uint256).max);
backing.mint(user, 1_000_000 ether);
vm.prank(user);
backing.approve(address(reHYPE), type(uint256).max);
reHYPE.setStart(100_000 ether, 1000 ether);
reHYPE.setMaxMintable(10_000_000 ether);
reHYPE.buy(owner, 500_000 ether);
vm.prank(user);
reHYPE.buy(user, 100_000 ether);
}
function test_NumberOfDaysFieldCorruption() public {
vm.startPrank(user);
reHYPE.borrow(10_000 ether, 90);
(,, uint256 endDate, uint256 initialNumberOfDays) = reHYPE.Loans(user);
assertEq(initialNumberOfDays, 90, "Initial loan should be 90 days");
vm.warp(block.timestamp + 30 days);
uint256 nextMidnight = reHYPE.getMidnightTimestamp(block.timestamp);
uint256 remainingDays = (endDate - nextMidnight) / 1 days;
reHYPE.borrowMore(5_000 ether);
(,, uint256 newEndDate, uint256 corruptedNumberOfDays) = reHYPE.Loans(user);
assertEq(newEndDate, endDate, "End date should remain the same");
assertEq(corruptedNumberOfDays, remainingDays, "numberOfDays was overwritten with remaining days");
assertLt(corruptedNumberOfDays, initialNumberOfDays, "numberOfDays is now smaller than original");
assertEq(corruptedNumberOfDays, 60, "numberOfDays should be ~60 days remaining");
vm.stopPrank();
}
function test_MaximumImpact_DataCorruptionInExtendLoan() public {
vm.startPrank(user);
reHYPE.borrow(10_000 ether, 100);
(,,, uint256 originalTenure) = reHYPE.Loans(user);
assertEq(originalTenure, 100, "Original tenure is 100 days");
vm.warp(block.timestamp + 50 days);
reHYPE.borrowMore(5_000 ether);
(,,, uint256 corruptedTenure) = reHYPE.Loans(user);
assertLt(corruptedTenure, originalTenure, "Tenure was corrupted");
assertEq(corruptedTenure, 50, "Corrupted tenure is ~50 days");
uint256 extensionDays = 200;
reHYPE.extendLoan(extensionDays);
(,,, uint256 finalNumberOfDays) = reHYPE.Loans(user);
uint256 expectedTotalTenure = originalTenure + extensionDays;
uint256 actualTotalTenure = finalNumberOfDays;
assertEq(actualTotalTenure, corruptedTenure + extensionDays, "extendLoan uses corrupted value");
assertLt(actualTotalTenure, expectedTotalTenure, "Total tenure calculation is wrong");
assertEq(actualTotalTenure, 250, "Wrong tenure: 50 + 200 = 250 instead of 100 + 200 = 300");
vm.stopPrank();
}
function test_RepeatedBorrowMoreCompoundsCorruption() public {
vm.startPrank(user);
reHYPE.borrow(10_000 ether, 365);
(,,, uint256 tenure0) = reHYPE.Loans(user);
assertEq(tenure0, 365, "Initial: 365 days");
vm.warp(block.timestamp + 50 days);
reHYPE.borrowMore(1_000 ether);
(,,, uint256 tenure1) = reHYPE.Loans(user);
assertEq(tenure1, 315, "After 50 days: ~315 remaining");
vm.warp(block.timestamp + 100 days);
reHYPE.borrowMore(1_000 ether);
(,,, uint256 tenure2) = reHYPE.Loans(user);
assertEq(tenure2, 215, "After another 100 days: ~215 remaining");
vm.warp(block.timestamp + 150 days);
reHYPE.borrowMore(1_000 ether);
(,,, uint256 tenure3) = reHYPE.Loans(user);
assertEq(tenure3, 65, "After another 150 days: ~65 remaining");
assertEq(tenure0, 365, "Started with 365 days");
assertEq(tenure3, 65, "Ended with 65 days in record");
vm.stopPrank();
}
function test_HistoricalDataLoss() public {
vm.startPrank(user);
reHYPE.borrow(50_000 ether, 180);
(uint256 collateral1, uint256 borrowed1, uint256 endDate1, uint256 originalDays) = reHYPE.Loans(user);
assertEq(originalDays, 180, "Original loan was 180 days");
vm.warp(block.timestamp + 90 days);
reHYPE.borrowMore(25_000 ether);
(uint256 collateral2, uint256 borrowed2, uint256 endDate2, uint256 newDays) = reHYPE.Loans(user);
assertEq(endDate2, endDate1, "End date unchanged");
assertGt(collateral2, collateral1, "Collateral increased");
assertGt(borrowed2, borrowed1, "Borrowed increased");
assertLt(newDays, originalDays, "Historical tenure data lost");
assertEq(newDays, 90, "Only ~90 days remaining recorded");
vm.stopPrank();
}
}
The numberOfDays field should remain unchanged after the initial loan creation:
function borrowMore(uint256 amountToBorrow) public nonReentrant {
// ... existing code ...
uint256 netUserCollateral = userCollateral + collateralDeficit;
uint256 netUserBorrow = userBorrowed + effectiveNewBorrow;
Loans[msg.sender] = Loan({
collateral: netUserCollateral,
borrowed: netUserBorrow,
endDate: endDate,
numberOfDays: Loans[msg.sender].numberOfDays // Preserve original
});
// ... rest of function ...
}
If both values are needed, add a separate field:
struct Loan {
uint256 collateral;
uint256 borrowed;
uint256 endDate;
uint256 numberOfDays; // Original duration
uint256 remainingDaysAtUpdate; // Last calculated remaining
}
Since the loan endDate already tracks maturity, the numberOfDays update is unnecessary:
function borrowMore(uint256 amountToBorrow) public nonReentrant {
// ... existing code ...
// Only update fields that actually change
Loans[msg.sender].collateral = netUserCollateral;
Loans[msg.sender].borrowed = netUserBorrow;
// Do NOT touch numberOfDays or endDate
// ... rest of function ...
}