Core Contracts

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

Double Scaling Vulnerability in RToken Contract Leading to Loss of Funds During Withdrawals

Summary

When the reserve.liquidityIndex becomes larger than 1e27, which occurs from normal usage of the protocol over time, the withdraw function in the StabilityPool.sol contract causes users to lose more funds over time. This issue is caused by the transfer function in the RToken.sol contract, which scales the amount twice: once in the transfer function and once in the _update function. This double scaling results in users receiving fewer rTokens for their deTokens during withdrawals.

Vulnerability Details

The vulnerability arises from the way the RToken contract handles transfers and updates. Specifically, the transfer function scales the amount by the liquidityIndex, and the _update function also scales the amount by the liquidityIndex. This double scaling leads to an incorrect calculation of the token amounts during transfers and withdrawals.

Steps to Reproduce:
Initial Setup: Users deposit rTokens into the StabilityPool.
Increase Liquidity Index: Over time, the reserve.liquidityIndex increases due to normal protocol usage.
Partial Withdrawal: Users attempt to withdraw a portion of their rTokens from the StabilityPool.
Double Scaling: The transfer function in RToken scales the amount by the liquidityIndex, and the _update function scales it again, resulting in users receiving fewer rTokens for their deTokens.

Impact

The impact of this vulnerability is significant as it causes users to lose more funds over time. Specifically, users receive fewer rTokens for their deTokens during withdrawals, leading to a loss of value. This issue can erode user trust and result in financial losses for users of the protocol.

Tools Used

Foundry
Solidity
Forge-std

Recommendations

To fix this vulnerability, the double scaling issue in the RToken contract needs to be addressed. Specifically, the transfer and _update functions should be reviewed and modified to ensure that the amount is scaled correctly only once. Here are some recommendations:

Review and Modify transfer Function: Ensure that the transfer function scales the amount correctly and does not apply double scaling.
Review and Modify _update Function: Ensure that the _update function scales the amount correctly and does not apply double scaling.

POC

Commands to run in the terminal

mkdir ../raacFoundry
cd ../raacFoundry
forge init
rm src/Counter.sol test/Counter.t.sol script/Counter.s.sol
cp -r ../2025-02-raac/contracts src
touch test/Test.sol
npm install @openzeppelin/contracts
npm install @openzeppelin/contracts
npm install @openzeppelin/contracts-upgradeable
npm install @chainlink/contracts
forge test

Add this to the forge.toml file

[profile.default]
src = "src"
out = "out"
libs = ['lib', 'node_modules']
remappings = [
"@openzeppelin/=node_modules/@openzeppelin/",
"@chainlink/=node_modules/@chainlink/"
]

Add this to the test/Test.sol file

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../src/contracts/core/pools/LendingPool/LendingPool.sol";
import "../src/contracts/mocks/core/tokens/crvUSDToken.sol";
import "../src/contracts/core/primitives/RAACHousePrices.sol";
import "../src/contracts/core/tokens/RAACNFT.sol";
import "../src/contracts/core/tokens/RToken.sol";
import "../src/contracts/core/tokens/DebtToken.sol";
import "../src/contracts/core/pools/StabilityPool/StabilityPool.sol";
import "../src/contracts/core/tokens/RAACToken.sol";
import "../src/contracts/core/minters/RAACMinter/RAACMinter.sol";
import "../src/contracts/core/tokens/DeToken.sol";
contract StabilityPoolTest is Test {
address owner;
address user1;
address user2;
address user3;
address treasury;
StabilityPool stabilityPool;
LendingPool lendingPool;
RAACMinter raacMinter;
RToken rToken;
DebtToken debtToken;
DEToken deToken;
RAACToken raacToken;
crvUSDToken crvusd;
RAACNFT raacNFT;
RAACHousePrices raacHousePrices;
uint256 currentTime = 1672531200; // Example timestamp (January 1, 2023)
uint256 constant WAD = 1e18;
uint256 constant RAY = 1e27;
function setUp() public {
owner = address(this);
user1 = address(0x1);
user2 = address(0x2);
user3 = address(0x3);
treasury = address(0x4);
crvusd = new crvUSDToken(owner);
crvusd.setMinter(owner);
raacToken = new RAACToken(owner, 100, 50);
raacHousePrices = new RAACHousePrices(owner);
raacHousePrices.setOracle(owner);
raacNFT = new RAACNFT(address(crvusd), address(raacHousePrices), owner);
rToken = new RToken("RToken", "RToken", owner, address(crvusd));
deToken = new DEToken("DEToken", "DEToken", owner, address(rToken));
debtToken = new DebtToken("DebtToken", "DT", owner);
uint256 initialPrimeRate = 5e27;
lendingPool = new LendingPool(
address(crvusd),
address(rToken),
address(debtToken),
address(raacNFT),
address(raacHousePrices),
initialPrimeRate
);
raacHousePrices.setOracle(owner);
stabilityPool = new StabilityPool(owner);
vm.warp(currentTime);
raacMinter = new RAACMinter(
address(raacToken),
address(stabilityPool),
address(lendingPool),
owner
);
lendingPool.setStabilityPool(address(stabilityPool));
deToken.setStabilityPool(address(stabilityPool));
deToken.transferOwnership(address(stabilityPool));
stabilityPool.initialize(
address(rToken),
address(deToken),
address(raacToken),
address(raacMinter),
address(crvusd),
address(lendingPool)
);
// svm.warp(currentTime);
debtToken.setReservePool(address(lendingPool));
rToken.setReservePool(address(lendingPool));
rToken.setMinter(address(lendingPool));
rToken.setBurner(address(lendingPool));
mintRToken(address(lendingPool), user2, 100, 1);
// Normal example of users using the system which increases the reserve.liquidityIndex as time goes on
mintAndDeposit(user1, 100);
forwardTime(365 days);
mintNFTAndBorrow(user1, 1, 50, 25);
forwardTime(365 days);
mintAndDeposit(user3, 1);
}
// Helper fuctions to forward time and mint/deposit tokens
function forwardTime(uint256 addTime) internal {
currentTime += addTime;
vm.warp(currentTime);
}
function mintAndDeposit(address userF, uint256 amount) internal {
console.log(("Minting and depositing "));
crvusd.mint(userF, amount);
vm.startPrank(userF);
crvusd.approve(address(lendingPool), amount);
lendingPool.deposit(amount);
vm.stopPrank();
}
function mintRToken(address from, address to, uint256 amount, uint256 liquidityIndex) internal {
vm.startPrank(from);
rToken.mint(from, to, amount , liquidityIndex);
vm.stopPrank();
}
function mintNFTAndBorrow(address user, uint256 nftId, uint256 nftValue, uint256 borrowAmount) internal {
console.log(string(abi.encodePacked("Minting NFT and borrowing ", Strings.toString(nftId), " ", Strings.toString(nftValue), " ", Strings.toString(borrowAmount))));
vm.startPrank(owner);
raacHousePrices.setHousePrice(nftId, nftValue);
crvusd.mint(user, nftValue);
vm.stopPrank();
vm.startPrank(user);
// Approve and mint the crvUSD tokens for the NFT
crvusd.approve(address(raacNFT), nftValue);
raacNFT.mint(nftId, nftValue);
// Set the house price in the oracle
// Approve and deposit the NFT into the lending pool
raacNFT.approve(address(lendingPool), nftId);
lendingPool.depositNFT(nftId);
// Borrow against the NFT
lendingPool.borrow(nftValue);
vm.stopPrank();
}
// POC FUNCTION
function testPartialWithdrawal() public {
// Start impersonating user2
vm.startPrank(user2);
// Initial deposit and withdrawal amounts
uint256 initialAmount = 100;
uint256 withdrawAmount = 50;
// Approve the StabilityPool to spend user2's rTokens and crvUSD tokens
rToken.approve(address(stabilityPool), initialAmount);
crvusd.approve(address(lendingPool), initialAmount);
// Deposit rTokens into the StabilityPool
stabilityPool.deposit(initialAmount);
// Log balances of rToken and deToken for user2 after deposit
uint256 rTokenBalanceBeforeWithdraw = rToken.balanceOf(user2);
uint256 deTokenBalanceBeforeWithdraw = deToken.balanceOf(user2);
console.log(string(abi.encodePacked("User2 rToken balance after deposit: ", Strings.toString(rTokenBalanceBeforeWithdraw))));
console.log(string(abi.encodePacked("User2 deToken balance after deposit: ", Strings.toString(deTokenBalanceBeforeWithdraw))));
// Withdraw rTokens from the StabilityPool
stabilityPool.withdraw(withdrawAmount);
// Log final balances of rToken and deToken for user2 after withdrawal
uint256 rTokenBalanceAfterWithdraw = rToken.balanceOf(user2);
uint256 deTokenBalanceAfterWithdraw = deToken.balanceOf(user2);
console.log(string(abi.encodePacked("User2 rToken balance after withdrawal: ", Strings.toString(rTokenBalanceAfterWithdraw))));
console.log(string(abi.encodePacked("User2 deToken balance after withdrawal: ", Strings.toString(deTokenBalanceAfterWithdraw))));
// Calculate the amount of deToken lost during the withdrawal
uint256 deTokenLost = (rTokenBalanceBeforeWithdraw + deTokenBalanceBeforeWithdraw) - (rTokenBalanceAfterWithdraw + deTokenBalanceAfterWithdraw);
console.log(string(abi.encodePacked("User2 deToken lost during withdrawal: ", Strings.toString(deTokenLost))));
// Assertions to verify the expected behavior
// assertEq(rToken.balanceOf(user2), initialAmount - withdrawAmount, "Incorrect rToken balance after withdrawal");
// assertEq(deToken.balanceOf(user2), initialAmount - deTokenLost, "Incorrect deToken balance after withdrawal");
// Stop impersonating user2
vm.stopPrank();
}
}
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.