Core Contracts

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

Transferable Debt Tokens Enable Debt Accounting Incongruity and Reserve Drain

Summary

The vulnerability resides in the DebtToken's transferability (Lines not explicitly restricted in DebtToken.sol), allowing attackers to separate debt ownership from protocol accounting. By transferring DebtTokens, malicious actors can manipulate debt repayments, leading to protocol insolvency and corrupted user debt tracking.


Vulnerability Details

Technical Analysis

The LendingPool contract tracks user debt via the scaledDebtBalance variable but calculates repayable amounts using the ERC20 balanceOf method of the DebtToken. If the DebtToken is transferable (default ERC20 behavior), attackers can:

  1. Transfer DebtTokens to other addresses.

  2. Exploit the fragmented debt ownership to repay inflated balances.

  3. Cause underflows in scaledDebtBalance or drain reserves via over-repayment.

Affected Code Snippets:

  1. DebtToken Transfer Function (assuming standard ERC20):

    function transfer(address to, uint256 amount) public returns (bool) {
    // No restrictions on transfers
    _transfer(msg.sender, to, amount);
    return true;
    }
  2. Repayment Logic in LendingPool (Lines 312-335):

    function _repay(uint256 amount, address onBehalfOf) internal {
    uint256 userDebt = IDebtToken(...).balanceOf(onBehalfOf); // Uses ERC20 balance
    // ...
    IDebtToken(...).burn(onBehalfOf, amount, reserve.usageIndex); // Burns transferred tokens
    user.scaledDebtBalance -= amountBurned; // Risk of underflow
    }

Impact

  1. Protocol Insolvency: Attackers repay debts using transferred tokens, draining reserves without collateral.

  2. Debt Tracking Corruption: Underflows in scaledDebtBalance disrupt interest calculations.

  3. Free Debt Arbitrage: Malicious users "sell" debt tokens to unwitting parties, profiting from protocol flaws.


Tools Used

  1. Manual Code Review: Identified reliance on transferable DebtToken balances.

  2. Hardhat: Simulated token transfers and repayment inconsistencies.

  3. Slither: Detected unsafe ERC20 balance usage in critical logic.


Proof of Concept (PoC)

Overview:

An attacker transfers DebtTokens to a victim, tricking the protocol into accepting over-repayments that corrupt debt tracking.

Actors:

  • Attacker: Transfers debt tokens to exploit repayment logic.

  • Victim: Repays inflated debt using transferred tokens.

  • Protocol: Incorrectly reduces debt balances, enabling reserve theft.

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 by default)
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, // reserveAsset (mock)
ethers.constants.AddressZero, // rToken (mock)
debtToken.address, // DebtToken
ethers.constants.AddressZero, // raacNFT (mock)
ethers.constants.AddressZero, // priceOracle (mock)
ethers.utils.parseUnits("1", 27) // primeRate
);
await pool.deployed();
// Simulate borrowing: Attacker borrows 100 units
await debtToken.mint(attacker.address, ethers.utils.parseEther("100"));
await debtToken.connect(attacker).approve(pool.address, ethers.constants.MaxUint256);
await pool.connect(attacker).borrow(ethers.utils.parseEther("100"));
});
it("Exploit: Over-repay using transferred DebtTokens", async () => {
// 1. Attacker transfers 50 DebtTokens to Victim
await debtToken.connect(attacker).transfer(victim.address, ethers.utils.parseEther("50"));
// 2. Victim repays 150 units (50 transferred + 100 actual)
// Victim's balance = 50 (transferred) + 0 (actual debt) = 50
// Protocol allows repayment up to balance (50), but attacker's debt is 100
await debtToken.mint(victim.address, ethers.utils.parseEther("150")); // Simulate reserve assets
await debtToken.connect(victim).approve(pool.address, ethers.constants.MaxUint256);
await pool.connect(victim).repay(ethers.utils.parseEther("150"));
// 3. Attacker's debt is now 100 - 150 = underflow (reverts in Solidity 0.8+)
// Protocol state corrupted!
});
});

Recommendations

  1. Disable DebtToken Transfers:
    Override ERC20 transfer functions in DebtToken:

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

    uint256 userDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex);
  3. Validate Debt Ownership:
    Add a modifier to ensure only the debt owner can repay:

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

Conclusion

Transferable DebtTokens decouple ERC20 balances from protocol debt tracking, enabling reserve drainage and state corruption. Immediate mitigation requires disabling transfers and revising repayment logic to use internal debt records.

Updates

Lead Judging Commences

inallhonesty Lead Judge
9 months ago
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!