Core Contracts

Regnum Aurum Acquisition Corp
HardhatReal World AssetsNFT
77,280 USDC
View results
Submission Details
Severity: high
Valid

When minting a token in the DebtToken::mint function, an additional balanceIncrease is added to the debt, doubling the interest on the debt.

01. Relevant GitHub Links

02. Summary

When the token is minted in the DebtToken::mint function, the balanceIncrease is added to the debt to increase the debt.

03. Vulnerability Details

The DebtToken::mint function is used to mint a DebtToken to the user when the user borrows.

function mint(
address user,
address onBehalfOf,
uint256 amount,
uint256 index
) external override onlyReservePool returns (bool, uint256, uint256) {
if (user == address(0) || onBehalfOf == address(0)) revert InvalidAddress();
if (amount == 0) {
return (false, 0, totalSupply());
}
uint256 amountScaled = amount.rayDiv(index);
if (amountScaled == 0) revert InvalidAmount();
uint256 scaledBalance = balanceOf(onBehalfOf);
bool isFirstMint = scaledBalance == 0;
uint256 balanceIncrease = 0;
if (_userState[onBehalfOf].index != 0 && _userState[onBehalfOf].index < index) {
@> balanceIncrease = scaledBalance.rayMul(index) - scaledBalance.rayMul(_userState[onBehalfOf].index);
}
_userState[onBehalfOf].index = index.toUint128();
@> uint256 amountToMint = amount + balanceIncrease;
_mint(onBehalfOf, amountToMint.toUint128());
emit Transfer(address(0), onBehalfOf, amountToMint);
emit Mint(user, onBehalfOf, amountToMint, balanceIncrease, index);
return (scaledBalance == 0, amountToMint, totalSupply());
}

balanceIncrease is calculated and added to amountToMint. However, balanceIncrease is a value that will increase as the index increases, so it doesn't need to be added. However, since it’s adding it once more, the interest is doubled.

04. Impact

  • The user has to pay more interest on the loan than expected.

  • This causes the user to lose assets.

05. Proof of Concept

You can test the following PoC with the forge test --mt test_poc_user_debt -vv command. Initially borrow 50e18 and then borrow 50e18 after some time. If you look at the log, the interest charged after the time is 9935741025606941332, but when you borrow again, more interest is added.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Test, console} from "forge-std/Test.sol";
import {crvUSDToken} from "src/mocks/core/tokens/crvUSDToken.sol";
import {RAACHousePrices} from "src/core/primitives/RAACHousePrices.sol";
import {RAACNFT} from "src/core/tokens/RAACNFT.sol";
import {RToken} from "src/core/tokens/RToken.sol";
import {DebtToken} from "src/core/tokens/DebtToken.sol";
import {LendingPool} from "src/core/pools/LendingPool/LendingPool.sol";
import {ReserveLibrary} from "src/libraries/pools/ReserveLibrary.sol";
contract BaseTest is Test {
crvUSDToken public crvUSDTokenInstance;
RAACHousePrices public raacHousePricesInstance;
RAACNFT public raacNFTInstance;
RToken public rTokenInstance;
DebtToken public debtTokenInstance;
LendingPool public lendingPoolInstance;
address alice = makeAddr("alice");
address bob = makeAddr("bob");
address hyuunn = makeAddr("hyuunn");
function setUp() public {
// crvUSDToken deploy
crvUSDTokenInstance = new crvUSDToken(address(this));
// raacHousePrices deploy
raacHousePricesInstance = new RAACHousePrices(address(this));
raacHousePricesInstance.setOracle(address(this));
// raacNFT deploy
raacNFTInstance = new RAACNFT(
address(crvUSDTokenInstance),
address(raacHousePricesInstance),
address(this)
);
_mintRaacNFT();
rTokenInstance = new RToken(
"RToken",
"RTK",
address(this),
address(crvUSDTokenInstance)
);
debtTokenInstance = new DebtToken("DebtToken", "DEBT", address(this));
lendingPoolInstance = new LendingPool(
address(crvUSDTokenInstance),
address(rTokenInstance),
address(debtTokenInstance),
address(raacNFTInstance),
address(raacHousePricesInstance),
0.1e27
);
rTokenInstance.setReservePool(address(lendingPoolInstance));
debtTokenInstance.setReservePool(address(lendingPoolInstance));
}
function _mintRaacNFT() internal {
// housePrices setting
raacHousePricesInstance.setHousePrice(0, 100e18);
raacHousePricesInstance.setHousePrice(1, 50e18);
raacHousePricesInstance.setHousePrice(2, 150e18);
// crvUSDToken mint
deal(address(crvUSDTokenInstance), alice, 1000e18);
deal(address(crvUSDTokenInstance), bob, 1000e18);
deal(address(crvUSDTokenInstance), hyuunn, 1000e18);
// raacNFT mint
vm.startPrank(alice);
crvUSDTokenInstance.approve(address(raacNFTInstance), 100e18 + 1);
raacNFTInstance.mint(0, 100e18 + 1);
vm.stopPrank();
vm.startPrank(bob);
crvUSDTokenInstance.approve(address(raacNFTInstance), 50e18 + 1);
raacNFTInstance.mint(1, 50e18 + 1);
vm.stopPrank();
}
function test_poc_user_debt() public {
// 1. bob deposit, depositNFT, borrow
vm.startPrank(bob);
crvUSDTokenInstance.approve(address(lendingPoolInstance), 500e18);
lendingPoolInstance.deposit(500e18);
raacNFTInstance.approve(address(lendingPoolInstance), 1);
lendingPoolInstance.depositNFT(1);
lendingPoolInstance.borrow(10e18);
vm.stopPrank();
// 2. alice depositNFT 0 and borrow
vm.startPrank(alice);
raacNFTInstance.approve(address(lendingPoolInstance), 0);
lendingPoolInstance.depositNFT(0);
assertEq(lendingPoolInstance.getUserCollateralValue(alice), 100e18);
lendingPoolInstance.borrow(50e18);
assertEq(debtTokenInstance.balanceOf(alice), 50e18);
console.log("Alice debt: ", debtTokenInstance.balanceOf(alice));
vm.stopPrank();
// time warp
vm.warp(block.timestamp + 365 days * 5);
lendingPoolInstance.updateState();
console.log("Alice debt over time: ", debtTokenInstance.balanceOf(alice));
uint256 before = debtTokenInstance.balanceOf(alice);
// 3. alice borrow more
// alice's debt increases.
vm.startPrank(alice);
lendingPoolInstance.borrow(50e18);
console.log("Alice's debt for borrowing an additional 50e18: ", debtTokenInstance.balanceOf(alice));
console.log("Alice's Increased debt: ", debtTokenInstance.balanceOf(alice) - before - 50e18);
vm.stopPrank();
}
}
Ran 1 test for test/BaseSetup.s.sol:BaseTest
[PASS] test_poc_user_debt() (gas: 882007)
Logs:
Alice debt: 50000000000000000000
Alice debt over time: 59935741025606941332
Alice's debt for borrowing an additional 50e18: 121845861045772460152
Alice's Increased debt: 11910120020165518820
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 13.13ms (2.58ms CPU time)

06. Tools Used

Manual Code Review and Foundry

07. Recommended Mitigation

function mint(
address user,
address onBehalfOf,
uint256 amount,
uint256 index
) external override onlyReservePool returns (bool, uint256, uint256) {
if (user == address(0) || onBehalfOf == address(0)) revert InvalidAddress();
if (amount == 0) {
return (false, 0, totalSupply());
}
uint256 amountScaled = amount.rayDiv(index);
if (amountScaled == 0) revert InvalidAmount();
uint256 scaledBalance = balanceOf(onBehalfOf);
bool isFirstMint = scaledBalance == 0;
uint256 balanceIncrease = 0;
if (_userState[onBehalfOf].index != 0 && _userState[onBehalfOf].index < index) {
balanceIncrease = scaledBalance.rayMul(index) - scaledBalance.rayMul(_userState[onBehalfOf].index);
}
_userState[onBehalfOf].index = index.toUint128();
- uint256 amountToMint = amount + balanceIncrease;
+ uint256 amountToMint = amount;
_mint(onBehalfOf, amountToMint.toUint128());
emit Transfer(address(0), onBehalfOf, amountToMint);
emit Mint(user, onBehalfOf, amountToMint, balanceIncrease, index);
return (scaledBalance == 0, amountToMint, totalSupply());
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Validated
Assigned finding tags:

DebtToken::mint miscalculates debt by applying interest twice, inflating borrow amounts and risking premature liquidations

Support

FAQs

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