Core Contracts

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

Incorrect curve vault implementation leads to DOS

Summary

The LendingPoolcontract attempts to integrate with Curve's vault by depositing reserves assets through the _depositIntoVaultfunction, but the direct deposit of crvUSD will revert since the asset is being stored in the RTokencontract and not in the LendingPool.

POC

  1. User deposits 100 crvUSD into the lending pool

  2. The _rebalanceLiquidity function is called to maintain the buffer ratio

  3. _depositIntoVault attempts to deposit excess funds but fails because:

To test how the protocol will act when a curve vault is added, here is a MockVault:

/ SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
contract MockVaultV3 is ERC20 {
using SafeERC20 for IERC20;
// Constants
uint256 public constant MAX_BPS = 10000;
uint256 public constant DECIMALS = 18;
// State variables
IERC20 public immutable asset;
bool public isShutdown;
uint256 public totalIdle;
uint256 public totalDebt;
// Strategy tracking
struct StrategyParams {
uint256 currentDebt;
uint256 totalDebt;
uint256 lastReport;
bool activated;
}
mapping(address => StrategyParams) public strategies;
// Events
event StrategyAdded(address indexed strategy);
event StrategyRevoked(address indexed strategy);
event DebtUpdated(address indexed strategy, uint256 currentDebt, uint256 newDebt);
constructor(address _asset, string memory _name, string memory _symbol) ERC20(_name, _symbol) {
require(_asset != address(0), "Invalid asset address");
asset = IERC20(_asset);
}
// Core functions
function deposit(uint256 assets, address receiver) external returns (uint256 shares) {
require(!isShutdown, "Vault is shutdown");
require(assets > 0, "Cannot deposit 0");
require(receiver != address(0), "Invalid receiver");
// Calculate shares
shares = _convertToShares(assets);
require(shares > 0, "Cannot mint 0 shares");
// Transfer assets from user
asset.safeTransferFrom(msg.sender, address(this), assets);
// Update accounting
totalIdle += assets;
// Mint shares
_mint(receiver, shares);
}
function withdraw(uint256 assets, address receiver, address owner, uint256 maxLoss, address[] calldata strategies)
external
returns (uint256 shares)
{
require(receiver != address(0), "Invalid receiver");
require(assets > 0, "Cannot withdraw 0");
require(maxLoss <= MAX_BPS, "Invalid maxLoss");
// Calculate shares to burn
shares = _convertToShares(assets);
require(shares <= balanceOf(owner), "Insufficient balance");
// Handle allowance if msg.sender != owner
if (msg.sender != owner) {
_spendAllowance(owner, msg.sender, shares);
}
// Check if we have enough idle assets
if (assets > totalIdle) {
// Would need to withdraw from strategies
// Mock implementation just reverts
revert("Insufficient idle assets");
}
// Update accounting
totalIdle -= assets;
// Burn shares and transfer assets
_burn(owner, shares);
asset.safeTransfer(receiver, assets);
}
// View functions
function totalAssets() public view returns (uint256) {
return totalIdle + totalDebt;
}
function pricePerShare() public view returns (uint256) {
uint256 supply = totalSupply();
if (supply == 0) return 10 ** DECIMALS;
return (totalAssets() * 10 ** DECIMALS) / supply;
}
// Internal functions
function _convertToShares(uint256 assets) internal view returns (uint256) {
uint256 supply = totalSupply();
if (supply == 0) return assets;
return (assets * supply) / totalAssets();
}
function _convertToAssets(uint256 shares) internal view returns (uint256) {
uint256 supply = totalSupply();
if (supply == 0) return shares;
return (shares * totalAssets()) / supply;
}
// Admin functions
function addStrategy(address strategy) external {
require(!strategies[strategy].activated, "Strategy already added");
strategies[strategy] =
StrategyParams({currentDebt: 0, totalDebt: 0, lastReport: block.timestamp, activated: true});
emit StrategyAdded(strategy);
}
function revokeStrategy(address strategy) external {
require(strategies[strategy].activated, "Strategy not active");
require(strategies[strategy].currentDebt == 0, "Strategy has debt");
delete strategies[strategy];
emit StrategyRevoked(strategy);
}
function shutdownVault() external {
require(!isShutdown, "Already shutdown");
isShutdown = true;
}
}

Create a foundry setup using the commands in this document:
https://book.getfoundry.sh/config/hardhat?highlight=hardhat#adding-foundry-to-a-hardhat-project

Create a raacFoundrySetup.t.sol file under the test directory and add this code:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;
import {Test, console} from "forge-std/Test.sol";
import {LendingPool} from "contracts/core/pools/LendingPool/LendingPool.sol";
import {StabilityPool} from "contracts/core/pools/StabilityPool/StabilityPool.sol";
import {crvUSDToken} from "contracts/mocks/core/tokens/crvUSDToken.sol";
import {RToken} from "contracts/core/tokens/RToken.sol";
import {DebtToken} from "contracts/core/tokens/DebtToken.sol";
import {RAACNFT} from "contracts/core/tokens/RAACNFT.sol";
import {RAACHousePricesMock} from "contracts/mocks/core/primitives/RAACHousePricesMock.sol";
import {RAACHousePriceOracle} from "contracts/core/oracles/RAACHousePriceOracle.sol";
import {MockFunctionsRouter} from "contracts/mocks/core/oracles/MockFunctionsRouter.sol";
import {FeeCollector} from "contracts/core/collectors/FeeCollector.sol";
import {MockVeToken} from "contracts/mocks/core/tokens/MockVeToken.sol";
import {RAACMinter} from "contracts/core/minters/RAACMinter/RAACMinter.sol";
import {DEToken} from "contracts/core/tokens/DEToken.sol";
import {RAACToken} from "contracts/core/tokens/RAACToken.sol";
import {MockVaultV3} from "contracts/mocks/CurveVaultMock.sol";
contract SetupContract is Test {
address public user1;
address public user2;
address public user3;
uint256 public currentBlockTimestamp = 1000 days;
address public treasury;
address public repairFund;
LendingPool public lendingPool;
crvUSDToken public _crvUSDToken;
RToken public rToken;
MockVeToken public veRToken;
DebtToken public debtToken;
RAACHousePricesMock public raacHousePrices;
RAACNFT public raacNFT;
RAACHousePriceOracle public raacHousePriceOracle;
FeeCollector public feeCollector;
StabilityPool public stabilityPool;
RAACMinter public raacMinter;
DEToken public deToken;
RAACToken public raacToken;
MockVaultV3 public curveVault;
uint256 public constant INITIAL_PRIME_RATE = 1e26;
function setUp() external {
vm.warp(currentBlockTimestamp);
user1 = makeAddr("user1");
user2 = makeAddr("user2");
user3 = makeAddr("user3");
treasury = makeAddr("treasury");
repairFund = makeAddr("repairFund");
stabilityPool = new StabilityPool(address(this));
veRToken = new MockVeToken();
_crvUSDToken = new crvUSDToken(address(this));
rToken = new RToken("rtoken", "rtk", address(this), address(_crvUSDToken)); // reserve pool
raacToken = new RAACToken(address(this), 0, 0);
deToken = new DEToken("deToken", "detk", address(this), address(rToken)); // setStabilityPool;
debtToken = new DebtToken("debtToken", "dtk", address(this)); //reservePool
raacHousePrices = new RAACHousePricesMock();
raacNFT = new RAACNFT(address(_crvUSDToken), address(raacHousePrices), address(this));
raacHousePriceOracle = new RAACHousePriceOracle(
address(new MockFunctionsRouter()), bytes32(bytes("fun-ethereum-mainnet-1")), address(this)
);
curveVault = new MockVaultV3(address(_crvUSDToken), "vault", "vv");
feeCollector = new FeeCollector(address(rToken), address(veRToken), treasury, repairFund, address(this));
lendingPool = new LendingPool(
address(_crvUSDToken),
address(rToken),
address(debtToken),
address(raacNFT),
address(raacHousePrices),
INITIAL_PRIME_RATE
);
lendingPool.setCurveVault(address(curveVault));
raacMinter = new RAACMinter(address(raacToken), address(stabilityPool), address(lendingPool), address(this));
stabilityPool.initialize(
address(rToken),
address(deToken),
address(raacToken),
address(raacMinter),
address(_crvUSDToken),
address(lendingPool)
);
rToken.setReservePool(address(lendingPool));
debtToken.setReservePool(address(lendingPool));
deToken.setStabilityPool(address(stabilityPool));
_crvUSDToken.mint(user1, 10000e18);
_crvUSDToken.mint(user2, 100e18);
_crvUSDToken.mint(user3, 1000e18);
}
function testDeposit() public {
vm.startPrank(user1);
_crvUSDToken.approve(address(lendingPool), 100e18);
lendingPool.deposit(1e18);
vm.stopPrank();
vm.startPrank(user2);
_crvUSDToken.approve(address(lendingPool), 100e18);
lendingPool.deposit(1e18);
vm.stopPrank();
}
}

The deposit test that was working fine befor adding the curveVault is reverting now due to ERC20InsufficientBalance.

Impact

The LendingPool will be DOS-ed not allowing users to interact with the system, some of them may have their funds stuck in the protocol. The liquidity buffer mechanism becomes non-functional.

Recommendations

Fix the deposit flow adding the funds in the LendingPool first before deciding where to rellocate them.

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.