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
User deposits 100 crvUSD into the lending pool
The _rebalanceLiquidity function is called to maintain the buffer ratio
_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;
uint256 public constant MAX_BPS = 10000;
uint256 public constant DECIMALS = 18;
IERC20 public immutable asset;
bool public isShutdown;
uint256 public totalIdle;
uint256 public totalDebt;
struct StrategyParams {
uint256 currentDebt;
uint256 totalDebt;
uint256 lastReport;
bool activated;
}
mapping(address => StrategyParams) public strategies;
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);
}
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");
shares = _convertToShares(assets);
require(shares > 0, "Cannot mint 0 shares");
asset.safeTransferFrom(msg.sender, address(this), assets);
totalIdle += assets;
_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");
shares = _convertToShares(assets);
require(shares <= balanceOf(owner), "Insufficient balance");
if (msg.sender != owner) {
_spendAllowance(owner, msg.sender, shares);
}
if (assets > totalIdle) {
revert("Insufficient idle assets");
}
totalIdle -= assets;
_burn(owner, shares);
asset.safeTransfer(receiver, assets);
}
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;
}
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;
}
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:
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));
raacToken = new RAACToken(address(this), 0, 0);
deToken = new DEToken("deToken", "detk", address(this), address(rToken));
debtToken = new DebtToken("debtToken", "dtk", address(this));
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.