Core Contracts

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

Depositing into Curve Vault from the LendingPool fails due to Incorrect Token Ownership


Summary

The deposit of crvUSD into the Curve Vault has an issue related to token ownership and transfer.

Vulnerability Details

The LendingPool contract allows users to deposit reserve assets (crvUSD) by calling deposit()

/**
* @notice Allows a user to deposit reserve assets and receive RTokens
* @param amount The amount of reserve assets to deposit
*/
function deposit(uint256 amount) external nonReentrant whenNotPaused onlyValidAmount(amount) {
// Update the reserve state before the deposit
ReserveLibrary.updateReserveState(reserve, rateData);
// Perform the deposit through ReserveLibrary
uint256 mintedAmount = ReserveLibrary.deposit(reserve, rateData, amount, msg.sender);
// Rebalance liquidity after deposit
_rebalanceLiquidity(); // -> Calls this to rebalance liquidity
emit Deposit(msg.sender, amount, mintedAmount);
}

The user tokens are transferred to the RToken contract and RTokens are minted in return.

/**
* @notice Handles deposit operation into the reserve.
* @dev Transfers the underlying asset from the depositor to the reserve, and mints RTokens to the depositor.
* This function assumes interactions with ERC20 before updating the reserve state (you send before we update how much you sent).
* A untrusted ERC20's modified mint function calling back into this library will cause incorrect reserve state updates.
* Implementing contracts need to ensure reentrancy guards are in place when interacting with this library.
* @param reserve The reserve data.
* @param rateData The reserve rate parameters.
* @param amount The amount to deposit.
* @param depositor The address of the depositor.
* @return amountMinted The amount of RTokens minted.
*/
function deposit(ReserveData storage reserve,ReserveRateData storage rateData,uint256 amount,address depositor) internal returns (uint256 amountMinted) {
if (amount < 1) revert InvalidAmount();
// Update reserve interests
updateReserveInterests(reserve, rateData);
// Transfer asset from caller to the RToken contract
IERC20(reserve.reserveAssetAddress).safeTransferFrom(
msg.sender, // from
reserve.reserveRTokenAddress, // to -> Actual crvUSD transfer goes to the RToken contract.
amount // amount
);
// Mint RToken to the depositor (scaling handled inside RToken)
(bool isFirstMint, uint256 amountScaled, uint256 newTotalSupply, uint256 amountUnderlying) = IRToken(reserve.reserveRTokenAddress).mint(
address(this), // caller
depositor, // onBehalfOf
amount, // amount
reserve.liquidityIndex // index
);
amountMinted = amountScaled;
// Update the total liquidity and interest rates
updateInterestRatesAndLiquidity(reserve, rateData, amount, 0);
emit Deposit(depositor, amount, amountMinted);
return amountMinted;
}

If the Curve Vault is set (by calling setCurveVault()), the internal _rebalanceLiquidity() function will attempt to balance the liquidity between the buffer and the Curve Vault to maintain the desired buffer ratio (i.e. 20% of total deposits). If the current buffer (RToken's balance) exceeds the desired buffer (currentBuffer > desiredBuffer), the contract will deposit the excess into the Curve Vault.

/**
* @notice Rebalances liquidity between the buffer and the Curve vault to maintain the desired buffer ratio
*/
function _rebalanceLiquidity() internal {
// if curve vault is not set, do nothing
if (address(curveVault) == address(0)) { // -> Checks if Curve vault is set.
return;
}
uint256 totalDeposits = reserve.totalLiquidity; // Total liquidity in the system
uint256 desiredBuffer = totalDeposits.percentMul(liquidityBufferRatio);
uint256 currentBuffer = IERC20(reserve.reserveAssetAddress).balanceOf(reserve.reserveRTokenAddress);
if (currentBuffer > desiredBuffer) {
uint256 excess = currentBuffer - desiredBuffer;
// Deposit excess into the Curve vault
_depositIntoVault(excess); // -> Deposits into vault if there is excess
} else if (currentBuffer < desiredBuffer) {
uint256 shortage = desiredBuffer - currentBuffer;
// Withdraw shortage from the Curve vault
_withdrawFromVault(shortage);
}
emit LiquidityRebalanced(currentBuffer, totalVaultDeposits);
}

_depositIntoVault() approves the Curve Vault and the Vault attempts to pull the tokens form the calling contract (i.e. LendingPool).

The issue is that the crvUSD resides in the RToken contract, but the LendingPool holds no token. This leads to a failed transfer when depositing to the Curve Vault.

/**
* @notice Internal function to deposit liquidity into the Curve vault
* @param amount The amount to deposit
*/
function _depositIntoVault(uint256 amount) internal {
IERC20(reserve.reserveAssetAddress).approve(address(curveVault), amount);
curveVault.deposit(amount, address(this));
totalVaultDeposits += amount;
}

Impact

The issue is marked as Medium Severity because it disrupts a core functionality of the protocol (rebalancing liquidity) and negatively impacts user experience, but it does not directly result in fund loss or protocol insolvency. User's transaction reverts due to insufficient crvUSD balance in the LendingPool when trying to deposit into the vault if there is excess reserve assets.

Tools Used

Manual Review

POC

pragma solidity ^0.8.13;
import {Test, console} from "forge-std/Test.sol";
import {IERC20} from "lib/forge-std/src/interfaces/IERC20.sol";
import {IERC721} from "lib/forge-std/src/interfaces/IERC721.sol";
// Pools
import {LendingPool} from "contracts/core/pools/LendingPool/LendingPool.sol";
import {StabilityPool} from "contracts/core/pools/StabilityPool/StabilityPool.sol";
import {MarketCreator} from "contracts/core/pools/StabilityPool/MarketCreator.sol";
import {NFTLiquidator} from "contracts/core/pools/StabilityPool/NFTLiquidator.sol";
// Collectors
import {FeeCollector} from "contracts/core/collectors/FeeCollector.sol";
import {Treasury} from "contracts/core/collectors/Treasury.sol";
// Tokens
import {DebtToken} from "contracts/core/tokens/DebtToken.sol";
import {DEToken} from "contracts/core/tokens/DEToken.sol";
import {IndexToken} from "contracts/core/tokens/IndexToken.sol";
import {LPToken} from "contracts/core/tokens/LPToken.sol";
import {RAACNFT} from "contracts/core/tokens/RAACNFT.sol";
import {RAACToken} from "contracts/core/tokens/RAACToken.sol";
import {RToken} from "contracts/core/tokens/RToken.sol";
import {veRAACToken} from "contracts/core/tokens/veRAACToken.sol";
// Primitives
import {RAACHousePrices} from "contracts/core/primitives/RAACHousePrices.sol";
// Minters
import {RAACMinter} from "contracts/core/minters/RAACMinter/RAACMinter.sol";
import {RAACReleaseOrchestrator} from "contracts/core/minters/RAACReleaseOrchestrator/RAACReleaseOrchestrator.sol";
// Governance
import {BoostController} from "contracts/core/governance/boost/BoostController.sol";
import {BaseGauge} from "contracts/core/governance/gauges/BaseGauge.sol";
import {GaugeController, IGaugeController} from "contracts/core/governance/gauges/GaugeController.sol";
import {RAACGauge} from "contracts/core/governance/gauges/RAACGauge.sol";
import {RWAGauge} from "contracts/core/governance/gauges/RWAGauge.sol";
import {Governance} from "contracts/core/governance/proposals/Governance.sol";
import {TimelockController} from "contracts/core/governance/proposals/TimelockController.sol";
// Oracles
import {BaseChainlinkFunctionsOracle} from "contracts/core/oracles/BaseChainlinkFunctionsOracle.sol";
import {RAACHousePriceOracle} from "contracts/core/oracles/RAACHousePriceOracle.sol";
import {RAACPrimeRateOracle} from "contracts/core/oracles/RAACPrimeRateOracle.sol";
contract RAACTest is Test {
// Tokens
DebtToken debtToken; // represents the debt token for the RAAC lending protocol.
DEToken deToken; //
IndexToken indexToken; //
LPToken lpToken;
RAACNFT raacNFT;
RAACToken raacToken;
RToken rToken;
veRAACToken veRaacToken;
// Pools
LendingPool lendingPool; // manages interactions with RTokens, DebtTokens, and handles the main logic for asset lending.
StabilityPool stabilityPool; // manages interactions with RAACTokens, DETokens, and handles the main logic for asset stability.
// Oracles
RAACHousePrices raacHousePrices; // manages the house prices associated with RAAC tokens.
RAACHousePriceOracle housePriceOracle; // fetches house pricing data from off-chain api.
RAACPrimeRateOracle primeRateOracle; // fetches prime rate from off-chain api.
// Minters
RAACMinter raacMinter; // mints RAAC tokens.
// Fee Collector/Treasury
FeeCollector feeCollector; // collects fees from the RAAC protocol.
Treasury treasury; // manages the treasury funds.
//Gauges
RAACGauge raacGauge; // manages RAAC token emissions and staking.
RWAGauge rwaGauge; // manages RWA token emissions and staking.
GaugeController gaugeController; // manages the gauges.
BoostController boostController; // manages the boost.
// Governance
Governance governance; // manages the governance proposals.
TimelockController timelockController; // manages the timelock.
address owner = makeAddr("owner");
address admin = makeAddr("admin");
address repairFund = makeAddr("repairFund");
address manager = makeAddr("manager");
address liquidityPool = makeAddr("liquidityPool");
address user1 = makeAddr("user1");
address user2 = makeAddr("user2");
address user3 = makeAddr("user3");
//address user3 = makeAddr("user3");
address functionsRouter = 0x65Dcc24F8ff9e51F10DCc7Ed1e4e2A61e6E14bd6;
bytes32 DON_ID = 0x66756e2d657468657265756d2d6d61696e6e65742d3100000000000000000000;
address crvUSD = 0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E;
address crvUSDVault = 0x0655977FEb2f289A4aB78af67BAB0d17aAb84367; // scrvUSD
function setUp() public {
vm.createSelectFork("https://rpc.ankr.com/eth");
vm.startPrank(address(owner));
//Tokens
debtToken = new DebtToken("DebtToken", "DT", address(owner));
rToken = new RToken(
"RToken",
"RT",
address(owner),
address(crvUSD)
);
deToken = new DEToken(
"DEToken",
"DET",
address(owner),
address(rToken)
);
indexToken = new IndexToken("IndexToken", "IT");
lpToken = new LPToken("LPToken", "LPT", address(owner));
raacToken = new RAACToken(address(owner), 0, 0);
raacHousePrices = new RAACHousePrices(address(owner));
raacNFT = new RAACNFT(
address(crvUSD),
address(raacHousePrices),
address(owner)
);
veRaacToken = new veRAACToken(address(raacToken));
//Pools
lendingPool = new LendingPool(
address(crvUSD),
address(rToken),
address(debtToken),
address(raacNFT),
address(raacHousePrices),
5_00 //5%
);
stabilityPool = new StabilityPool(address(owner));
lendingPool.setCurveVault(address(crvUSDVault));
lendingPool.setStabilityPool(address(stabilityPool));
//Minters
raacMinter = new RAACMinter(
address(raacToken),
address(stabilityPool),
address(lendingPool),
address(owner)
);
// raacMinter.setStabilityPool(address(stabilityPool));
// raacMinter.setLendingPool(address(lendingPool));
stabilityPool.initialize(address(rToken), address(deToken), address(raacToken), address(raacMinter), address(crvUSD), address(lendingPool));
stabilityPool.setRAACMinter(address(raacMinter));
stabilityPool.setLiquidityPool(address(liquidityPool));
stabilityPool.addManager(address(manager), 50_000e18);
//Collectors
treasury = new Treasury(address(owner));
feeCollector = new FeeCollector(address(raacToken), address(veRaacToken), address(treasury), address(repairFund), address(owner));
//Oracles/Primitives
housePriceOracle = new RAACHousePriceOracle(
address(functionsRouter),
DON_ID,
address(raacHousePrices)
);
primeRateOracle = new RAACPrimeRateOracle(
address(functionsRouter),
DON_ID,
address(lendingPool)
);
// Gauges
gaugeController = new GaugeController(address(veRaacToken));
raacGauge = new RAACGauge(
address(raacToken),
address(raacToken),
address(gaugeController)
);
rwaGauge = new RWAGauge(
address(raacToken),
address(crvUSD),
address(gaugeController)
);
raacHousePrices.setOracle(address(housePriceOracle));
debtToken.setReservePool(address(lendingPool));
rToken.setReservePool(address(lendingPool));
deToken.setStabilityPool(address(stabilityPool));
raacToken.setMinter(address(raacMinter));
raacToken.setFeeCollector(address(feeCollector));
raacToken.manageWhitelist(address(liquidityPool), true);
raacToken.manageWhitelist(address(stabilityPool), true);
veRaacToken.setMinter(address(raacMinter));
vm.stopPrank();
vm.startPrank(address(housePriceOracle));
raacHousePrices.setHousePrice(0, 10_000e18);
vm.stopPrank();
vm.startPrank(address(owner));
gaugeController.addGauge(address(raacGauge), IGaugeController.GaugeType.RAAC, 5000);
gaugeController.addGauge(address(rwaGauge), IGaugeController.GaugeType.RWA, 5000);
vm.stopPrank();
vm.startPrank(address(gaugeController));
raacGauge.setWeeklyEmission(200_000e18);
rwaGauge.setMonthlyEmission(1_500_000e18);
// raacGauge.setEmission(200_000e18);
// rwaGauge.setEmission(1_500_000e18);
// raacGauge.setInitialWeight(5000); // 40% of the total weight
// rwaGauge.setInitialWeight(5000); // 20% of the total weight
// raacGauge.setDistributionCap(400_000e18);
// rwaGauge.setDistributionCap(2_000_000e18);
// raacGauge.setBoostParameters(25000, 1e18, 7 days); // 2.5x boost
// rwaGauge.setBoostParameters(20000, 1e18, 14 days); // 2x boost
vm.stopPrank();
deal(address(crvUSD), address(user1), 50_000e18);
deal(address(crvUSD), address(user2), 50_000e18);
deal(address(crvUSD), address(stabilityPool), 20_000e18); //@todo - Check for how the stability pool is funded.
vm.label(address(crvUSD), "crvUSD");
vm.label(address(crvUSDVault), "CurveVault");
vm.label(address(deToken), "deToken");
vm.label(address(debtToken), "debtToken");
vm.label(address(indexToken), "indexToken");
vm.label(address(lpToken), "lpToken");
vm.label(address(raacNFT), "raacNFT");
vm.label(address(raacToken), "raacToken");
vm.label(address(rToken), "rToken");
vm.label(address(veRaacToken), "veRaacToken");
vm.label(address(lendingPool), "LendingPool");
vm.label(address(stabilityPool), "StabilityPool");
vm.label(address(raacHousePrices), "RAACHousePrices");
vm.label(address(housePriceOracle), "HousePriceOracle");
vm.label(address(primeRateOracle), "PrimeRateOracle");
vm.label(address(raacMinter), "RAACMinter");
vm.label(address(gaugeController), "GaugeController");
vm.label(address(raacGauge), "RAACGauge");
vm.label(address(rwaGauge), "RWAGauge");
vm.label(address(governance), "Governance");
vm.label(address(timelockController), "TimelockController");
}
function testDepositAndWithdraw() public {
vm.startPrank(address(user1));
IERC20(crvUSD).approve(address(lendingPool), type(uint256).max);
lendingPool.deposit(10_000e18);
vm.stopPrank();
}
}

Recommendations

Before depositing into the vault, transfer crvUSD from the RToken contract to the LendingPool, ensuring the LendingPool has the necessary tokens to proceed.

/**
* @notice Internal function to deposit liquidity into the Curve vault
* @param amount The amount to deposit
*/
function _depositIntoVault(uint256 amount) internal {
+ IRToken(reserve.reserveRTokenAddress).transferAsset(address(this), amount);
IERC20(reserve.reserveAssetAddress).approve(address(curveVault), amount);
curveVault.deposit(amount, address(this)); //@audit-issue #1 - crvUSD balance in this contract is empty. Tokens are actually "deposited" in the RToken contract.
totalVaultDeposits += amount;
}
Updates

Lead Judging Commences

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

LendingPool::_depositIntoVault and _withdrawFromVault don't transfer tokens between RToken and LendingPool, breaking Curve vault interactions

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

LendingPool::_depositIntoVault and _withdrawFromVault don't transfer tokens between RToken and LendingPool, breaking Curve vault interactions

Support

FAQs

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