Summary
The LendingPool
contract attempts to integrate with Curve's vault by depositing reserves assets through the _depositIntoVault
function, but the direct deposit of crvUSD will revert since the asset is being stored in the RToken
contract 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.