Core Contracts

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

Permanent DoS when providing liquidity

Summary

When users call LendingPool::deposit() they deposit crvUSD tokens and receive RTokens in return. However upon invoking this function the call will revert everytime.

Vulnerability Details

When a user calls the deposit function the specified crvUSD tokens are transfered from the caller to RToken address and gets minted Rtoken, then the internal _rebalanceLiquidity() is invoked, which calculates the current buffer and the desired buffer and then transfers the difference in a crvUSD vault.

LendingPool:
function deposit(uint256 amount) external nonReentrant whenNotPaused onlyValidAmount(amount) {
...
@> uint256 mintedAmount = ReserveLibrary.deposit(reserve, rateData, amount, msg.sender);
// Rebalance liquidity after deposit
@> _rebalanceLiquidity();
emit Deposit(msg.sender, amount, mintedAmount);
}
ReserveLibrary:
function deposit(ReserveData storage reserve,ReserveRateData storage rateData,uint256 amount,address depositor) internal returns (uint256 amountMinted) {
...
// Transfer asset from caller to the RToken contract
@> IERC20(reserve.reserveAssetAddress).safeTransferFrom(
msg.sender, // from
reserve.reserveRTokenAddress, // to
amount // amount
);
LendingPool:
function _rebalanceLiquidity() internal {
// if curve vault is not set, do nothing
if (address(curveVault) == address(0)) {
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);
}
function _depositIntoVault(uint256 amount) internal {
IERC20(reserve.reserveAssetAddress).approve(address(curveVault), amount);
curveVault.deposit(amount, address(this));
totalVaultDeposits += amount;
}

The problem is that the curve vault will try to transfer from the Lending pool, but this contract doesn't hold any tokens, as they are directly deposited from the depositor to the RToken contract. Hence this will cause the function to revert everytime.

Here is a coded PoC demonstrating the issue, i'm using Foundry for tests:

  1. Install Foundry

  2. Run forge init --force in the terminal

  3. Paste the following file in the test folder and run forge test --mt testX in the terminal

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Test, console2} from "../lib/forge-std/src/Test.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {LendingPool} from "../contracts/core/pools/LendingPool/LendingPool.sol";
import {ILendingPool} from "../contracts/interfaces/core/pools/LendingPool/ILendingPool.sol";
import {RToken} from "../contracts/core/tokens/RToken.sol";
import {DebtToken} from "../contracts/core/tokens/DebtToken.sol";
import {RAACHousePrices} from "../contracts/core/primitives/RAACHousePrices.sol";
import {RAACNFT} from "../contracts/core/tokens/RAACNFT.sol";
import {IRAACNFT} from "../contracts/interfaces/core/tokens/IRAACNFT.sol";
import {WadRayMath} from "../contracts/libraries/math/WadRayMath.sol";
import {ICurveCrvUSDVault} from "../contracts/interfaces/curve/ICurveCrvUSDVault.sol";
contract CrvUSD is IERC20, ERC20 {
constructor() ERC20("crvUSD", "CRVUSD") {}
function mint(address to, uint256 amount) public {
_mint(to, amount);
}
}
contract MockCurveVault is ICurveCrvUSDVault {
CrvUSD crvUSD;
constructor(CrvUSD _crvUSD) {
crvUSD = _crvUSD;
}
function deposit(uint256 assets, address receiver) external returns (uint256 shares) {
crvUSD.transferFrom(msg.sender, address(this), assets);
}
function withdraw(uint256 assets, address receiver, address owner, uint256 maxLoss, address[] calldata strategies) external returns (uint256 shares) {}
function asset() external view returns (address) {}
function totalAssets() external view returns (uint256) {}
function pricePerShare() external view returns (uint256) {}
function totalIdle() external view returns (uint256) {}
function totalDebt() external view returns (uint256) {}
function isShutdown() external view returns (bool) {}
}
contract Tester is Test {
LendingPool lendingPool;
CrvUSD crvUSD;
RToken rToken;
DebtToken debtToken;
RAACHousePrices housePrices;
RAACNFT nft;
MockCurveVault vault;
uint256 initialPrimeRate = 1e26;
address owner = makeAddr("owner");
address bob = makeAddr("bob");
address lender = makeAddr("lender");
function setUp() external {
vm.startPrank(owner);
crvUSD = new CrvUSD();
vault = new MockCurveVault(crvUSD);
rToken = new RToken("RToken", "RT", owner, address(crvUSD));
debtToken = new DebtToken("DebtToken", "DT", owner);
housePrices = new RAACHousePrices(owner);
nft = new RAACNFT(address(crvUSD), address(housePrices), owner);
lendingPool = new LendingPool(
address(crvUSD),
address(rToken),
address(debtToken),
address(nft),
address(housePrices),
initialPrimeRate
);
housePrices.setOracle(owner); // owner is set as oracle for simplicity
housePrices.setHousePrice(1, 1000e18); // tokenID = 1 will cost 1000e18 crvUSD
debtToken.setReservePool(address(lendingPool));
rToken.setReservePool(address(lendingPool));
lendingPool.setCurveVault(address(vault));
vm.stopPrank();
}
function testX() public {
// Lender wants to provide liquidity
vm.startPrank(lender);
crvUSD.mint(lender, 8000e18);
crvUSD.approve(address(lendingPool), type(uint256).max);
vm.expectRevert(); // reverts with insufficient balance error, remove this line to confirm
lendingPool.deposit(1000e18);
vm.stopPrank();
}
}

Impact

  • Impact: Medium, permanent DoS on deposits, which is the entry-point of the protocol

  • Likelihood: High, will happen everytime

  • Overall: High

Tools Used

Manual Review

Recommendations

The excess amount should be transfered from the RToken contract.

Updates

Lead Judging Commences

inallhonesty Lead Judge 4 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 4 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.