Core Contracts

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

Lending Pool accrued dust is incorrectly calculated

Summary

Lending Pool accrued dust is incorrectly calculated.

Vulnerability Details

Lending pool borrower's debt accruing is compounding, whereas the liquidity rate is linear.

As such, lender's profit is less than the interest paid by the borrower, the transferAccruedDust() exists so those funds can be collected.

Assuming the deposited reserve asset amount by lenders is a, the borrowed amount is b, by the time the borrower repays, usageIndex is u and liquidtiyIndex is l, then the transferrable dust amount should be b * (u - l).

When transfer the dust, the way of calculation should be a + b * (u - 1) - a * l, where a + b * (u - 1) is the total reserve assets receive after repay, and a * l is the reserve assets owned by lenders.

However, in RToken, the function for calculating dust amount is wrongly implemented.

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 implementation simply does not make much sense.

Impact

The accrued dust reserve assets cannot be properly transferred.

POC

Please run forge test --mt testAudit_CannotTransferDust.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Test, console, stdError} from "forge-std/Test.sol";
import "@openzeppelin/contracts/utils/math/SafeCast.sol";
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import "../contracts/libraries/math/WadRayMath.sol";
import "../contracts/core/pools/LendingPool/LendingPool.sol";
import "../contracts/core/pools/StabilityPool/StabilityPool.sol";
import "../contracts/mocks/core/tokens/crvUSDToken.sol";
import "../contracts/core/tokens/RToken.sol";
import "../contracts/core/tokens/DebtToken.sol";
import "../contracts/core/tokens/DeToken.sol";
import "../contracts/core/tokens/RAACToken.sol";
import "../contracts/core/tokens/RAACNFT.sol";
import "../contracts/core/primitives/RAACHousePrices.sol";
import "../contracts/core/minters/RAACMinter/RAACMinter.sol";
contract Audit is Test {
using WadRayMath for uint256;
using SafeCast for uint256;
address owner = makeAddr("Owner");
LendingPool lendingPool;
StabilityPool stabilityPool;
RAACHousePrices raacHousePrices;
crvUSDToken crvUSD;
RToken rToken;
DebtToken debtToken;
DEToken deToken;
RAACToken raacToken;
RAACNFT raacNft;
RAACMinter raacMinter;
function setUp() public {
vm.warp(1 days);
raacHousePrices = new RAACHousePrices(owner);
crvUSD = new crvUSDToken(owner);
rToken = new RToken("RToken", "RToken", owner, address(crvUSD));
debtToken = new DebtToken("DebtToken", "DT", owner);
raacNft = new RAACNFT(address(crvUSD), address(raacHousePrices), owner);
deToken = new DEToken("DEToken", "DEToken", owner, address(rToken));
raacToken = new RAACToken(owner, 100, 50);
lendingPool = new LendingPool(
address(crvUSD),
address(rToken),
address(debtToken),
address(raacNft),
address(raacHousePrices),
0.1e27
);
lendingPool.transferOwnership(owner);
// Deploy stabilityPool Proxy
bytes memory data = abi.encodeWithSelector(
StabilityPool.initialize.selector,
address(rToken),
address(deToken),
address(raacToken),
address(owner),
address(crvUSD),
address(lendingPool)
);
address stabilityPoolProxy = address(
new TransparentUpgradeableProxy(
address(new StabilityPool(owner)),
owner,
data
)
);
stabilityPool = StabilityPool(stabilityPoolProxy);
raacMinter = new RAACMinter(
address(raacToken),
address(stabilityPool),
address(lendingPool),
owner
);
vm.startPrank(owner);
raacHousePrices.setOracle(owner);
rToken.setReservePool(address(lendingPool));
debtToken.setReservePool(address(lendingPool));
raacToken.setMinter(address(raacMinter));
stabilityPool.setRAACMinter(address(raacMinter));
lendingPool.setStabilityPool(address(stabilityPool));
vm.stopPrank();
vm.label(address(crvUSD), "crvUSD");
vm.label(address(rToken), "RToken");
vm.label(address(debtToken), "DebtToken");
vm.label(address(deToken), "DEToken");
vm.label(address(raacToken), "RAACToken");
vm.label(address(raacNft), "RAAC NFT");
vm.label(address(lendingPool), "LendingPool");
vm.label(address(stabilityPool), "StabilityPool");
vm.label(address(raacMinter), "RAACMinter");
}
function testAudit_CannotTransferDust() public {
// Deposit liquidity
uint256 depositAmount = 1000e18;
address alice = makeAddr("Alice");
crvUSD.mint(alice, depositAmount);
vm.startPrank(alice);
crvUSD.approve(address(lendingPool), depositAmount);
lendingPool.deposit(depositAmount);
vm.stopPrank();
// Deposit NFT
uint256 nftTokenId = 1;
uint256 nftPrice = 2000e18;
vm.prank(owner);
raacHousePrices.setHousePrice(nftTokenId, nftPrice);
address bob = makeAddr("Bob");
crvUSD.mint(bob, nftPrice);
vm.startPrank(bob);
crvUSD.approve(address(raacNft), nftPrice);
raacNft.mint(nftTokenId, nftPrice);
raacNft.approve(address(lendingPool), nftTokenId);
lendingPool.depositNFT(nftTokenId);
vm.stopPrank();
// Borrow liquidity
uint256 borrowAmount = 1000e18;
vm.prank(bob);
lendingPool.borrow(borrowAmount);
vm.warp(block.timestamp + 365 days);
lendingPool.updateState();
// Repay debt
uint256 bobDebt = lendingPool.getUserDebt(bob);
crvUSD.mint(bob, bobDebt - borrowAmount);
vm.startPrank(bob);
crvUSD.approve(address(lendingPool), bobDebt);
lendingPool.repay(bobDebt);
vm.stopPrank();
// Transfer Dust
vm.prank(owner);
// Dust cannot be transferred
vm.expectRevert(IRToken.NoDust.selector);
lendingPool.transferAccruedDust(owner, type(uint256).max);
}
}

Tools Used

Manual Review

Recommendations

Dust amount should be calculated as below (note protocol fee is ignored for simplicity):

function calculateDustAmount() public view returns (uint256) {
uint256 contractBalance = IERC20(_assetAddress).balanceOf(address(this));
// Calculate the total real obligations to the token holders
uint256 currentTotalSupply = totalSupply();
// All balance, that is not tied to rToken are dust (can be donated or is the rest of exponential vs linear)
return contractBalance <= currentTotalSupply ? 0 : contractBalance - currentTotalSupply;
}
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!