Core Contracts

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

applying rayMul twice in currentTotalSupply will impact the dust amount being transfer , apply twice will increase the current supply artificially

Summary

The Protocol allowed the owner to remove the dust amount if not backed by rToken , However to Find the totalSupply of rToken the rayMul is applied twice which will artificially increase the totalSupply of rToken.

Vulnerability Details

The LendingPool could accumulate Dust amount overTime which will be removed by owner of the pool via calling transferAccruedDust.

/contracts/core/pools/LendingPool/LendingPool.sol:784
784: function transferAccruedDust(address recipient, uint256 amount) external onlyOwner {
785: // update state
786: ReserveLibrary.updateReserveState(reserve, rateData);
787: require(recipient != address(0), "LendingPool: Recipient cannot be zero address");
788: IRToken(reserve.reserveRTokenAddress).transferAccruedDust(recipient, amount);
789: }

First we get the Dust Amount available in the pool.

function calculateDustAmount() public view returns (uint256) {
// Calculate the actual balance of the underlying asset held by this contract
uint256 contractBalance = IERC20(_assetAddress).balanceOf(address(this)).rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
// Calculate the total real obligations to the token holders
@--> uint256 currentTotalSupply = totalSupply();
// Calculate the total real balance equivalent to the total supply
uint256 totalRealBalance = currentTotalSupply.rayMul(ILendingPool(_reservePool).getNormalizedIncome());
// All balance, that is not tied to rToken are dust (can be donated or is the rest of exponential vs linear)
return contractBalance <= totalRealBalance ? 0 : contractBalance - totalRealBalance;
}

The logic is that we check if there is any assets in the pool which is not backed by rToken than we transfer those assets out from pool.
Following step will help us to find the unbacked assets:

  1. We fetch the assets balance of rToken.

  2. Fetch the totalSupply of rToken.

  3. Than we apply liquidityIndex on totalSupply again.

  4. Than we check if there is assets without rToken backing than we withdraw extra assets.
    The Issue in above flow is that when we fetch totalSupply it applies rayMul and return scaled balance.

function totalSupply() public view override(ERC20, IERC20) returns (uint256) {
return super.totalSupply().rayMul(ILendingPool(_reservePool).getNormalizedIncome());
}

and calculateDustAmount function we again apply rayMul which return wrong totalSupply:

totalSupply unscaled : 99999999996353916006
liquidityIndex : 1000000000036460839945676932
totalSupply scaled : 100000000000000000000
# as we apply it twice so
totalSupply scaled : 100000000003646083995

As it can be seen from above example that the totalSupply is artificially increased by 3646083995.
Add Following POC to LendingPool.test.js in describe("Full sequence", section:

it.only("apply rayMul on total supply twice", async function () {
const MockVault = await ethers.getContractFactory("MockUsdVault");
// create obligations
await crvusd.connect(user1).approve(lendingPool.target, ethers.parseEther("100"));
await lendingPool.connect(user1).deposit(ethers.parseEther("100"));
await crvusd.connect(owner).mint(owner.address, ethers.parseEther("1000"));
await crvusd.connect(owner).transfer(rToken.target, ethers.parseEther("1"));
await rToken.connect(owner).setReservePool(lendingPool.target);
let index = await lendingPool.getNormalizedIncome();
console.log("index" , index);
// Calculate dust amount
const dustAmount = await rToken.calculateDustAmount();
console.log("Dust amount:", dustAmount);
});
Logs:
Dust amount: 2717422734

Impact

Due To applying rayMul twice the dust amount return is not correct , which will not allow to remove the full dust from pool.

Tools Used

Unit Testing, Manual Review

Recommendations

One Recommended Fix could be :

@@ -318,16 +319,10 @@ contract RToken is ERC20, ERC20Permit, IRToken, Ownable {
function calculateDustAmount() public view returns (uint256) {
// Calculate the actual balance of the underlying asset held by this contract
uint256 contractBalance = IERC20(_assetAddress).balanceOf(address(this)).rayDiv(ILendingPool(_reservePool).getNormalizedIncome()); // @audit : this calculation is not correct as the as
sets will be deposit into crvVault
- console.log("actual contract bal" , contractBalance);
// contractBalance +=ILendingPool(_reservePool).getTotalVaultDeposits().rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
// Calculate the total real obligations to the token holders
uint256 currentTotalSupply = totalSupply(); // @audit : it is already Mul with the index but in next call we again do it.
// Calculate the total real balance equivalent to the total supply
- // uint256 totalRealBalance = currentTotalSupply.rayMul(ILendingPool(_reservePool).getNormalizedIncome());
// All balance, that is not tied to rToken are dust (can be donated or is the rest of exponential vs linear)
- return contractBalance <= totalRealBalance ? 0 : contractBalance - totalRealBalance;
+ return contractBalance <= currentTotalSupply ? 0 : contractBalance - currentTotalSupply;
}

After runing again newDust amount is :

Logs:
Dust amount: 6363506729
Updates

Lead Judging Commences

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

Give us feedback!