Core Contracts

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

Double Scaling in `RToken.transfer` Function Leads to Under-Transfer of Funds

Summary:

The RToken contract's transfer function incorrectly scales down the transfer amount twice, leading to users transferring significantly less than intended. This double scaling occurs because the user's balance gives underlying amount as transfer amount, and the transfer function scales it down before calling the internal _update function, which also scales it down.

Vulnerability Details:

The transfer function in the RToken contract has a critical flaw in its scaling logic:

function transfer(address recipient, uint256 amount) public override(ERC20, IERC20) returns (bool) {
// amount is scaled up by normalized income, to reflect interest accrued
uint256 scaledAmount = amount.rayDiv(ILendingPool(_reservePool).getNormalizedIncome()); // Incorrect: Should be rayMul
return super.transfer(recipient, scaledAmount);
}
function _update(address from, address to, uint256 amount) internal override {
// scaled amount are scaled down here
uint256 scaledAmount = amount.rayDiv(ILendingPool(_reservePool).getNormalizedIncome()); // Correct scaling happens here.
super._update(from, to, scaledAmount);
}
  1. Incorrect Initial Scaling: The transfer function divides the amount by the normalized income using rayDiv. This is the inverse of what should happen. The amount represents the user's intended transfer amount in underlying asset units. Since the user's amount is in scaled down form, the amount needs to be scaled up using rayMul to get the correct scaled amount to transfer.

  2. Double Scaling in _update: The _update function also scales the amount down by the normalized income. Because the amount has already been incorrectly scaled down in the transfer function, this second scaling results in a double scaling, drastically reducing the actual transfer amount.

Impact:

  • Significant Loss of Funds: Users transfer a fraction of the amount they intend to, leading to a substantial loss of funds. The magnitude of the loss is directly related to the normalized income at the time of transfer.

  • Inconsistent State Changes: The double scaling can lead to inconsistent accounting within the contract, potentially causing issues with calculations with user balances.

Proof of Concept:

  1. Alice has a scaled balance of 110 RTokens (representing 100 underlying units with a normalized income of 1.1).

  2. Alice wants to transfer 100 underlying units to Bob.

  3. The transfer function calculates scaledAmount = 100 / 1.1 = 90.91 (approximately). This is incorrect. It should be 100 * 1.1 = 110.

  4. The _update function then calculates scaledAmount = 90.91 / 1.1 = 82.65 (approximately). This is the double-scaled amount.

  5. Alice only transfers 82.65 RTokens to Bob, even though she intended to transfer 100.

Proof of Code:

  1. Use this guide to intergrate foundry into your project: foundry

  2. Create a new file FortisAudits.t.sol in the test directory.

  3. Add the following gist code to the file: Gist Code

  4. Run the test using forge test --mt test_FortisAudits_IncorrectTransferScalingInRToken -vvvv.

function test_FortisAudits_IncorrectTransferScalingInRToken() public {
address lp = makeAddr("lp");
address lp2 = makeAddr("lp2");
uint256 price = 50_000e18;
uint256 tokenId = 1;
_reserveAsset.mint(lp, price * 2);
_reserveAsset.mint(address(rToken), price * 2);
_reserveAsset.mint(lp2, price * 2);
rwaToken.mint(anon, price * 2);
// owner setting up the lending pool and minting the tokens
vm.startPrank(initialOwner);
raacHouse.setOracle(initialOwner);
raacHouse.setHousePrice(tokenId, price);
raacHouse.setHousePrice(tokenId+1, price);
debtToken.setReservePool(address(lendingPool));
vm.stopPrank();
// Lp deposits the reserve asset
vm.startPrank(lp);
_reserveAsset.approve(address(lendingPool), price * 2);
lendingPool.deposit(price);
vm.stopPrank();
vm.startPrank(lp2);
_reserveAsset.approve(address(lendingPool), price * 2);
lendingPool.deposit(price);
vm.stopPrank();
// Anon deposits the RWA token and mints the NFTs
vm.startPrank(anon);
rwaToken.approve(address(raacNFT), price * 2);
raacNFT.mint(tokenId, price);
raacNFT.mint(tokenId+1, price);
raacNFT.setApprovalForAll(address(lendingPool), true);
// Deposit
lendingPool.depositNFT(1);
lendingPool.depositNFT(2);
// Borrow
lendingPool.borrow(price);
uint256 repay = debtToken.balanceOf(anon);
vm.stopPrank();
skip(14 days);
vm.startPrank(lp);
lendingPool.updateState();
lendingPool.deposit(price);
uint256 underLyingAmount = rToken.scaledBalanceOf(lp);
console.log("LP's total balance: %d RTokens", underLyingAmount);
address lp_new = makeAddr("lp_new");
console.log("Lp_New balance before transfer: %d RTokens", rToken.scaledBalanceOf(lp_new));
rToken.transfer(lp_new, underLyingAmount);
console.log("Lp_New balance after transfer: %d RTokens", rToken.scaledBalanceOf(lp_new));
vm.stopPrank();
}

Logs before the fix:

[PASS] test_FortisAudits_IncorrectTransferScalingInRToken() (gas: 1372990)
Logs:
LP's total balance: 100000000000000000000000 RTokens
Lp_New balance before transfer: 0 RTokens
Lp_New balance after transfer: 99724884039023413274522 RTokens

Logs after the fix:

[PASS] test_FortisAudits_IncorrectTransferScalingInRToken() (gas: 1353068)
Logs:
LP's total balance: 100000000000000000000000 RTokens
Lp_New balance before transfer: 0 RTokens
Lp_New balance after transfer: 100000000000000000000000 RToken

Recommended Mitigation:

The transfer function should scale the amount up by the normalized income using rayMul before calling the super.transfer function. This will ensure that the correct scaled amount is transferred.

function transfer(address recipient, uint256 amount) public override(ERC20, IERC20) returns (bool) {
// amount is scaled up by normalized income, to reflect interest accrued
- uint256 scaledAmount = amount.rayDiv(ILendingPool(_reservePool).getNormalizedIncome()); // Incorrect: Should be rayMul
// FIX ⬇️
+ uint256 scaledAmount = amount.rayMul(ILendingPool(_reservePool).getNormalizedIncome()); // Correct: Scale up
return super.transfer(recipient, scaledAmount);
}
Updates

Lead Judging Commences

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

RToken::transfer and transferFrom double-scale amounts by dividing in both external functions and _update, causing users to transfer significantly less than intended

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

RToken::transfer and transferFrom double-scale amounts by dividing in both external functions and _update, causing users to transfer significantly less than intended

Support

FAQs

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