Core Contracts

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

[H-08] Erroneous implementation of Index-based tokens leads to increased amount of DebtToken minted per borrow()

Summary

Whenever a user borrow() an amount of liquidity against their NFT collateral, and it's not their first time borrowing, they will be charged more interest due to an error in the way that the usage index is used in the mint() function of DebtToken.

Vulnerability Details

The idea of an index-based system is to keep track of the interest accrued by a token via an index variable, this variable is updated before performing any operation with the token in order to account for the new interest.

With the implementation used, both DebtToken and RToken use this approach, the former to account for the interest accrued by the user towards the pool and the later from the pool to the user. In both cases a token trades 1:1 with their underlying at all times, ie to pay a debt of 1 DebtToken the user needs to provide 1 unit of the underlying asset.

The issue here is that the mint() function tries to perform the job already done by the index and adds increaseBalance to the amount to mint.
However this amount is already accounted for by the change in index, which is applied by multiplying the stored balance (which is index-agnostic since it's stored after being divided by the index in _update()) with the current index.
This only happens after the first mint by the user and if there's been an index change since the last mint, as shown by the condition _userState[onBehalfOf].index != 0 && _userState[onBehalfOf].index < index.

Location

Impact

The increaseBalance variable doubles up on the adjustment performed by the index, increasing the amount of DebtToken per mint() beyond what's expected by the user, saddling them with unexpected debt and making the protocol less trustworthy and appealing to the participants.

Tools Used

Manual review.

Recommendations

The elegant solution is to clean-up the code by deleting all uses of the userState struct since it's only used to keep track of the index and to erroneously increase the debt balance as outlined.

The easiest solution is just to delete the line of code uint256 amountToMint = amount + balanceIncrease; and just using _mint(onBehalfOf, amount.toUint128()); instead of amountToMint.

Proof of Code

As can be seen in the snippet, after the first mint, the interest is immediately charged for the second mint, making it impossible to immediately repay the debt without incurring any penalty. The longer the time between any two mints, the higher this value will be making it even more problematic.

import { expect } from "chai";
import hre from "hardhat";
const { ethers } = hre;
import { time, mine } from "@nomicfoundation/hardhat-network-helpers";
import { deployContracts } from './utils/deployContracts.js';
describe('Exploit Tests', function () {
// Set higher timeout for deployments
this.timeout(300000); // 5 minutes
let contracts;
let owner, user1, user2, user3, treasury, repairFund;
const INITIAL_MINT_AMOUNT = ethers.parseEther('1000');
const HOUSE_TOKEN_ID = '1021000';
const HOUSE_PRICE = ethers.parseEther('100');
const ONE_YEAR = 365 * 24 * 3600;
const FOUR_YEARS = 4 * ONE_YEAR;
const BASIS_POINTS = 10000;
before(async function () {
[owner, user1, user2, user3, treasury, repairFund] = await ethers.getSigners();
contracts = await deployContracts(owner, user1, user2, user3);
const displayContracts = Object.fromEntries(Object.entries(contracts).map(([key, value]) => [key, value.target]));
console.log(displayContracts);
// Set house price for testing
await contracts.housePrices.setHousePrice(HOUSE_TOKEN_ID, HOUSE_PRICE);
// Mint initial tokens to users
for (const user of [user1, user2, user3]) {
await contracts.crvUSD.mint(user.address, INITIAL_MINT_AMOUNT);
}
});
describe.only('Bugs:', function () {
it('[H-08] Erroneous implementation of Index-based tokens leads to increased amount of DebtToken minted per borrow()', async function () {
/*
NEED TO ADD THIS TO CONSTRUCTOR OF LENDING POOL TO WORK (values are arbitrary)
rateData.currentLiquidityRate = 1e25;
rateData.currentUsageRate = 1e25;
*/
const DEPOSIT_AMOUNT = ethers.parseEther('1');
// user1 deposits crvUSD into lending pool to allow user2 to borrow
await contracts.crvUSD.connect(user1).approve(contracts.lendingPool.target, DEPOSIT_AMOUNT);
await contracts.lendingPool.connect(user1).deposit(DEPOSIT_AMOUNT);
await contracts.crvUSD.connect(user1).approve(contracts.lendingPool.target, DEPOSIT_AMOUNT);
await contracts.lendingPool.connect(user1).deposit(DEPOSIT_AMOUNT);
// set price of house
await contracts.housePrices.connect(owner).setHousePrice(1, DEPOSIT_AMOUNT);
await contracts.housePrices.connect(owner).setHousePrice(2, DEPOSIT_AMOUNT);
// mint nft for user2
await contracts.crvUSD.connect(user2).approve(contracts.nft.target, INITIAL_MINT_AMOUNT);
await contracts.nft.connect(user2).mint(1, DEPOSIT_AMOUNT);
await contracts.nft.connect(user2).mint(2, DEPOSIT_AMOUNT);
// burn all of user2's crvUSD to simplify example
await contracts.crvUSD.connect(user2).burn(await contracts.crvUSD.balanceOf(user2.address));
// deposit user2's nft into lendingpool
await contracts.nft.connect(user2).setApprovalForAll(contracts.lendingPool.target, true);
await contracts.lendingPool.connect(user2).depositNFT(1);
await contracts.lendingPool.connect(user2).depositNFT(2);
// borrow using nft as collateral
await contracts.lendingPool.connect(user2).borrow(DEPOSIT_AMOUNT);
// See user2 balance
console.log("Amount of crvUSD borrowed by user2: " + await contracts.crvUSD.balanceOf(user2.address)); // 1000000000000000000
console.log("Amount of debtToken of user2: " + await contracts.debtToken.balanceOf(user2.address));
// check after a while
await mine(100);
await contracts.lendingPool.updateState();
console.log("Amount of debtToken of user2 (after some mining): " + await contracts.debtToken.balanceOf(user2.address)); // 1000000225635013714
// More borrowing to trigger the "balanceIncrease" bug
await contracts.lendingPool.connect(user2).borrow(DEPOSIT_AMOUNT);
console.log("Amount of debtToken of user2 (after more borrowing): " + await contracts.debtToken.balanceOf(user2.address)); // 2000000451270078840
});
});
});
Updates

Lead Judging Commences

inallhonesty Lead Judge 7 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.

Give us feedback!