Core Contracts

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

Incorrect Scaling in `RToken.mint` Function Leads to Loss of Funds for Users

Summary

The RToken contract's mint function incorrectly handles the scaling of the minting amount, leading to a loss of funds for users who deposit assets. The amount provided to the mint function is not scaled up by the liquidity index before being used in the internal _mint function. While the _update function does scale down the amount to represent the balance in scaled down amount, the initial mint amount is already incorrect, resulting in fewer RTokens minted than expected.

Vulnerability Details

The RToken contract is designed to be a rebasing token, where balances are scaled based on a liquidity index. The mint function is responsible for minting new RTokens when a user deposits the underlying asset. However, the current implementation contains a flaw in how it handles scaling:

function mint(
address caller, // lendingPool
address onBehalfOf, // user
uint256 amountToMint, // unscaled amount
uint256 index
) external override onlyReservePool returns (bool, uint256, uint256, uint256) {
// ... other code ...
uint256 amountScaled = amountToMint.rayDiv(index); // Incorrect: Should be rayMul
// ... other code ...
_mint(onBehalfOf, amountToMint.toUint128()); // Uses the *unscaled* amount!
// ... other code ...
return (isFirstMint, amountToMint, totalSupply(), amountScaled); // Returns the incorrectly scaled amount
}
function _update(address from, address to, uint256 amount) internal override {
uint256 scaledAmount = amount.rayDiv(ILendingPool(_reservePool).getNormalizedIncome()); // Correct scaling happens here, but too late
super._update(from, to, scaledAmount);
}
  1. Incorrect Scaling: The amountToMint (the user's deposit amount) is divided by the index (liquidity index) using rayDiv. This is incorrect. It should be multiplied by the index using rayMul to account for the accruing interest.

  2. Unscaled Amount Used for Minting: The _mint function is called with the original, unscaled amountToMint instead of correct scaled up balance. This means the user receives fewer RTokens than they should, based on the current liquidity index.

  3. Late Scaling in _update: The _update function does correctly scale the amount down by the normalized income. However, this happens after the incorrect _mint call, so the user has already lost value. The damage is done when the tokens are minted with the unscaled amount.

Impact

  • Loss of Funds for Users: Users receive fewer RTokens than they are entitled to, resulting in a direct financial loss. The magnitude of the loss is proportional to the difference between the liquidity index at the time of deposit and the initial liquidity index.

Proof of Concept

  1. Assume the liquidity index (index) is 1.1 * 10**27 (representing a 10% increase since the initial value).

  2. Alice deposits 100 units of the underlying asset.

  3. The mint function calculates amountScaled as 100 / 1.1 = 90.91 (approximately). This is incorrect; it should be 100 * 1.1 = 110.

  4. The _mint function is called with 100 instead of correct amountScaled.

  5. Alice receives 100 / 1.1 = 90.91 RTokens, but she should have received 100 RTokens.

  6. Alice has lost approximately 9.09 RTokens due to the incorrect scaling.

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_IncorrectMintScalingInRToken -vvvv.

function test_FortisAudits_IncorrectMintScalingInRToken() public {
address lp = makeAddr("lp");
uint256 price = 50_000e18;
uint256 tokenId = 1;
_reserveAsset.mint(lp, 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();
// 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);
vm.stopPrank();
skip(14 days);
vm.startPrank(lp);
lendingPool.updateState();
lendingPool.deposit(price);
console.log("LP's total deposit: %d USD", price * 2);
console.log("LP's Actual RToken balance: %d RTokens", rToken.scaledBalanceOf(lp));
vm.stopPrank();
}

Logs before the fix:

[PASS] test_FortisAudits_IncorrectMintScalingInRToken() (gas: 1194072)
Logs:
LP's total deposit: 100000000000000000000000 USD
LP's Actual RToken balance: 99244468429573664328117 RTokens

Logs after the fix:

Logs:
LP's total deposit: 100000000000000000000000 USD
LP's Actual RToken balance: 100000000000000000000000 RTokens

Recommended Mitigation

To mitigate this vulnerability, the mint function should correctly scale the minting amount by multiplying it by the liquidity index before calling the _mint function. This will ensure that users receive the correct amount of RTokens based on the current liquidity index.

function mint(
address caller, // lendingPool
address onBehalfOf, // user
uint256 amountToMint, // unscaled amount
uint256 index
) external override onlyReservePool returns (bool, uint256, uint256, uint256) {
// ... other code ...
- uint256 amountScaled = amountToMint.rayDiv(index); // Incorrect: Should be rayMul
+ uint256 amountScaled = amountToMint.rayMul(index); // Correct: Scale up the amount
// ... other code ...
- _mint(onBehalfOf, amountToMint.toUint128()); // Uses the *unscaled* amount!
+ _mint(onBehalfOf, amountScaled.toUint128()); // Use the correctly scaled amount
// ... other code ...
return (isFirstMint, amountToMint, totalSupply(), amountScaled); // Returns the incorrectly scaled amount
}
Updates

Lead Judging Commences

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

DebtToken::mint miscalculates debt by applying interest twice, inflating borrow amounts and risking premature liquidations

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

DebtToken::mint miscalculates debt by applying interest twice, inflating borrow amounts and risking premature liquidations

Support

FAQs

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

Give us feedback!