Core Contracts

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

Mint amount is wrong in debt token.sol

Summary
The current implementation mints the raw amount + balanceIncrease instead of using scaled values, violating the standard debt token pattern used in Aave's reference implementation. This creates two key issues:

https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/tokens/DebtToken.sol#L162

Vulnerability Details

// Current Incorrect Implementation
uint256 amountScaled = amount.rayDiv(index);
uint256 amountToMint = amount + balanceIncrease;
_mint(onBehalfOf, amountToMint.toUint128());

Proof of concept

const { expect } = require("chai");
const { ethers, network } = require("hardhat");
describe("DebtToken Vulnerability PoC", function () {
const RAY = ethers.BigNumber.from("1000000000000000000000000000"); // 1e27
const INCREASED_INDEX = ethers.BigNumber.from("1100000000000000000000000000"); // 1.1e27
const BORROW_AMOUNT = ethers.BigNumber.from("100000000000000000000"); // 100e18
let owner, user;
let dummyLendingPool, debtToken;
before(async function () {
[owner, user] = await ethers.getSigners();
// Deploy DummyLendingPool
const DummyLendingPool = await ethers.getContractFactory("DummyLendingPool");
dummyLendingPool = await DummyLendingPool.deploy();
await dummyLendingPool.deployed();
// Deploy DebtToken with owner as initialOwner
const DebtToken = await ethers.getContractFactory("DebtToken");
debtToken = await DebtToken.deploy("Debt Token", "DBT", owner.address);
await debtToken.deployed();
// Set reserve pool to DummyLendingPool address (from owner)
await debtToken.setReservePool(dummyLendingPool.address);
// Fund the DummyLendingPool address for impersonation
await network.provider.send("hardhat_setBalance", [
dummyLendingPool.address,
"0x3635C9ADC5DEA00000", // 1000 ETH in hex
]);
});
it("demonstrates over-mint vulnerability", async function () {
// Set normalizedDebt to RAY initially
await dummyLendingPool.setNormalizedDebt(RAY);
// Impersonate DummyLendingPool (the onlyReservePool required for minting)
await network.provider.request({
method: "hardhat_impersonateAccount",
params: [dummyLendingPool.address],
});
const reserveSigner = await ethers.getSigner(dummyLendingPool.address);
// First mint call at index = RAY
let tx1 = await debtToken.connect(reserveSigner).mint(
user.address,
user.address,
BORROW_AMOUNT,
RAY
);
let receipt1 = await tx1.wait();
// (For clarity, we simply fetch totalSupply after first mint)
const supplyAfterFirst = await debtToken.totalSupply();
// Update normalizedDebt to INCREASED_INDEX (simulate interest accrual)
await dummyLendingPool.setNormalizedDebt(INCREASED_INDEX);
// Second mint call at increased index
let tx2 = await debtToken.connect(reserveSigner).mint(
user.address,
user.address,
BORROW_AMOUNT,
INCREASED_INDEX
);
let receipt2 = await tx2.wait();
const supplyAfterSecond = await debtToken.totalSupply();
console.log("Supply after first mint:", supplyAfterFirst.toString());
console.log("Supply after second mint:", supplyAfterSecond.toString());
// Stop impersonation
await network.provider.request({
method: "hardhat_stopImpersonatingAccount",
params: [dummyLendingPool.address],
});
});
});

Impact

  • Over-minting creates artificial debt inflation
    -User borrows 100 DAI when index=1.0

  • Later borrows another 100 DAI when index=1.1

  • The current code would mint 210 tokens (100 + 100 + 10 interest)

  • Should mint 100/1.0 + 100/1.1 ≈ 190.91 tokens

  • The protocol shows 210 DAI debt owed but only 200 DAI lent out

Recommendations

function mint(...) external override onlyReservePool returns (...) {
// ... existing validation ...
uint256 amountScaled = amount.rayDiv(index);
if (amountScaled == 0) revert InvalidAmount();
uint256 previousScaledBalance = super.balanceOf(onBehalfOf);
bool isFirstMint = previousScaledBalance == 0;
// Calculate balance increase using scaled values
uint256 balanceIncrease = previousScaledBalance.rayMul(index - _userState[onBehalfOf].index);
_userState[onBehalfOf].index = index.toUint128();
// Mint only the scaled amount
_mint(onBehalfOf, amountScaled);
// Emit correct values
emit Transfer(address(0), onBehalfOf, amountScaled);
emit Mint(user, onBehalfOf, amount, balanceIncrease, index);
return (isFirstMint, amountScaled, totalSupply());
}

Adhere to the variable debt token by aave
https://github.com/aave/aave-v3-core/blob/782f51917056a53a2c228701058a6c3fb233684a/contracts/protocol/tokenization/base/ScaledBalanceTokenBase.sol

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!