Core Contracts

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

`RToken::calculateDustAmount` are incorrectly calculated, leading to not be able to transfer the accrued dust amount

Summary

dust amount accrued inside RTokenis supposed to be transferred out by calling transferAccruedDust. but there are flaw when calculating dust amount inside calculateDustAmountfunction, leading to incorrectly state that there are no dust amount when actually there are discrepancy in balance between the asset and the RToken normalized balance.

Vulnerability Details

function calculateDustAmount is used to check if
there are dust amount inside the RToken contract.

RToken.sol#L317-L328

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 function would compare if the contract's asset balance would be greater thanRToken total supply, if that the case then there are dust amount inside the contract that can be transferred out.

notice that the contract's asset balance is divided by the index, so we should compare it with scaled amount version of the rToken balance.

if we check the function totalSupply the amount returned already multiplied by current index income (normalized).

the issue is when calculating the totalRealBalancewhere the amount of currentTotalSupplythat already multiplied by the index income are getting multiplied again, this would make the totalRealBalanceinflated so much that it makes the comparison of contractBalance <= totalRealBalancewould result to true, and making the dust amount reported by this function to be 0.

to run PoC provided, use the detailed step below:

  1. Run npm i --save-dev @nomicfoundation/hardhat-foundry in the terminal to install the hardhat-foundry plugin.

  2. Add require("@nomicfoundation/hardhat-foundry"); to the top of the hardhat.config.cjs file.

  3. Run npx hardhat init-foundry in the terminal.

  4. rename ReserveLibraryMock.solto ReserveLibraryMock.sol.bakinside test/unit/librariesfolder so it does not throw error.

  5. Create a file “Test.t.sol” in the “test/” directory and paste the provided PoC.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../contracts/core/tokens/RToken.sol";
import "../contracts/mocks/core/tokens/ERC20Mock.sol";
import "../contracts/libraries/math/WadRayMath.sol";
contract MockLendingPool {
RToken public rToken;
uint256 normalizedIncome;
constructor(address _rToken) {
rToken = RToken(_rToken);
normalizedIncome = 1e27;
}
function setNormalizedIncome(uint256 _normalizedIncome) external {
normalizedIncome = _normalizedIncome;
}
function getNormalizedIncome() external view returns (uint256) {
return normalizedIncome;
}
function mockMint(address caller, address onBehalfOf, uint256 amount) external returns (bool, uint256, uint256, uint256) {
return rToken.mint(caller, onBehalfOf, amount, normalizedIncome);
}
}
contract TestRToken is Test {
ERC20Mock public assetToken;
RToken public rToken;
MockLendingPool public lendingPool;
using WadRayMath for uint256;
address owner = makeAddr("owner");
address borrower = makeAddr("borrower");
address lender = makeAddr("lender");
address alice = makeAddr("alice");
function setUp() public {
vm.startPrank(owner);
assetToken = new ERC20Mock("Asset Token", "ATKN");
rToken = new RToken("RToken", "RTKN", owner, address(assetToken));
lendingPool = new MockLendingPool(address(rToken));
rToken.setReservePool(address(lendingPool));
// mint asset tokens
assetToken.mint(address(borrower), 1000 ether);
assetToken.mint(address(lender), 1000 ether);
assetToken.mint(address(alice), 1000 ether);
vm.stopPrank();
}
function test_dustAmountNotCorrectlyCompared() public {
// mint some RToken
uint256 index = 1.1e27;
lendingPool.setNormalizedIncome(index);
// we are using 100 ether of asset token to mint rToken
uint256 amount = 100 ether;
lendingPool.mockMint(address(lendingPool), lender, amount);
// because we are using mock, the contract does not have any asset token currently, we need to transfer 100 ether of asset token to the contract
vm.prank(lender);
assetToken.transfer(address(rToken), amount);
// simulate 10 ether of asset as dust by transferring it to the contract
uint256 dustAmount = 10 ether;
vm.startPrank(lender);
assetToken.transfer(address(rToken), dustAmount);
vm.stopPrank();
// now the contract has 110 ether of asset token and 100 ether of rToken (normalized)
// so the dust amount should be 10 ether because 110 ether > 100 ether
// we calculate the scaled version of dust
uint256 scaledDustAmount = dustAmount.rayDiv(index);
// check dust amount (in scaled form)
uint256 calculatedDust = rToken.calculateDustAmount();
console.log("dust: ", calculatedDust);
assertEq(calculatedDust, scaledDustAmount);
}
}

run the following command forge t --mt test_dust -vvthe result would fail:

Ran 1 test for test/RToken.t.sol:TestRToken
[FAIL: assertion failed: 0 != 10000000000000000000] test_dustAmountNotCorrectlyCompared() (gas: 151616)
Logs:
asset token dust amount in contract: 0
Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 1.17ms (179.45µs CPU time)
Ran 1 test suite in 8.70ms (1.17ms CPU time): 0 tests passed, 1 failed, 0 skipped (1 total tests)
Failing tests:
Encountered 1 failing test in test/RToken.t.sol:TestRToken
[FAIL: assertion failed: 0 != 10000000000000000000] test_dustAmountNotCorrectlyCompared() (gas: 151616)

Impact

the asset dust amount would be stuck inside the contract and cant be transferred out unless the dust amount is larger than total asset balance times index^2

Tools Used

manual review

Recommendations

because this function return value are getting used as the transfer amount of assetToken, then we should normalize all the value inside it before comparing it:

diff --git a/contracts/core/tokens/RToken.sol b/contracts/core/tokens/RToken.sol
index 25f6324..d9cfc96 100644
--- a/contracts/core/tokens/RToken.sol
+++ b/contracts/core/tokens/RToken.sol
@@ -316,13 +325,13 @@ 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());
+ uint256 contractBalance = IERC20(_assetAddress).balanceOf(address(this));
// 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());
+ uint256 totalRealBalance = currentTotalSupply;
// 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;
}

verify the function by run the test again, the result should pass:

Ran 1 test for test/RToken.t.sol:TestRToken
[PASS] test_dustAmountNotCorrectlyCompared() (gas: 149937)
Logs:
asset token dust amount in contract: 10000000000000000000
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.21ms (165.83µs CPU time)
Ran 1 test suite in 8.64ms (1.21ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 month 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

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