Core Contracts

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

Due to not counting the assets stake on crvVault the reported amount of dust will not be correct

Summary

The Protocol allowed the owner to remove the dust amount if not backed by rToken , However to Find the Assets in the pool we only check the assets owned by pool and did not count the assets deposited in crvUSD vault.

Vulnerability Details

the Core flow of assets is that we only keep the required buffer amount in the pool and deposit the rest of assets in crvUSD pool for yield generation purpose. The logic to remove the Dust from vault is based on the assets users has deposited and the rToken totalSupply if both of them are different than we send out the extra assets to out from the rToken address as rToken holds all the assets for pool operation.
The Root cause :

function calculateDustAmount() public view returns (uint256) {
@---> uint256 contractBalance = IERC20(_assetAddress).balanceOf(address(this)).rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
uint256 currentTotalSupply = totalSupply();
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;
}

Here we only fetch the assets owned by this contract. but we also deposit the extra assets on crvUsd vault here:

/contracts/core/pools/LendingPool/LendingPool.sol:817
817: function _rebalanceLiquidity() internal {
818: // if curve vault is not set, do nothing
819: if (address(curveVault) == address(0)) {
820: return;
821: }
822:
823: uint256 totalDeposits = reserve.totalLiquidity; // Total liquidity in the system
824: uint256 desiredBuffer = totalDeposits.percentMul(liquidityBufferRatio); // 24
825: uint256 currentBuffer = IERC20(reserve.reserveAssetAddress).balanceOf(reserve.reserveRTokenAddress); // 120
826:
827: if (currentBuffer > desiredBuffer) {
828: uint256 excess = currentBuffer - desiredBuffer; // 96
829: // Deposit excess into the Curve vault
830: _depositIntoVault(excess);
831: } else if (currentBuffer < desiredBuffer) {
...
837: emit LiquidityRebalanced(currentBuffer, totalVaultDeposits);
838: }

As from the above code it can be seen that we deposit the extra token in crvUSd vault and only check that assets owned by rToken and does not include the assets deposited on crvUSd vault So the final Dust amount will not correct or it will be less than the actual Dust.

As there are few issues in these code which I reported seperated I will share the code here to run the POC:

//Add this File in mocks
import {ICurveCrvUSDVault} from "../interfaces/curve/ICurveCrvUSDVault.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract MockUsdVault is ICurveCrvUSDVault {
address assetAddress;
constructor(address _assetAddress) {
assetAddress = _assetAddress;
}
function deposit(
uint256 assets,
address receiver
) external returns (uint256 shares) {
IERC20(assetAddress).transferFrom(msg.sender, address(this), assets);
return shares;
}
function withdraw(
uint256 assets,
address receiver,
address owner,
uint256 maxLoss,
address[] calldata strategies
) external returns (uint256 shares) {
IERC20(assetAddress).transfer(msg.sender, assets);
return shares;
}
function asset() external view returns (address) {
return address(this);
}
function totalAssets() external view returns (uint256) {
return 0;
}
function pricePerShare() external view returns (uint256) {
return 1e18;
}
function totalIdle() external view returns (uint256) {
return 0;
}
function totalDebt() external view returns (uint256) {
return 0;
}
function isShutdown() external view returns (bool) {
return false;
}
}

Add following line to _depositIntoVault:

IRToken(reserve.reserveRTokenAddress).transferAsset(address(this), amount);

Add this POC to LendingPool.test.js in describe("Full sequence", section:

it.only("should transfer accrued dust correctly", 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("0.00000001"));
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);
});
it.only("should not transfer accrued dust correctly because of not checking the crv vault balance", async function () {
const MockVault = await ethers.getContractFactory("MockUsdVault");
let vault = await MockVault.deploy(crvusd.target);
await lendingPool.connect(owner).setCurveVault(vault.target);
// 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"));
// Calculate dust amount
const dustAmount = await rToken.calculateDustAmount();
console.log("Dust amount:", dustAmount);
});

The above to Test case when show Dust Amount 2717422610 when all the assets owned by rToken and other show DustAmount 0 when 80% assets hold by crvUsd vault.

Impact

Due not including the assets owned by crvUSd vault , the Dust amount calculation will return less or 0 value in Dust.

Tools Used

Unit Testing, Manual Review

Recommendations

On Fix Which i suggest could be to also add all the assets deposited in crvUSd vault here , So that we will get the actual Dust Amount we have in Pool.
Add this function in ILendingPool.sol:

+ function getTotalVaultDeposits() external view returns (uint256);

Add this line to

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 assets will be deposit into crvVault
+ contractBalance +=ILendingPool(_reservePool).getTotalVaultDeposits().rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
Updates

Lead Judging Commences

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

RToken dust calculation structurally impossible with outstanding loans or funds deposited in the vault

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

RToken dust calculation structurally impossible with outstanding loans or funds deposited in the vault

Support

FAQs

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

Give us feedback!