Core Contracts

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

Incorrect calculation in calculateDustAmount() leads to inability to transfer accrued dust

Description

calculateDustAmount() is supposed to return the amount of crvUSD that is not tied to rToken. However the logic inside the function is incorrect and under-reports the dust amount. It should be:

File: contracts/core/tokens/RToken.sol
317: function calculateDustAmount() public view returns (uint256) {
318: // Calculate the actual balance of the underlying asset held by this contract
- 319: uint256 contractBalance = IERC20(_assetAddress).balanceOf(address(this)).rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
+ 319: uint256 contractBalance = IERC20(_assetAddress).balanceOf(address(this));
320:
- 321: // Calculate the total real obligations to the token holders
- 322: uint256 currentTotalSupply = totalSupply();
323:
324: // Calculate the total real balance equivalent to the total supply
- 325: uint256 totalRealBalance = currentTotalSupply.rayMul(ILendingPool(_reservePool).getNormalizedIncome());
+ 325: uint256 totalRealBalance = totalSupply();
326: // All balance, that is not tied to rToken are dust (can be donated or is the rest of exponential vs linear)
327: return contractBalance <= totalRealBalance ? 0 : contractBalance - totalRealBalance;
328: }
  • The IERC20(_assetAddress).balanceOf(address(this)) on L319 gives the actual amount of crvUSD in the contract, there is no need to divide it further by liquidity index.

  • totalSupply() called on L322 returns the scaledRTokenSupply * liquidityIndex so there is no need to multiply it further on L325.

  • Simply compare the two.

Impact

transferAccruedDust() can't transfer the dust amount correctly since calculateDustAmount() under-reports it and hence contract has dust crvUSD stuck in it.

Proof of Concept

Add this inside LendingPool.test.js and run to see it pass with the following output:

it("discovers bug in calculateDustAmount", async function () {
// log initial values
const rTokenInitialBalance = await rToken.balanceOf(user2.address);
const crvUSDInitialBalance = await crvusd.balanceOf(user2.address);
console.log("initial rToken balance =", ethers.formatEther(rTokenInitialBalance));
console.log("initial rToken balance (scaled) =", ethers.formatEther(await rToken.scaledBalanceOf(user2.address)));
expect(rTokenInitialBalance).to.equal(ethers.parseEther("1000"));
console.log("old liquidity index =", ethers.formatUnits(await lendingPool.getNormalizedIncome(), 27));
expect(await lendingPool.getNormalizedIncome()).to.equal(ethers.parseUnits("1", 27));
// Simulate index update to 1.025 (2.5% increase):
// User1 deposits NFT and borrows to increase liquidity index
const tokenId2 = 2;
const amountToPay2 = ethers.parseEther("600");
await raacHousePrices.setOracle(owner.address);
await raacHousePrices.setHousePrice(tokenId2, amountToPay2);
await token.mint(user1.address, amountToPay2);
await token.connect(user1).approve(raacNFT.target, amountToPay2);
await raacNFT.connect(user1).mint(tokenId2, amountToPay2);
await raacNFT.connect(user1).approve(lendingPool.target, tokenId2);
await lendingPool.connect(user1).depositNFT(tokenId2);
const borrowAmount = ethers.parseEther("400");
await lendingPool.connect(user1).borrow(borrowAmount);
// skip time
console.log("\nskipping time after borrow...\n");
await ethers.provider.send("evm_increaseTime", [365 * 24 * 60 * 60]);
await ethers.provider.send("evm_mine", []);
await lendingPool.connect(user2).updateState();
let newLiquidityIndex = await lendingPool.getNormalizedIncome();
console.log("new liquidity index =", ethers.formatUnits(newLiquidityIndex, 27));
expect(newLiquidityIndex).to.be.closeTo(ethers.parseUnits("1.025", 27), ethers.parseUnits("1", 18));
console.log("new rToken balance =", ethers.formatEther(await rToken.balanceOf(user2.address)));
console.log("new rToken balance (scaled) =", ethers.formatEther(await rToken.scaledBalanceOf(user2.address)));
expect(await crvusd.balanceOf(user2.address)).to.equal(crvUSDInitialBalance);
expect(await rToken.balanceOf(user2.address)).to.be.closeTo(ethers.parseEther("1025"), ethers.parseEther("1"));
// donate crvUSD to simulate DUST
await crvusd.mint(rToken.target, ethers.parseEther("500")); // DUST should be = 600 + 500 - 1025 = 75 ether
// check dust
console.log("dust amount in RToken contract =", ethers.formatEther(await rToken.calculateDustAmount()));
});

Output:

LendingPool
initial rToken balance = 1000.0
initial rToken balance (scaled) = 1000.0
old liquidity index = 1.0
skipping time after borrow...
new liquidity index = 1.025
new rToken balance = 1025.0
new rToken balance (scaled) = 1000.0
dust amount in RToken contract = 22.545731707317073171 ❌ <----- should have been 600 + 500 - 1025 = 75
✔ discovers bug in calculateDustAmount (117ms)
1 passing (7s)

Mitigation

Shown in the Description section.

Updates

Lead Judging Commences

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

RToken::calculateDustAmount incorrectly applies liquidity index, severely under-reporting dust amounts and permanently trapping crvUSD in contract

Support

FAQs

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