Core Contracts

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

Incorrect Token Location Causes Vault Integration Failure in LendingPool

Summary

The LendingPool contract attempts to approve and deposit reserve tokens into the Curve vault from its own address, but the tokens are actually held at the RToken contract address. This architectural mismatch causes all vault deposit operations to fail, effectively breaking the vault integration functionality and potentially disrupting core protocol operations like deposits and withdrawals when a curve vault is configured.

Vulnerability Details

  • at the end of the functions deposit and withdraw and borrow in the lendingPool contract , the _rebalanceLiquidity function is called to rebalance the liquidity.

  • the _rebalanceLiquidity function will check if there is an excess liquidity in the lendingPool ,it will deposit this excess liquidity into the Curve vault , by calling the _depositIntoVault function :

function _rebalanceLiquidity() internal {
// ....prev code
if (currentBuffer > desiredBuffer) {
uint256 excess = currentBuffer - desiredBuffer;
// Deposit excess into the Curve vault
>> _depositIntoVault(excess);
}
......ect
}

The issue lies in the _depositIntoVault function where the LendingPool attempts to approve and deposit tokens to the curveVault from its own address:

// LendingPool.sol#L842-846
function _depositIntoVault(uint256 amount) internal {
IERC20(reserve.reserveAssetAddress).approve(address(curveVault), amount);
curveVault.deposit(amount, address(this));
totalVaultDeposits += amount;
}

However the lendingPool address holds no tokens as all the liquidity lives in the Rtoken contract address (reserve.reserveRTokenAddress).

  • this will lead to a revert when ever this function is called .

PoC

Foundry Envirement Setup
  • i'm using foundry for test , to integrate foundry :
    run :

    npm install --save-dev @nomicfoundation/hardhat-foundry

    add this to hardhat.config.cjs :

    require("@nomicfoundation/hardhat-foundry");

    run :

    npx hardhat init-foundry
  • comment the test/unit/libraries/ReserveLibraryMock.sol as it's causing compiling errors

  • inside test folder , create new dir foundry and inside it , create new file baseTest.sol , and copy/paste this there :

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "forge-std/console2.sol";
import "../../contracts/core/tokens/DebtToken.sol";
import "../../contracts/core/tokens/RAACToken.sol";
import "../../contracts/core/tokens/veRAACToken.sol";
import "../../contracts/core/tokens/RToken.sol";
import "../../contracts/core/tokens/DEToken.sol";
import "../../contracts/core/pools/LendingPool/LendingPool.sol";
import "../../contracts/core/pools/StabilityPool/StabilityPool.sol";
import "../../contracts/core/tokens/RAACNFT.sol";
import "../../contracts/core/primitives/RAACHousePrices.sol";
import "../../contracts/mocks/core/tokens/crvUSDToken.sol";
import "../../contracts/core/collectors/FeeCollector.sol";
import "../../contracts/core/collectors/Treasury.sol";
import "../../contracts/core/minters/RAACMinter/RAACMinter.sol";
import "../../contracts/core/minters/RAACReleaseOrchestrator/RAACReleaseOrchestrator.sol";
contract baseTest is Test {
// Protocol contracts
crvUSDToken public crvUSD;
RAACToken public raacToken;
veRAACToken public veToken;
RAACHousePrices public housePrices;
RAACNFT public raacNFT;
RToken public rToken;
DebtToken public debtToken;
DEToken public deToken;
LendingPool public lendingPool;
StabilityPool public stabilityPool;
Treasury public treasury;
Treasury public repairFund;
FeeCollector public feeCollector;
RAACMinter public minter;
RAACReleaseOrchestrator public releaseOrchestrator;
// Test accounts
address public admin = makeAddr("admin");
address public user1 = makeAddr("user1");
address public user2 = makeAddr("user2");
address public user3 = makeAddr("user3");
// Constants
uint256 public constant INITIAL_MINT = 1000 ether;
uint256 public constant HOUSE_PRICE = 100 ether;
uint256 public constant INITIAL_PRIME_RATE = 0.1e27; // 10% in RAY
uint256 public constant TAX_RATE = 200; // 2% in basis points
uint256 public constant BURN_RATE = 50; // 0.5% in basis points
function setUp() public virtual {
vm.startPrank(admin);
// Deploy base tokens
crvUSD = new crvUSDToken(admin);
crvUSD.setMinter(admin);
raacToken = new RAACToken(admin, TAX_RATE, BURN_RATE);
veToken = new veRAACToken(address(raacToken));
releaseOrchestrator = new RAACReleaseOrchestrator(address(raacToken));
// Deploy mock oracle
housePrices = new RAACHousePrices(admin);
housePrices.setOracle(admin);
// Deploy NFT
raacNFT = new RAACNFT(address(crvUSD), address(housePrices), admin);
// Deploy pool tokens
rToken = new RToken("RToken", "RT", admin, address(crvUSD));
debtToken = new DebtToken("DebtToken", "DT", admin);
deToken = new DEToken("DEToken", "DEToken", admin, address(rToken));
// Deploy core components
treasury = new Treasury(admin);
repairFund = new Treasury(admin);
feeCollector =
new FeeCollector(address(raacToken), address(veToken), address(treasury), address(repairFund), admin);
lendingPool = new LendingPool(
address(crvUSD),
address(rToken),
address(debtToken),
address(raacNFT),
address(housePrices),
INITIAL_PRIME_RATE
);
stabilityPool = new StabilityPool(admin);
minter = new RAACMinter(address(raacToken), address(stabilityPool), address(lendingPool), address(treasury));
// Initialize contracts
raacToken.setFeeCollector(address(feeCollector));
raacToken.manageWhitelist(address(feeCollector), true);
raacToken.manageWhitelist(address(veToken), true);
raacToken.manageWhitelist(admin, true);
raacToken.setMinter(admin);
raacToken.mint(user2, INITIAL_MINT);
raacToken.mint(user3, INITIAL_MINT);
raacToken.setMinter(address(minter));
bytes32 FEE_MANAGER_ROLE = feeCollector.FEE_MANAGER_ROLE();
bytes32 EMERGENCY_ROLE = feeCollector.EMERGENCY_ROLE();
bytes32 DISTRIBUTOR_ROLE = feeCollector.DISTRIBUTOR_ROLE();
feeCollector.grantRole(FEE_MANAGER_ROLE, admin);
feeCollector.grantRole(EMERGENCY_ROLE, admin);
feeCollector.grantRole(DISTRIBUTOR_ROLE, admin);
rToken.setReservePool(address(lendingPool));
debtToken.setReservePool(address(lendingPool));
deToken.setStabilityPool(address(stabilityPool));
raacToken.transferOwnership(address(minter));
rToken.transferOwnership(address(lendingPool));
debtToken.transferOwnership(address(lendingPool));
stabilityPool.initialize(
address(rToken),
address(deToken),
address(raacToken),
address(minter),
address(crvUSD),
address(lendingPool)
);
lendingPool.setStabilityPool(address(stabilityPool));
// Setup test environment
crvUSD.mint(user1, INITIAL_MINT);
crvUSD.mint(user2, INITIAL_MINT);
crvUSD.mint(user3, INITIAL_MINT);
housePrices.setHousePrice(1, HOUSE_PRICE);
vm.stopPrank();
}
// Helper functions
function mintCrvUSD(address to, uint256 amount) public {
vm.prank(admin);
crvUSD.mint(to, amount);
}
function setHousePrice(uint256 tokenId, uint256 price) public {
vm.prank(admin);
housePrices.setHousePrice(tokenId, price);
}
function calculateInterest(uint256 principal, uint256 rate, uint256 time) public pure returns (uint256) {
return principal * rate * time / 365 days / 1e27;
}
function warpAndAccrue(uint256 time) public {
vm.warp(block.timestamp + time);
lendingPool.updateState();
}
}
  • now create a pocs.sol inside test/foundry , and copy/paste this there :

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "./baseTest.sol";
import {ERC4626} from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MockCurveVault is ERC4626 {
constructor(address _asset) ERC4626(ERC20(_asset)) ERC20("Mock Curve Vault", "mcrvUSD") {}
}
contract pocs is baseTest {
//poc here
}

The following test demonstrates how deposits fail when a curve vault is configured because the lendingPool address holds no tokens:

function test_poc01() public {
// 1. Deploy and setup mock vault
MockCurveVault vault = new MockCurveVault(address(crvUSD));
vm.prank(admin);
lendingPool.setCurveVault(address(vault));
vm.startPrank(user1);
uint256 amount = 1000 ether;
// 3. User approves LendingPool
crvUSD.approve(address(lendingPool), amount);
// 4. User deposits - This will fail because:
// a. LendingPool.deposit calls crvUSD.transferFrom(user1, rToken, amount)
// b. LendingPool approve vault to spend crvUSD
// c. Vault tries to transferFrom LendingPool but fails (tokens are in RToken)
vm.expectRevert();
lendingPool.deposit(amount);
}
  • traces :

Ran 1 test for test/foundry/pocs.sol:pocs
[PASS] test_poc01() (gas: 1321611)
├ .....
├ .....
├ ....
├─ [6972] MockCurveVault::deposit(800000000000000000000 [8e20], LendingPool: [0x13BD9422D1ae8356644c4b134F7d672B5DfF6C2d])
│ │ ├─ [560] crvUSDToken::balanceOf(MockCurveVault: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f]) [staticcall]
│ │ │ └─ ← [Return] 0
│ │ ├─ [1769] crvUSDToken::transferFrom(LendingPool: [0x13BD9422D1ae8356644c4b134F7d672B5DfF6C2d], MockCurveVault: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], 800000000000000000000 [8e20])
│ │ │ ├─ storage changes:
│ │ │ │ @ 0x904cf3c7f7d6ed43c61a559d48b7fd8114332e2ddd9aba037a4a25e87be9cc53: 0x00000000000000000000000000000000000000000000002b5e3af16b18800000 → 0
│ │ │ └─ ← [Revert] ERC20InsufficientBalance(0x13BD9422D1ae8356644c4b134F7d672B5DfF6C2d, 0, 800000000000000000000 [8e20])
│ │ └─ ← [Revert] ERC20InsufficientBalance(0x13BD9422D1ae8356644c4b134F7d672B5DfF6C2d, 0, 800000000000000000000 [8e20])
│ ├─ storage changes:
│ │ @ 0xd952c6a4a18c919de24d6a10afb7c1171dbf1f6b700e51a18472d2be14c07964: 0 → 0x00000000000000000000000000000000000000000000003635c9adc5dea00000
│ │ @ 6: 0 → 0x00000000000000000000000000000000000000000000003635c9adc5dea00000
│ │ @ 11: 0 → 0x00000000000000000000000000000000000000000014adf4b7320334b9000000
│ │ @ 1: 1 → 2
│ │ @ 0x904cf3c7f7d6ed43c61a559d48b7fd8114332e2ddd9aba037a4a25e87be9cc53: 0 → 0x00000000000000000000000000000000000000000000002b5e3af16b18800000
│ └─ ← [Revert] ERC20InsufficientBalance(0x13BD9422D1ae8356644c4b134F7d672B5DfF6C2d, 0, 800000000000000000000 [8e20])

To run the test:

forge test --mt test_poc01 -vvv

Impact

  • complete DOS on all deposit , withdraw and borrow functions will fail when a curve vault is configured which This breaks core protocol functionality

Tools Used

  • Foundry

  • Manual Review

Recommendations

  1. Transfer tokens from RToken to LendingPool before vault operations:

// LendingPool.sol#L842-846
function _depositIntoVault(uint256 amount) internal {
+ // Transfer tokens from RToken to LendingPool first
+ IERC20(reserve.reserveRTokenAddress).transferAsset(address(this), amount);
// Now LendingPool has the tokens and can deposit them
IERC20(reserve.reserveAssetAddress).approve(address(curveVault), amount);
curveVault.deposit(amount, address(this));
totalVaultDeposits += amount;
}
Updates

Lead Judging Commences

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