diff --git a/.gitignore b/.gitignore index 193f33236..e3520f322 100644 --- a/.gitignore +++ b/.gitignore @@ -64,3 +64,4 @@ packages/crab-netting/cache soljson* +lcov.info \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index 6a731a0ea..654de3fc6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -33,3 +33,9 @@ [submodule "packages/crab-netting/lib/openzeppelin-contracts"] path = packages/crab-netting/lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts +[submodule "packages/crab-netting/lib/v3-periphery"] + path = packages/crab-netting/lib/v3-periphery + url = https://github.com/uniswap/v3-periphery +[submodule "packages/crab-netting/lib/v3-core"] + path = packages/crab-netting/lib/v3-core + url = https://github.com/uniswap/v3-core diff --git a/packages/crab-netting/lib/v3-core b/packages/crab-netting/lib/v3-core new file mode 160000 index 000000000..412d9b236 --- /dev/null +++ b/packages/crab-netting/lib/v3-core @@ -0,0 +1 @@ +Subproject commit 412d9b236a1e75a98568d49b1aeb21e3a1430544 diff --git a/packages/crab-netting/lib/v3-periphery b/packages/crab-netting/lib/v3-periphery new file mode 160000 index 000000000..b06959dd0 --- /dev/null +++ b/packages/crab-netting/lib/v3-periphery @@ -0,0 +1 @@ +Subproject commit b06959dd01f5999aa93e1dc530fe573c7bb295f6 diff --git a/packages/crab-netting/remappings.txt b/packages/crab-netting/remappings.txt index 00862edca..23b1671cf 100644 --- a/packages/crab-netting/remappings.txt +++ b/packages/crab-netting/remappings.txt @@ -1,4 +1,6 @@ ds-test/=lib/forge-std/lib/ds-test/src/ forge-std/=lib/forge-std/src/ squeeth-monorepo/=lib/squeeth-monorepo/packages/hardhat/contracts/ -openzeppelin/=lib/openzeppelin-contracts/contracts/ \ No newline at end of file +openzeppelin/=lib/openzeppelin-contracts/contracts/ +@uniswap/v3-periphery/=lib/v3-periphery/ +@uniswap/v3-core/=lib/v3-core/ \ No newline at end of file diff --git a/packages/crab-netting/src/CrabNetting.sol b/packages/crab-netting/src/CrabNetting.sol index 0285a7978..73673725b 100644 --- a/packages/crab-netting/src/CrabNetting.sol +++ b/packages/crab-netting/src/CrabNetting.sol @@ -1,55 +1,805 @@ +// SPDX-License-Identifier: GPL-3.0-only pragma solidity ^0.8.13; +// interface import {IERC20} from "openzeppelin/token/ERC20/IERC20.sol"; -import "forge-std/console.sol"; +import {IWETH} from "../src/interfaces/IWETH.sol"; +import {IOracle} from "../src/interfaces/IOracle.sol"; +import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; +import {ICrabStrategyV2} from "../src/interfaces/ICrabStrategyV2.sol"; +import {IController} from "../src/interfaces/IController.sol"; -contract CrabNetting { - address usdc; - address crab; +// contract +import {Ownable} from "openzeppelin/access/Ownable.sol"; +import {EIP712} from "openzeppelin/utils/cryptography/draft-EIP712.sol"; +import {ECDSA} from "openzeppelin/utils/cryptography/ECDSA.sol"; - mapping(address => uint256) usd_balance; - mapping(address => uint256) crab_balance; - struct Receipt { - address depositor; - uint256 amount; - } - Receipt[] deposits; - Receipt[] crab_deposits; +/// @dev order struct for a signed order from market maker +struct Order { + uint256 bidId; + address trader; + uint256 quantity; + uint256 price; + bool isBuying; + uint256 expiry; + uint256 nonce; + uint8 v; + bytes32 r; + bytes32 s; +} + +/// @dev struct to store proportional amounts of erc20s (received or to send) +struct Portion { + uint256 crab; + uint256 eth; + uint256 sqth; +} + +/// @dev params for deposit auction +struct DepositAuctionParams { + /// @dev USDC to deposit + uint256 depositsQueued; + /// @dev minETH equivalent to get from uniswap of the USDC to deposit + uint256 minEth; + /// @dev total ETH to deposit after selling the minted SQTH + uint256 totalDeposit; + /// @dev orders to buy sqth + Order[] orders; + /// @dev price from the auction to sell sqth + uint256 clearingPrice; + /// @dev remaining ETH to flashDeposit + uint256 ethToFlashDeposit; + /// @dev fee to pay uniswap for ethUSD swap + uint24 ethUSDFee; + /// @dev fee to pay uniswap for sqthETH swap + uint24 flashDepositFee; +} + +/// @dev params for withdraw auction +struct WithdrawAuctionParams { + /// @dev amont of crab to queue for withdrawal + uint256 crabToWithdraw; + /// @dev orders that sell sqth to the auction + Order[] orders; + /// @dev price that the auction pays for the purchased sqth + uint256 clearingPrice; + /// @dev minUSDC to receive from swapping the ETH obtained by withdrawing + uint256 minUSDC; + /// @dev uniswap fee for swapping eth to USD; + uint24 ethUSDFee; +} + +/// @dev receipt used to store deposits and withdraws +struct Receipt { + /// @dev address of the depositor or withdrawer + address sender; + /// @dev usdc amount to queue for deposit or crab amount to queue for withdrawal + uint256 amount; +} + +/** + * @dev CrabNetting contract + * @notice Contract for Netting Deposits and Withdrawals + * @author Opyn team + */ +contract CrabNetting is Ownable, EIP712 { + /// @dev typehash for signed orders + bytes32 private constant _CRAB_NETTING_TYPEHASH = keccak256( + "Order(uint256 bidId,address trader,uint256 quantity,uint256 price,bool isBuying,uint256 expiry,uint256 nonce)" + ); + /// @dev owner sets to true when starting auction + bool public isAuctionLive; + + /// @dev sqth twap period + uint32 public immutable sqthTwapPeriod; + /// @dev twap period to use for auction calculations + uint32 public auctionTwapPeriod = 420 seconds; + + /// @dev min USDC amounts to withdraw or deposit via netting + uint256 public minUSDCAmount; + + /// @dev min CRAB amounts to withdraw or deposit via netting + uint256 public minCrabAmount; + + // @dev OTC price must be within this distance of the uniswap twap price + uint256 public otcPriceTolerance = 5e16; // 5% + // @dev OTC price tolerance cannot exceed 20% + uint256 public constant MAX_OTC_PRICE_TOLERANCE = 2e17; // 20% + + /// @dev address for ERC20 tokens + address public immutable usdc; + address public immutable crab; + address public immutable weth; + address public immutable sqth; + + /// @dev address for uniswap router + ISwapRouter public immutable swapRouter; + + /// @dev address for uniswap oracle + address public immutable oracle; + + /// @dev address for sqth eth pool + address public immutable ethSqueethPool; + + /// @dev address for usdc eth pool + address public immutable ethUsdcPool; + + /// @dev address for sqth controller + address public immutable sqthController; + + /// @dev array index of last processed deposits + uint256 public depositsIndex; + + /// @dev array index of last processed withdraws + uint256 public withdrawsIndex; + + /// @dev array of deposit receipts + Receipt[] public deposits; + /// @dev array of withdrawal receipts + Receipt[] public withdraws; + + /// @dev usd amount to deposit for an address + mapping(address => uint256) public usdBalance; + + /// @dev crab amount to withdraw for an address + mapping(address => uint256) public crabBalance; + + /// @dev indexes of deposit receipts of an address + mapping(address => uint256[]) public userDepositsIndex; + + /// @dev indexes of withdraw receipts of an address + mapping(address => uint256[]) public userWithdrawsIndex; + + /// @dev store the used flag for a nonce for each address + mapping(address => mapping(uint256 => bool)) public nonces; + + event USDCQueued( + address indexed depositor, uint256 amount, uint256 depositorsBalance, uint256 indexed receiptIndex + ); - constructor(address _usdc, address _crab) { - usdc = _usdc; + event USDCDeQueued(address indexed depositor, uint256 amount, uint256 depositorsBalance); + + event CrabQueued( + address indexed withdrawer, uint256 amount, uint256 withdrawersBalance, uint256 indexed receiptIndex + ); + + event CrabDeQueued(address indexed withdrawer, uint256 amount, uint256 withdrawersBalance); + + event USDCDeposited( + address indexed depositor, + uint256 usdcAmount, + uint256 crabAmount, + uint256 indexed receiptIndex, + uint256 refundedETH + ); + + event CrabWithdrawn( + address indexed withdrawer, uint256 crabAmount, uint256 usdcAmount, uint256 indexed receiptIndex + ); + + event BidTraded(uint256 indexed bidId, address indexed trader, uint256 quantity, uint256 price, bool isBuying); + + event SetAuctionTwapPeriod(uint32 previousTwap, uint32 newTwap); + event SetOTCPriceTolerance(uint256 previousTolerance, uint256 newOtcPriceTolerance); + event SetMinCrab(uint256 amount); + event SetMinUSDC(uint256 amount); + event NonceTrue(address sender, uint256 nonce); + event ToggledAuctionLive(bool isAuctionLive); + + /** + * @notice netting contract constructor + * @dev initializes the erc20 address, uniswap router and approves them + * @param _crab address of crab contract token + * @param _swapRouter address of uniswap swap router + */ + constructor(address _crab, address _swapRouter) EIP712("CRABNetting", "1") { crab = _crab; + swapRouter = ISwapRouter(_swapRouter); + + sqthController = ICrabStrategyV2(_crab).powerTokenController(); + usdc = IController(sqthController).quoteCurrency(); + weth = ICrabStrategyV2(_crab).weth(); + sqth = ICrabStrategyV2(_crab).wPowerPerp(); + oracle = ICrabStrategyV2(_crab).oracle(); + ethSqueethPool = ICrabStrategyV2(_crab).ethWSqueethPool(); + ethUsdcPool = IController(sqthController).ethQuoteCurrencyPool(); + sqthTwapPeriod = IController(sqthController).TWAP_PERIOD(); + + // approve crab and sqth so withdraw can happen + IERC20(sqth).approve(crab, type(uint256).max); + + IERC20(weth).approve(address(swapRouter), type(uint256).max); + IERC20(usdc).approve(address(swapRouter), type(uint256).max); + } + + /** + * @dev view function to get the domain seperator used in signing + */ + function DOMAIN_SEPARATOR() external view returns (bytes32) { + return _domainSeparatorV4(); } - function depositUSDC(uint256 amount) public { - IERC20(usdc).transferFrom(msg.sender, address(this), amount); - usd_balance[msg.sender] = usd_balance[msg.sender] + amount; - deposits.push(Receipt(msg.sender, amount)); + /** + * @dev toggles the value of isAuctionLive + */ + function toggleAuctionLive() external onlyOwner { + isAuctionLive = !isAuctionLive; + emit ToggledAuctionLive(isAuctionLive); } - function withdrawUSDC(uint256 amount) public { - require(usd_balance[msg.sender] >= amount); - usd_balance[msg.sender] = usd_balance[msg.sender] - amount; - IERC20(usdc).transfer(msg.sender, amount); + /** + * @notice set nonce to true + * @param _nonce the number to be set true + */ + function setNonceTrue(uint256 _nonce) external { + nonces[msg.sender][_nonce] = true; + emit NonceTrue(msg.sender, _nonce); } - function depositCrab(uint256 amount) public { - IERC20(crab).transferFrom(msg.sender, address(this), amount); - crab_balance[msg.sender] = crab_balance[msg.sender] + amount; - crab_deposits.push(Receipt(msg.sender, amount)); + /** + * @notice set minUSDCAmount + * @param _amount the number to be set as minUSDC + */ + function setMinUSDC(uint256 _amount) external onlyOwner { + minUSDCAmount = _amount; + emit SetMinUSDC(_amount); + } + + /** + * @notice set minCrabAmount + * @param _amount the number to be set as minCrab + */ + function setMinCrab(uint256 _amount) external onlyOwner { + minCrabAmount = _amount; + emit SetMinCrab(_amount); + } + + /** + * @notice queue USDC for deposit into crab strategy + * @param _amount USDC amount to deposit + */ + function depositUSDC(uint256 _amount) external { + require(_amount >= minUSDCAmount, "deposit amount smaller than minimum OTC amount"); + + IERC20(usdc).transferFrom(msg.sender, address(this), _amount); + + // update usd balance of user, add their receipt, and receipt index to user deposits index + usdBalance[msg.sender] = usdBalance[msg.sender] + _amount; + deposits.push(Receipt(msg.sender, _amount)); + userDepositsIndex[msg.sender].push(deposits.length - 1); + + emit USDCQueued(msg.sender, _amount, usdBalance[msg.sender], deposits.length - 1); + } + + /** + * @notice withdraw USDC from queue + * @param _amount USDC amount to dequeue + */ + function withdrawUSDC(uint256 _amount) external { + require(!isAuctionLive, "auction is live"); + + usdBalance[msg.sender] = usdBalance[msg.sender] - _amount; + require( + usdBalance[msg.sender] >= minUSDCAmount || usdBalance[msg.sender] == 0, + "remaining amount smaller than minimum, consider removing full balance" + ); + + // start withdrawing from the users last deposit + uint256 toRemove = _amount; + uint256 lastIndexP1 = userDepositsIndex[msg.sender].length; + for (uint256 i = lastIndexP1; i > 0; i--) { + Receipt storage r = deposits[userDepositsIndex[msg.sender][i - 1]]; + if (r.amount > toRemove) { + r.amount -= toRemove; + toRemove = 0; + break; + } else { + toRemove -= r.amount; + delete deposits[userDepositsIndex[msg.sender][i - 1]]; + } + } + IERC20(usdc).transfer(msg.sender, _amount); + + emit USDCDeQueued(msg.sender, _amount, usdBalance[msg.sender]); + } + + /** + * @notice queue Crab for withdraw from crab strategy + * @param _amount crab amount to withdraw + */ + function queueCrabForWithdrawal(uint256 _amount) external { + require(_amount >= minCrabAmount, "withdraw amount smaller than minimum OTC amount"); + IERC20(crab).transferFrom(msg.sender, address(this), _amount); + crabBalance[msg.sender] = crabBalance[msg.sender] + _amount; + withdraws.push(Receipt(msg.sender, _amount)); + userWithdrawsIndex[msg.sender].push(withdraws.length - 1); + emit CrabQueued(msg.sender, _amount, crabBalance[msg.sender], withdraws.length - 1); + } + + /** + * @notice withdraw Crab from queue + * @param _amount Crab amount to dequeue + */ + function dequeueCrab(uint256 _amount) external { + require(!isAuctionLive, "auction is live"); + crabBalance[msg.sender] = crabBalance[msg.sender] - _amount; + require( + crabBalance[msg.sender] >= minCrabAmount || crabBalance[msg.sender] == 0, + "remaining amount smaller than minimum, consider removing full balance" + ); + // deQueue crab from the last, last in first out + uint256 toRemove = _amount; + uint256 lastIndexP1 = userWithdrawsIndex[msg.sender].length; + for (uint256 i = lastIndexP1; i > 0; i--) { + Receipt storage r = withdraws[userWithdrawsIndex[msg.sender][i - 1]]; + if (r.amount > toRemove) { + r.amount -= toRemove; + toRemove = 0; + break; + } else { + toRemove -= r.amount; + delete withdraws[userWithdrawsIndex[msg.sender][i - 1]]; + } + } + IERC20(crab).transfer(msg.sender, _amount); + emit CrabDeQueued(msg.sender, _amount, crabBalance[msg.sender]); + } + + /** + * @dev swaps _quantity amount of usdc for crab at _price + * @param _price price of crab in usdc + * @param _quantity amount of USDC to net + */ + function netAtPrice(uint256 _price, uint256 _quantity) external onlyOwner { + _checkCrabPrice(_price); + uint256 crabQuantity = (_quantity * 1e18) / _price; + require(_quantity <= IERC20(usdc).balanceOf(address(this)), "Not enough deposits to net"); + require(crabQuantity <= IERC20(crab).balanceOf(address(this)), "Not enough withdrawals to net"); + + // process deposits and send crab + uint256 i = depositsIndex; + uint256 amountToSend; + while (_quantity > 0) { + Receipt memory deposit = deposits[i]; + if (deposit.amount == 0) { + i++; + continue; + } + if (deposit.amount <= _quantity) { + // deposit amount is lesser than quantity use it fully + _quantity = _quantity - deposit.amount; + usdBalance[deposit.sender] -= deposit.amount; + amountToSend = (deposit.amount * 1e18) / _price; + IERC20(crab).transfer(deposit.sender, amountToSend); + emit USDCDeposited(deposit.sender, deposit.amount, amountToSend, i, 0); + delete deposits[i]; + i++; + } else { + // deposit amount is greater than quantity; use it partially + deposits[i].amount = deposit.amount - _quantity; + usdBalance[deposit.sender] -= _quantity; + amountToSend = (_quantity * 1e18) / _price; + IERC20(crab).transfer(deposit.sender, amountToSend); + emit USDCDeposited(deposit.sender, _quantity, amountToSend, i, 0); + _quantity = 0; + } + } + depositsIndex = i; + + // process withdraws and send usdc + i = withdrawsIndex; + while (crabQuantity > 0) { + Receipt memory withdraw = withdraws[i]; + if (withdraw.amount == 0) { + i++; + continue; + } + if (withdraw.amount <= crabQuantity) { + crabQuantity = crabQuantity - withdraw.amount; + crabBalance[withdraw.sender] -= withdraw.amount; + amountToSend = (withdraw.amount * _price) / 1e18; + IERC20(usdc).transfer(withdraw.sender, amountToSend); + + emit CrabWithdrawn(withdraw.sender, withdraw.amount, amountToSend, i); + + delete withdraws[i]; + i++; + } else { + withdraws[i].amount = withdraw.amount - crabQuantity; + crabBalance[withdraw.sender] -= crabQuantity; + amountToSend = (crabQuantity * _price) / 1e18; + IERC20(usdc).transfer(withdraw.sender, amountToSend); + + emit CrabWithdrawn(withdraw.sender, withdraw.amount, amountToSend, i); + + crabQuantity = 0; + } + } + withdrawsIndex = i; + } + + /** + * @return sum usdc amount in queue + */ + function depositsQueued() external view returns (uint256) { + uint256 j = depositsIndex; + uint256 sum; + while (j < deposits.length) { + sum = sum + deposits[j].amount; + j++; + } + return sum; + } + + /** + * @return sum crab amount in queue + */ + function withdrawsQueued() external view returns (uint256) { + uint256 j = withdrawsIndex; + uint256 sum; + while (j < withdraws.length) { + sum = sum + withdraws[j].amount; + j++; + } + return sum; + } + + function checkOrder(Order memory _order) external { + return _checkOrder(_order); + } + + /** + * @dev checks the expiry nonce and signer of an order + * @param _order is the Order struct + */ + function _checkOrder(Order memory _order) internal { + _useNonce(_order.trader, _order.nonce); + bytes32 structHash = keccak256( + abi.encode( + _CRAB_NETTING_TYPEHASH, + _order.bidId, + _order.trader, + _order.quantity, + _order.price, + _order.isBuying, + _order.expiry, + _order.nonce + ) + ); + + bytes32 hash = _hashTypedDataV4(structHash); + address offerSigner = ECDSA.recover(hash, _order.v, _order.r, _order.s); + require(offerSigner == _order.trader, "Signature not correct"); + require(_order.expiry >= block.timestamp, "order expired"); + } + + /** + * @dev calculates wSqueeth minted when amount is deposited + * @param _amount to deposit into crab + */ + function _debtToMint(uint256 _amount) internal view returns (uint256) { + uint256 feeAdjustment = _calcFeeAdjustment(); + (,, uint256 collateral, uint256 debt) = ICrabStrategyV2(crab).getVaultDetails(); + uint256 wSqueethToMint = (_amount * debt) / (collateral + (debt * feeAdjustment)); + return wSqueethToMint; + } + + /** + * @dev takes in orders from mm's to buy sqth and deposits the usd amount from the depositQueue into crab along with the eth from selling sqth + * @param _p DepositAuction Params that contain orders, usdToDeposit, uniswap min amount and fee + */ + function depositAuction(DepositAuctionParams calldata _p) external onlyOwner { + _checkOTCPrice(_p.clearingPrice, false); + /** + * step 1: get eth from mm + * step 2: get eth from deposit usdc + * step 3: crab deposit + * step 4: flash deposit + * step 5: send sqth to mms + * step 6: send crab to depositors + */ + uint256 initCrabBalance = IERC20(crab).balanceOf(address(this)); + uint256 initEthBalance = address(this).balance; + + uint256 sqthToSell = _debtToMint(_p.totalDeposit); + // step 1 get all the eth in + uint256 remainingToSell = sqthToSell; + for (uint256 i = 0; i < _p.orders.length; i++) { + require(_p.orders[i].isBuying, "auction order not buying sqth"); + require(_p.orders[i].price >= _p.clearingPrice, "buy order price less than clearing"); + _checkOrder(_p.orders[i]); + if (_p.orders[i].quantity >= remainingToSell) { + IWETH(weth).transferFrom( + _p.orders[i].trader, address(this), (remainingToSell * _p.clearingPrice) / 1e18 + ); + remainingToSell = 0; + break; + } else { + IWETH(weth).transferFrom( + _p.orders[i].trader, address(this), (_p.orders[i].quantity * _p.clearingPrice) / 1e18 + ); + remainingToSell -= _p.orders[i].quantity; + } + } + require(remainingToSell == 0, "not enough buy orders for sqth"); + + // step 2 + ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({ + tokenIn: usdc, + tokenOut: weth, + fee: _p.ethUSDFee, + recipient: address(this), + deadline: block.timestamp, + amountIn: _p.depositsQueued, + amountOutMinimum: _p.minEth, + sqrtPriceLimitX96: 0 + }); + swapRouter.exactInputSingle(params); + + // step 3 + IWETH(weth).withdraw(IWETH(weth).balanceOf(address(this))); + ICrabStrategyV2(crab).deposit{value: _p.totalDeposit}(); + + // step 4 + Portion memory to_send; + to_send.eth = address(this).balance - initEthBalance; + if (to_send.eth > 0 && _p.ethToFlashDeposit > 0) { + if (to_send.eth <= _p.ethToFlashDeposit) { + // we cant send more than the flashDeposit + ICrabStrategyV2(crab).flashDeposit{value: to_send.eth}(_p.ethToFlashDeposit, _p.flashDepositFee); + } + } + + // step 5 + to_send.sqth = IERC20(sqth).balanceOf(address(this)); + remainingToSell = to_send.sqth; + for (uint256 j = 0; j < _p.orders.length; j++) { + if (_p.orders[j].quantity < remainingToSell) { + IERC20(sqth).transfer(_p.orders[j].trader, _p.orders[j].quantity); + remainingToSell -= _p.orders[j].quantity; + emit BidTraded(_p.orders[j].bidId, _p.orders[j].trader, _p.orders[j].quantity, _p.clearingPrice, true); + } else { + IERC20(sqth).transfer(_p.orders[j].trader, remainingToSell); + emit BidTraded(_p.orders[j].bidId, _p.orders[j].trader, remainingToSell, _p.clearingPrice, true); + break; + } + } + + // step 6 send crab to depositors + uint256 remainingDeposits = _p.depositsQueued; + uint256 k = depositsIndex; + + to_send.crab = IERC20(crab).balanceOf(address(this)) - initCrabBalance; + // get the balance between start and now + to_send.eth = address(this).balance - initEthBalance; + IWETH(weth).deposit{value: to_send.eth}(); + + while (remainingDeposits > 0) { + uint256 queuedAmount = deposits[k].amount; + Portion memory portion; + if (queuedAmount == 0) { + k++; + continue; + } + if (queuedAmount <= remainingDeposits) { + remainingDeposits = remainingDeposits - queuedAmount; + usdBalance[deposits[k].sender] -= queuedAmount; + + portion.crab = (((queuedAmount * 1e18) / _p.depositsQueued) * to_send.crab) / 1e18; + + IERC20(crab).transfer(deposits[k].sender, portion.crab); + + portion.eth = (((queuedAmount * 1e18) / _p.depositsQueued) * to_send.eth) / 1e18; + if (portion.eth > 1e12) { + IWETH(weth).transfer(deposits[k].sender, portion.eth); + } else { + portion.eth = 0; + } + emit USDCDeposited(deposits[k].sender, queuedAmount, portion.crab, k, portion.eth); + + delete deposits[k]; + k++; + } else { + usdBalance[deposits[k].sender] -= remainingDeposits; + + portion.crab = (((remainingDeposits * 1e18) / _p.depositsQueued) * to_send.crab) / 1e18; + IERC20(crab).transfer(deposits[k].sender, portion.crab); + + portion.eth = (((remainingDeposits * 1e18) / _p.depositsQueued) * to_send.eth) / 1e18; + if (portion.eth > 1e12) { + IWETH(weth).transfer(deposits[k].sender, portion.eth); + } else { + portion.eth = 0; + } + emit USDCDeposited(deposits[k].sender, remainingDeposits, portion.crab, k, portion.eth); + + deposits[k].amount -= remainingDeposits; + remainingDeposits = 0; + } + } + depositsIndex = k; + isAuctionLive = false; + } + + /** + * @dev takes in orders from mm's to sell sqth and withdraws the crab amount in q + * @param _p Withdraw Params that contain orders, crabToWithdraw, uniswap min amount and fee + */ + function withdrawAuction(WithdrawAuctionParams calldata _p) public onlyOwner { + _checkOTCPrice(_p.clearingPrice, true); + uint256 initWethBalance = IERC20(weth).balanceOf(address(this)); + uint256 initEthBalance = address(this).balance; + /** + * step 1: get sqth from mms + * step 2: withdraw from crab + * step 3: send eth to mms + * step 4: convert eth to usdc + * step 5: send usdc to withdrawers + */ + + // step 1 get sqth from mms + uint256 sqthRequired = ICrabStrategyV2(crab).getWsqueethFromCrabAmount(_p.crabToWithdraw); + uint256 toPull = sqthRequired; + for (uint256 i = 0; i < _p.orders.length && toPull > 0; i++) { + _checkOrder(_p.orders[i]); + require(!_p.orders[i].isBuying, "auction order is not selling"); + require(_p.orders[i].price <= _p.clearingPrice, "sell order price greater than clearing"); + if (_p.orders[i].quantity < toPull) { + toPull -= _p.orders[i].quantity; + IERC20(sqth).transferFrom(_p.orders[i].trader, address(this), _p.orders[i].quantity); + } else { + IERC20(sqth).transferFrom(_p.orders[i].trader, address(this), toPull); + toPull = 0; + } + } + + // step 2 withdraw from crab + ICrabStrategyV2(crab).withdraw(_p.crabToWithdraw); + + // step 3 pay all mms + IWETH(weth).deposit{value: address(this).balance - initEthBalance}(); + toPull = sqthRequired; + uint256 sqthQuantity; + for (uint256 i = 0; i < _p.orders.length && toPull > 0; i++) { + if (_p.orders[i].quantity < toPull) { + sqthQuantity = _p.orders[i].quantity; + } else { + sqthQuantity = toPull; + } + IERC20(weth).transfer(_p.orders[i].trader, (sqthQuantity * _p.clearingPrice) / 1e18); + toPull -= sqthQuantity; + emit BidTraded(_p.orders[i].bidId, _p.orders[i].trader, sqthQuantity, _p.clearingPrice, false); + } + + // step 4 convert to USDC + ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({ + tokenIn: address(weth), + tokenOut: address(usdc), + fee: _p.ethUSDFee, + recipient: address(this), + deadline: block.timestamp, + amountIn: (IERC20(weth).balanceOf(address(this)) - initWethBalance), + amountOutMinimum: _p.minUSDC, + sqrtPriceLimitX96: 0 + }); + uint256 usdcReceived = swapRouter.exactInputSingle(params); + + // step 5 pay all withdrawers and mark their withdraws as done + uint256 remainingWithdraws = _p.crabToWithdraw; + uint256 j = withdrawsIndex; + uint256 usdcAmount; + while (remainingWithdraws > 0) { + Receipt memory withdraw = withdraws[j]; + if (withdraw.amount == 0) { + j++; + continue; + } + if (withdraw.amount <= remainingWithdraws) { + // full usage + remainingWithdraws -= withdraw.amount; + crabBalance[withdraw.sender] -= withdraw.amount; + + // send proportional usdc + usdcAmount = (((withdraw.amount * 1e18) / _p.crabToWithdraw) * usdcReceived) / 1e18; + IERC20(usdc).transfer(withdraw.sender, usdcAmount); + emit CrabWithdrawn(withdraw.sender, withdraw.amount, usdcAmount, j); + delete withdraws[j]; + j++; + } else { + withdraws[j].amount -= remainingWithdraws; + crabBalance[withdraw.sender] -= remainingWithdraws; + + // send proportional usdc + usdcAmount = (((remainingWithdraws * 1e18) / _p.crabToWithdraw) * usdcReceived) / 1e18; + IERC20(usdc).transfer(withdraw.sender, usdcAmount); + emit CrabWithdrawn(withdraw.sender, remainingWithdraws, usdcAmount, j); + + remainingWithdraws = 0; + } + } + withdrawsIndex = j; + isAuctionLive = false; + } + + /** + * @notice owner can set the twap period in seconds that is used for obtaining TWAP prices + * @param _auctionTwapPeriod the twap period, in seconds + */ + function setAuctionTwapPeriod(uint32 _auctionTwapPeriod) external onlyOwner { + require(_auctionTwapPeriod >= 180, "twap period cannot be less than 180"); + uint32 previousTwap = auctionTwapPeriod; + + auctionTwapPeriod = _auctionTwapPeriod; + + emit SetAuctionTwapPeriod(previousTwap, _auctionTwapPeriod); + } + + /** + * @notice owner can set a threshold, scaled by 1e18 that determines the maximum discount of a clearing sale price to the current uniswap twap price + * @param _otcPriceTolerance the OTC price tolerance, in percent, scaled by 1e18 + */ + function setOTCPriceTolerance(uint256 _otcPriceTolerance) external onlyOwner { + // Tolerance cannot be more than 20% + require(_otcPriceTolerance <= MAX_OTC_PRICE_TOLERANCE, "Price tolerance has to be less than 20%"); + uint256 previousOtcTolerance = auctionTwapPeriod; + + otcPriceTolerance = _otcPriceTolerance; + + emit SetOTCPriceTolerance(previousOtcTolerance, _otcPriceTolerance); + } + + /** + * @dev set nonce flag of the trader to true + * @param _trader address of the signer + * @param _nonce number that is to be traded only once + */ + function _useNonce(address _trader, uint256 _nonce) internal { + require(!nonces[_trader][_nonce], "Nonce already used"); + nonces[_trader][_nonce] = true; + } + + /** + * @notice check that the proposed sale price is within a tolerance of the current Uniswap twap + * @param _price clearing price provided by manager + * @param _isAuctionBuying is crab buying or selling oSQTH + */ + function _checkOTCPrice(uint256 _price, bool _isAuctionBuying) internal view { + // Get twap + uint256 squeethEthPrice = IOracle(oracle).getTwap(ethSqueethPool, sqth, weth, auctionTwapPeriod, true); + + if (_isAuctionBuying) { + require( + _price <= (squeethEthPrice * (1e18 + otcPriceTolerance)) / 1e18, + "Price too high relative to Uniswap twap." + ); + } else { + require( + _price >= (squeethEthPrice * (1e18 - otcPriceTolerance)) / 1e18, + "Price too low relative to Uniswap twap." + ); + } } - function withdrawCrab(uint256 amount) public { - require(crab_balance[msg.sender] >= amount); - crab_balance[msg.sender] = crab_balance[msg.sender] - amount; - IERC20(crab).transfer(msg.sender, amount); + function _checkCrabPrice(uint256 _price) internal view { + // Get twap + uint256 squeethEthPrice = IOracle(oracle).getTwap(ethSqueethPool, sqth, weth, auctionTwapPeriod, true); + uint256 usdcEthPrice = IOracle(oracle).getTwap(ethUsdcPool, weth, usdc, auctionTwapPeriod, true); + (,, uint256 collateral, uint256 debt) = ICrabStrategyV2(crab).getVaultDetails(); + uint256 crabFairPrice = + ((collateral - ((debt * squeethEthPrice) / 1e18)) * usdcEthPrice) / ICrabStrategyV2(crab).totalSupply(); + crabFairPrice = crabFairPrice / 1e12; //converting from units of 18 to 6 + require(_price <= (crabFairPrice * (1e18 + otcPriceTolerance)) / 1e18, "Crab Price too high"); + require(_price >= (crabFairPrice * (1e18 - otcPriceTolerance)) / 1e18, "Crab Price too low"); } - function balanceOf(address account) public view returns (uint256) { - return usd_balance[account]; + function _calcFeeAdjustment() internal view returns (uint256) { + uint256 feeRate = IController(sqthController).feeRate(); + if (feeRate == 0) return 0; + uint256 squeethEthPrice = IOracle(oracle).getTwap(ethSqueethPool, sqth, weth, sqthTwapPeriod, true); + return (squeethEthPrice * feeRate) / 10000; } - function crabBalanceOf(address account) public view returns (uint256) { - return crab_balance[account]; + receive() external payable { + require(msg.sender == weth || msg.sender == crab, "only weth and crab can send me monies"); } } diff --git a/packages/crab-netting/src/interfaces/IController.sol b/packages/crab-netting/src/interfaces/IController.sol new file mode 100644 index 000000000..ea6ee9502 --- /dev/null +++ b/packages/crab-netting/src/interfaces/IController.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +interface IController { + function feeRate() external view returns (uint256); + + function TWAP_PERIOD() external view returns (uint32); + + function quoteCurrency() external view returns (address); + + function ethQuoteCurrencyPool() external view returns (address); +} diff --git a/packages/crab-netting/src/interfaces/ICrabStrategyV2.sol b/packages/crab-netting/src/interfaces/ICrabStrategyV2.sol new file mode 100644 index 000000000..ae451924b --- /dev/null +++ b/packages/crab-netting/src/interfaces/ICrabStrategyV2.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {IERC20} from "openzeppelin/interfaces/IERC20.sol"; + +interface ICrabStrategyV2 is IERC20 { + function getVaultDetails() external view returns (address, uint256, uint256, uint256); + + function deposit() external payable; + + function withdraw(uint256 _crabAmount) external; + + function flashDeposit(uint256 _ethToDeposit, uint24 _poolFee) external payable; + + function getWsqueethFromCrabAmount(uint256 _crabAmount) external view returns (uint256); + + function powerTokenController() external view returns (address); + + function weth() external view returns (address); + + function wPowerPerp() external view returns (address); + + function oracle() external view returns (address); + + function ethWSqueethPool() external view returns (address); +} diff --git a/packages/crab-netting/src/interfaces/IOracle.sol b/packages/crab-netting/src/interfaces/IOracle.sol new file mode 100644 index 000000000..4e9de6cbc --- /dev/null +++ b/packages/crab-netting/src/interfaces/IOracle.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +interface IOracle { + function getTwap(address _pool, address _base, address _quote, uint32 _period, bool _checkPeriod) + external + view + returns (uint256); +} diff --git a/packages/crab-netting/src/interfaces/IWETH.sol b/packages/crab-netting/src/interfaces/IWETH.sol new file mode 100644 index 000000000..aadc9fb00 --- /dev/null +++ b/packages/crab-netting/src/interfaces/IWETH.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {IERC20} from "openzeppelin/interfaces/IERC20.sol"; + +interface IWETH is IERC20 { + function deposit() external payable; + + function withdraw(uint256 wad) external; +} diff --git a/packages/crab-netting/test/BaseForkSetup.t.sol b/packages/crab-netting/test/BaseForkSetup.t.sol new file mode 100644 index 000000000..c2846f007 --- /dev/null +++ b/packages/crab-netting/test/BaseForkSetup.t.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import "forge-std/Test.sol"; + +import {ERC20} from "openzeppelin/token/ERC20/ERC20.sol"; +import {IWETH} from "../src/interfaces/IWETH.sol"; +import {IOracle} from "../src/interfaces/IOracle.sol"; +import {ICrabStrategyV2} from "../src/interfaces/ICrabStrategyV2.sol"; +import {CrabNetting, Order} from "../src/CrabNetting.sol"; +import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; +import {IQuoter} from "@uniswap/v3-periphery/contracts/interfaces/IQuoter.sol"; + +contract BaseForkSetup is Test { + ICrabStrategyV2 crab; + ERC20 usdc; + IWETH weth; + ERC20 sqth; + CrabNetting netting; + ISwapRouter swapRouter; + IQuoter quoter; + IOracle oracle; + uint256 activeFork; + + uint256 internal ownerPrivateKey; + address internal owner; + uint256 internal depositorPk; + address internal depositor; + uint256 internal withdrawerPk; + address internal withdrawer; + + uint256 internal mm1Pk; + address internal mm1; + + uint256 internal mm2Pk; + address internal mm2; + + Order[] orders; + + function setUp() public virtual { + string memory FORK_URL = vm.envString("FORK_URL"); + activeFork = vm.createSelectFork(FORK_URL, 15819213); + + crab = ICrabStrategyV2(0x3B960E47784150F5a63777201ee2B15253D713e8); + weth = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + usdc = ERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + sqth = ERC20(0xf1B99e3E573A1a9C5E6B2Ce818b617F0E664E86B); + swapRouter = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564); + quoter = IQuoter(0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6); + oracle = IOracle(0x65D66c76447ccB45dAf1e8044e918fA786A483A1); + + netting = new CrabNetting(address(crab), address(swapRouter)); + vm.prank(address(netting)); + payable(depositor).transfer(address(netting).balance); + + ownerPrivateKey = 0xA11CE; + owner = vm.addr(ownerPrivateKey); + + depositorPk = 0xA11CA; + depositor = vm.addr(depositorPk); + vm.label(depositor, "depositor"); + + withdrawerPk = 0xA11CB; + withdrawer = vm.addr(withdrawerPk); + vm.label(withdrawer, "withdrawer"); + + mm1Pk = 0xA11CC; + mm1 = vm.addr(mm1Pk); + vm.label(mm1, "market maker 1"); + mm2Pk = 0xA11CA; + mm2 = vm.addr(mm2Pk); + vm.label(mm2, "market maker 2"); + } +} diff --git a/packages/crab-netting/test/BaseSetup.t.sol b/packages/crab-netting/test/BaseSetup.t.sol new file mode 100644 index 000000000..115623c35 --- /dev/null +++ b/packages/crab-netting/test/BaseSetup.t.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import "forge-std/Test.sol"; +import {ERC20} from "openzeppelin/token/ERC20/ERC20.sol"; + +import {CrabNetting} from "../src/CrabNetting.sol"; +import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; +import {IOracle} from "../src/interfaces/IOracle.sol"; + +contract FixedERC20 is ERC20 { + constructor(uint256 initialSupply) ERC20("USDC", "USDC") { + _mint(msg.sender, initialSupply); + } +} + +contract BaseSetup is Test { + FixedERC20 usdc; + FixedERC20 crab; + FixedERC20 weth; + FixedERC20 sqth; + CrabNetting netting; + ISwapRouter public immutable swapRouter = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564); + + IOracle public immutable oracle = IOracle(0x65D66c76447ccB45dAf1e8044e918fA786A483A1); + + uint256 internal ownerPrivateKey; + address internal owner; + uint256 internal depositorPk; + address internal depositor; + uint256 internal withdrawerPk; + address internal withdrawer; + + function setUp() public virtual { + usdc = new FixedERC20(10000 * 1e6); + crab = new FixedERC20(10000 * 1e18); + weth = new FixedERC20(10000 * 1e18); + sqth = new FixedERC20(10000 * 1e18); + netting = new CrabNetting(address(crab), address(swapRouter)); + + ownerPrivateKey = 0xA11CE; + owner = vm.addr(ownerPrivateKey); + + depositorPk = 0xA11CA; + depositor = vm.addr(depositorPk); + vm.label(depositor, "depositor"); + + withdrawerPk = 0xA11CB; + withdrawer = vm.addr(withdrawerPk); + vm.label(withdrawer, "withdrawer"); + } +} diff --git a/packages/crab-netting/test/Deposit.t.sol b/packages/crab-netting/test/Deposit.t.sol index 98e25217c..b0d1472a4 100644 --- a/packages/crab-netting/test/Deposit.t.sol +++ b/packages/crab-netting/test/Deposit.t.sol @@ -1,64 +1,133 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; -import "forge-std/Test.sol"; -import {ERC20} from "openzeppelin/token/ERC20/ERC20.sol"; +import {BaseForkSetup} from "./BaseForkSetup.t.sol"; -import {CrabNetting} from "../src/CrabNetting.sol"; +contract DepositTest is BaseForkSetup { + function setUp() public override { + BaseForkSetup.setUp(); // gives you netting, depositor, withdrawer, usdc, crab -contract FixedERC20 is ERC20 { - constructor(uint256 initialSupply) ERC20("USDC", "USDC") { - _mint(msg.sender, initialSupply); + vm.startPrank(0x57757E3D981446D585Af0D9Ae4d7DF6D64647806); + usdc.transfer(depositor, 20e6); + vm.stopPrank(); + + vm.prank(0x06CECFbac34101aE41C88EbC2450f8602b3d164b); + crab.transfer(withdrawer, 20e18); } -} -contract DepositTest is Test { - FixedERC20 usdc; - FixedERC20 crab; - CrabNetting netting; + function testDepositMin() public { + netting.setMinUSDC(1e6); + vm.startPrank(depositor); + usdc.approve(address(netting), 2 * 1e6); + netting.depositUSDC(1e6); + assertEq(netting.usdBalance(depositor), 1e6); + } - uint256 internal ownerPrivateKey; - address internal owner; - uint256 internal depositorPk; - address internal depositor; - uint256 internal withdrawerPk; - address internal withdrawer; + function testDepositLessThanMin() public { + netting.setMinUSDC(1e6); + vm.startPrank(depositor); + usdc.approve(address(netting), 5 * 1e5); + vm.expectRevert(); + netting.depositUSDC(5e5); + } - function setUp() public { - usdc = new FixedERC20(10000 * 1e18); - crab = new FixedERC20(10000 * 1e18); - netting = new CrabNetting(address(usdc), address(crab)); + function testDepositAndWithdrawPartialUSDC() public { + vm.startPrank(depositor); + usdc.approve(address(netting), 2 * 1e6); - ownerPrivateKey = 0xA11CE; - owner = vm.addr(ownerPrivateKey); + netting.depositUSDC(2 * 1e6); + assertEq(netting.usdBalance(depositor), 2e6); - depositorPk = 0xA11CA; - depositor = vm.addr(depositorPk); + netting.withdrawUSDC(1 * 1e6); - withdrawerPk = 0xA11CB; - withdrawer = vm.addr(withdrawerPk); + assertEq(netting.usdBalance(depositor), 1e6); + assertEq(netting.depositsQueued(), 1e6); + } - usdc.transfer(depositor, 2 * 1e18); - crab.transfer(withdrawer, 2 * 1e18); + function testDepositAndWithdrawFullUSDC() public { + vm.startPrank(depositor); + usdc.approve(address(netting), 2 * 1e6); + + netting.depositUSDC(2 * 1e6); + assertEq(netting.usdBalance(depositor), 2e6); + + netting.withdrawUSDC(2 * 1e6); + assertEq(netting.usdBalance(depositor), 0); + assertEq(netting.depositsQueued(), 0); } - function testDepositAndWithdraw() public { + function testLargeWithdraw() public { vm.startPrank(depositor); - usdc.approve(address(netting), 2 * 1e18); - netting.depositUSDC(2 * 1e18); + usdc.approve(address(netting), 4 * 1e6); + + netting.depositUSDC(2 * 1e6); + netting.depositUSDC(2 * 1e6); + assertEq(netting.usdBalance(depositor), 4e6); + + netting.withdrawUSDC(3 * 1e6); + assertEq(netting.usdBalance(depositor), 1e6); + assertEq(netting.depositsQueued(), 1e6); + } + + function testDepositAndWithdrawCrabPartial() public { + vm.startPrank(withdrawer); + crab.approve(address(netting), 2 * 1e6); + + netting.queueCrabForWithdrawal(2 * 1e6); + assertEq(netting.crabBalance(withdrawer), 2e6); - assertEq(netting.balanceOf(depositor), 2e18); - netting.withdrawUSDC(1 * 1e18); - assertEq(netting.balanceOf(depositor), 1e18); + netting.dequeueCrab(1 * 1e6); + assertEq(netting.crabBalance(withdrawer), 1e6); + assertEq(netting.withdrawsQueued(), 1e6); } - function testCrabDepositAndWithdraw() public { + function testDepositAndWithdrawCrabFull() public { + vm.startPrank(withdrawer); + crab.approve(address(netting), 2 * 1e6); + + netting.queueCrabForWithdrawal(2 * 1e6); + assertEq(netting.crabBalance(withdrawer), 2e6); + + netting.dequeueCrab(2 * 1e6); + assertEq(netting.crabBalance(withdrawer), 0); + assertEq(netting.withdrawsQueued(), 0); + } + + function testCrabDepositLargeWithdraw() public { + vm.startPrank(withdrawer); + crab.approve(address(netting), 4 * 1e6); + + netting.queueCrabForWithdrawal(2 * 1e6); + netting.queueCrabForWithdrawal(2 * 1e6); + assertEq(netting.crabBalance(withdrawer), 4e6); + + netting.dequeueCrab(3 * 1e6); + + assertEq(netting.crabBalance(withdrawer), 1e6, "withdrawer balance incorrect"); + assertEq(netting.withdrawsQueued(), 1e6, "withdraws queued balance incorrect"); + } + + function testCannotWithdrawCrabWhenAuctionLive() public { + netting.toggleAuctionLive(); + vm.startPrank(withdrawer); crab.approve(address(netting), 2 * 1e18); - netting.depositCrab(2 * 1e18); + netting.queueCrabForWithdrawal(2 * 1e18); + + vm.expectRevert(bytes("auction is live")); + netting.dequeueCrab(2 * 1e18); + vm.stopPrank(); + } + + function testCannotWithdrawUSDCWhenAuctionLive() public { + netting.toggleAuctionLive(); + + vm.startPrank(depositor); + usdc.approve(address(netting), 2 * 1e6); + netting.depositUSDC(2 * 1e6); - assertEq(netting.crabBalanceOf(withdrawer), 2e18); - netting.withdrawCrab(1 * 1e18); - assertEq(netting.crabBalanceOf(withdrawer), 1e18); + vm.expectRevert(bytes("auction is live")); + netting.withdrawUSDC(2 * 1e6); + vm.stopPrank(); } } diff --git a/packages/crab-netting/test/DepositAuction.t.sol b/packages/crab-netting/test/DepositAuction.t.sol new file mode 100644 index 000000000..d7dcfe033 --- /dev/null +++ b/packages/crab-netting/test/DepositAuction.t.sol @@ -0,0 +1,430 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import {BaseForkSetup} from "./BaseForkSetup.t.sol"; + +import {Order, DepositAuctionParams} from "../src/CrabNetting.sol"; +import {ICrabStrategyV2} from "../src/interfaces/ICrabStrategyV2.sol"; + +import {SigUtils} from "./utils/SigUtils.sol"; + +struct Sign { + uint8 v; + bytes32 r; + bytes32 s; +} + +contract DepositAuctionTest is BaseForkSetup { + SigUtils sig; + + function setUp() public override { + BaseForkSetup.setUp(); + sig = new SigUtils(netting.DOMAIN_SEPARATOR()); + + vm.deal(depositor, 100000000e18); + + // this is a crab whale, get some crab token from + //vm.prank(0x06CECFbac34101aE41C88EbC2450f8602b3d164b); + // crab.tranfer(depositor, 100e18); + + // some WETH and USDC rich address + vm.startPrank(0x57757E3D981446D585Af0D9Ae4d7DF6D64647806); + weth.transfer(depositor, 10000e18); + weth.transfer(mm1, 1000e18); + weth.transfer(mm2, 1000e18); + usdc.transfer(depositor, 500000e6); + vm.stopPrank(); + + vm.startPrank(depositor); + usdc.approve(address(netting), 1500000 * 1e6); + netting.depositUSDC(200000 * 1e6); + vm.stopPrank(); + + // depositor has queued in 200k USDC + } + + function _findTotalDepositAndToMint(uint256 _eth, uint256 _collateral, uint256 _debt, uint256 _price) + internal + pure + returns (uint256, uint256) + { + uint256 totalDeposit = (_eth * 1e18) / (1e18 - ((_debt * _price) / _collateral)); + return (totalDeposit, (totalDeposit * _debt) / _collateral); + } + + function _findTotalDepositFromAuctioned(uint256 _collateral, uint256 _debt, uint256 _auctionedSqth) + internal + pure + returns (uint256) + { + return (_collateral * _auctionedSqth) / _debt; + } + + function testDepositAuctionPartialFill() public { + DepositAuctionParams memory p; + uint256 sqthPriceLimit = (_getSqthPrice(1e18) * 988) / 1000; + (,, uint256 collateral, uint256 debt) = crab.getVaultDetails(); + // Large first deposit. 10 & 40 as the deposit. 20 is the amount to net + vm.prank(depositor); + netting.depositUSDC(300000 * 1e6); //200+300 500k usdc deposited + + p.depositsQueued = 300000 * 1e6; + p.minEth = (_convertUSDToETH(p.depositsQueued) * 9975) / 10000; + + uint256 toMint; + (p.totalDeposit, toMint) = _findTotalDepositAndToMint(p.minEth, collateral, debt, sqthPriceLimit); + bool trade_works = _isEnough(p.minEth, toMint, sqthPriceLimit, p.totalDeposit); + require(trade_works, "depositing more than we have from sellling"); + Order memory order = + Order(0, mm1, toMint, (sqthPriceLimit * 1005) / 1000, true, block.timestamp, 0, 1, 0x00, 0x00); + + bytes32 digest = sig.getTypedDataHash(order); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(mm1Pk, digest); + order.v = v; + order.r = r; + order.s = s; + + orders.push(order); + p.orders = orders; + vm.prank(mm1); + weth.approve(address(netting), 1e30); + + p.clearingPrice = (sqthPriceLimit * 1005) / 1000; + uint256 excessEth = (toMint * (p.clearingPrice - sqthPriceLimit)) / 1e18; + + p.ethUSDFee = 500; + p.flashDepositFee = 3000; + + // Find the borrow ration for toFlash + uint256 mid = _findBorrow(excessEth, debt, collateral); + p.ethToFlashDeposit = (excessEth * mid) / 10 ** 7; + // ------------- // + uint256 depositorBalance = weth.balanceOf(depositor); + netting.depositAuction(p); + + assertApproxEqAbs(ICrabStrategyV2(crab).balanceOf(depositor), 221e18, 1e18); + assertEq(netting.usdBalance(depositor), 200000e6); + assertEq(sqth.balanceOf(mm1), toMint); + assertEq(weth.balanceOf(address(netting)), 1); + assertApproxEqAbs(weth.balanceOf(depositor) - depositorBalance, 5e17, 1e17); + } + + function testDepositAuctionAfterFullWithdrawal() public { + vm.startPrank(depositor); + console.log(netting.usdBalance(depositor), "depositor balance"); + netting.withdrawUSDC(netting.usdBalance(depositor)); + assertEq(netting.usdBalance(depositor), 0, "depositor balancez ero"); + netting.depositUSDC(200000e6); + vm.stopPrank(); + + DepositAuctionParams memory p; + uint256 sqthPriceLimit = (_getSqthPrice(1e18) * 988) / 1000; + (,, uint256 collateral, uint256 debt) = crab.getVaultDetails(); + // Large first deposit. 10 & 40 as the deposit. 20 is the amount to net + vm.prank(depositor); + netting.depositUSDC(300000 * 1e6); //200+300 500k usdc deposited + + p.depositsQueued = 300000 * 1e6; + p.minEth = (_convertUSDToETH(p.depositsQueued) * 9975) / 10000; + + uint256 toMint; + (p.totalDeposit, toMint) = _findTotalDepositAndToMint(p.minEth, collateral, debt, sqthPriceLimit); + bool trade_works = _isEnough(p.minEth, toMint, sqthPriceLimit, p.totalDeposit); + require(trade_works, "depositing more than we have from sellling"); + Order memory order = + Order(0, mm1, toMint, (sqthPriceLimit * 1005) / 1000, true, block.timestamp, 0, 1, 0x00, 0x00); + + bytes32 digest = sig.getTypedDataHash(order); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(mm1Pk, digest); + order.v = v; + order.r = r; + order.s = s; + + orders.push(order); + p.orders = orders; + vm.prank(mm1); + weth.approve(address(netting), 1e30); + + p.clearingPrice = (sqthPriceLimit * 1005) / 1000; + uint256 excessEth = (toMint * (p.clearingPrice - sqthPriceLimit)) / 1e18; + + p.ethUSDFee = 500; + p.flashDepositFee = 3000; + + // Find the borrow ration for toFlash + uint256 mid = _findBorrow(excessEth, debt, collateral); + p.ethToFlashDeposit = (excessEth * mid) / 10 ** 7; + // ------------- // + uint256 depositorBalance = weth.balanceOf(depositor); + console.log(depositorBalance, "balance bfore"); + netting.depositAuction(p); + + console.log(ICrabStrategyV2(crab).balanceOf(depositor), "crab balance"); + assertGt(ICrabStrategyV2(crab).balanceOf(depositor), 221e18); + assertEq(netting.usdBalance(depositor), 200000e6); + assertEq(sqth.balanceOf(mm1), toMint); + assertLe(weth.balanceOf(address(netting)), 1e16); + assertGt(weth.balanceOf(depositor) - depositorBalance, 5e17, "0.5 eth not remaining"); + assertEq(netting.depositsIndex(), 2); + } + + function testSqthPriceTooLow() public { + DepositAuctionParams memory p; + uint256 sqthPriceLimit = (_getSqthPrice(1e18) * 99) / 100; + (,, uint256 collateral, uint256 debt) = crab.getVaultDetails(); + // Large first deposit. 10 & 40 as the deposit. 20 is the amount to net + vm.prank(depositor); + netting.depositUSDC(300000 * 1e6); //200+300 500k usdc deposited + + p.depositsQueued = 300000 * 1e6; + p.minEth = (_convertUSDToETH(p.depositsQueued) * 9975) / 10000; + + uint256 toMint; + (p.totalDeposit, toMint) = _findTotalDepositAndToMint(p.minEth, collateral, debt, sqthPriceLimit); + Order memory order = Order(0, mm1, toMint, sqthPriceLimit, true, block.timestamp, 0, 1, 0x00, 0x00); + orders.push(order); + p.orders = orders; + + vm.prank(mm1); + weth.approve(address(netting), 1e30); + + p.clearingPrice = (sqthPriceLimit * 94) / 100; + p.ethUSDFee = 500; + p.flashDepositFee = 3000; + p.ethToFlashDeposit = (p.ethToFlashDeposit * 1) / 10 ** 7; + + vm.expectRevert(bytes("Price too low relative to Uniswap twap.")); + netting.depositAuction(p); + } + + function testFirstDepositAuction() public { + DepositAuctionParams memory p; + // get the usd to deposit remaining + p.depositsQueued = netting.depositsQueued(); + // find the eth value of it + p.minEth = (_convertUSDToETH(p.depositsQueued) * 9975) / 10000; + + // lets get the uniswap price, you can get this from uniswap function in crabstratgegy itself + uint256 sqthPrice = (_getSqthPrice(1e18) * 988) / 1000; + // get the vault details + (,, uint256 collateral, uint256 debt) = crab.getVaultDetails(); + // get the total deposit + uint256 toMint; + (p.totalDeposit, toMint) = _findTotalDepositAndToMint(p.minEth, collateral, debt, sqthPrice); + // -------- + // then write a test suite with a high eth value where it fails + bool trade_works = _isEnough(p.minEth, toMint, sqthPrice, p.totalDeposit); + require(trade_works, "depositing more than we have from sellling"); + + // if i sell the sqth and get eth add to user eth, will it be > total deposit + + // then reduce the total value to get more trade value like in crab otc looping + // find out the root cause of this rounding issue + + // turns out the issue did not occur, + // so we go ahead as though the auction closed for 0.993 osqth price + + Order memory order = + Order(0, mm1, toMint - 1e18, (sqthPrice * 1005) / 1000, true, block.timestamp, 0, 1, 0x00, 0x00); + + Sign memory s; + (s.v, s.r, s.s) = vm.sign(mm1Pk, sig.getTypedDataHash(order)); + order.v = s.v; + order.r = s.r; + order.s = s.s; + + Order memory order0 = Order(0, mm1, 1e18, (sqthPrice * 1005) / 1000, true, block.timestamp, 1, 1, 0x00, 0x00); + + Sign memory s0; + (s0.v, s0.r, s0.s) = vm.sign(mm1Pk, sig.getTypedDataHash(order0)); + order0.v = s0.v; + order0.r = s0.r; + order0.s = s0.s; + + orders.push(order0); + orders.push(order); + vm.prank(mm1); + weth.approve(address(netting), 1e30); + + p.orders = orders; + p.clearingPrice = (sqthPrice * 1005) / 1000; + uint256 excessEth = (toMint * (p.clearingPrice - sqthPrice)) / 1e18; + console.log(excessEth, "excess eth is"); + + console.log(ICrabStrategyV2(crab).balanceOf(depositor), "balance start crab"); + + // Find the borrow ration for toFlash + uint256 mid = _findBorrow(excessEth, debt, collateral); + console.log(mid, "borrow percentage is"); + p.ethToFlashDeposit = (excessEth * mid) / 10 ** 7; + console.log("after multiplying", p.ethToFlashDeposit); + p.ethUSDFee = 500; + p.flashDepositFee = 3000; + // ------------- // + console.log(p.depositsQueued, p.minEth, p.totalDeposit, toMint); + console.log(p.clearingPrice); + uint256 initEthBalance = weth.balanceOf(depositor); + netting.depositAuction(p); + + assertApproxEqAbs(ICrabStrategyV2(crab).balanceOf(depositor), 147e18, 1e18); + assertEq(sqth.balanceOf(mm1), toMint); + assertApproxEqAbs(weth.balanceOf(depositor) - initEthBalance, 3e17, 1e17); + } + + // TODO find a way to make this reusable and test easily + // for multiple ETH movements and external events like partial fills + // eth going down + function testDepositAuctionEthUp() public { + DepositAuctionParams memory p; + // get the usd to deposit remaining + p.depositsQueued = netting.depositsQueued(); + // find the eth value of it + p.minEth = _convertUSDToETH(p.depositsQueued); + console.log("Starting ETH", p.minEth / 10 ** 18); + + // lets get the uniswap price, you can get this from uniswap function in crabstratgegy itself + uint256 sqthPrice = (_getSqthPrice(1e18) * 988) / 1000; + // get the vault details + (,, uint256 collateral, uint256 debt) = crab.getVaultDetails(); + // get the total deposit + uint256 toMint; + (p.totalDeposit, toMint) = _findTotalDepositAndToMint(p.minEth, collateral, debt, sqthPrice); + console.log("Auctioning for ", toMint / 10 ** 18, "sqth"); + // -------- + // then write a test suite with a high eth value where it fails + require(_isEnough(p.minEth, toMint, sqthPrice, p.totalDeposit), "depositing more than we have from sellling"); + + // if i sell the sqth and get eth add to user eth, will it be > total deposit + + // then reduce the total value to get more trade value like in crab otc looping + // find out the root cause of this rounding issue + + // turns out the issue did not occur, + // so we go ahead as though the auction closed for 0.993 osqth price + + Order memory order = Order( + 0, + mm1, + toMint, + 63974748984830990, // sqth price in the future + true, + block.timestamp + 26000000, + 0, + 1, + 0x00, + 0x00 + ); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(mm1Pk, sig.getTypedDataHash(order)); + order.v = v; + order.r = r; + order.s = s; + orders.push(order); + vm.prank(mm1); + weth.approve(address(netting), 1e30); + + p.orders = orders; + p.clearingPrice = (sqthPrice * 1005) / 1000; + uint256 excessEth = (toMint * (p.clearingPrice - sqthPrice)) / 1e18; + + console.log(ICrabStrategyV2(crab).balanceOf(depositor), "balance start crab"); + + // Find the borrow ration for toFlash + uint256 mid = _findBorrow(excessEth, debt, collateral); + p.ethToFlashDeposit = (excessEth * mid) / 10 ** 7; + p.ethUSDFee = 500; + p.flashDepositFee = 3000; + // ------------- // + + vm.stopPrank(); + assertEq(activeFork, vm.activeFork()); + vm.makePersistent(address(netting)); + vm.makePersistent(address(weth)); + vm.makePersistent(address(usdc)); + + vm.rollFork(activeFork, 15829113); + console.log(address(depositor).balance, "starting"); + p.minEth = _convertUSDToETH(p.depositsQueued); + p.clearingPrice = _getSqthPrice(1e18); + console.log("Ending ETH", p.minEth / 10 ** 18); + (,, collateral, debt) = ICrabStrategyV2(crab).getVaultDetails(); + p.totalDeposit = _findTotalDepositFromAuctioned(collateral, debt, toMint); + console.log("Using only", toMint, "sqth"); + console.log(p.totalDeposit); + + uint256 mm1Balance = weth.balanceOf(mm1); + uint256 initDepositorBalance = weth.balanceOf(depositor); + netting.depositAuction(p); + assertLe(((toMint * p.clearingPrice) / 10 ** 18) - (mm1Balance - weth.balanceOf(mm1)), 180); + + assertApproxEqAbs(ICrabStrategyV2(crab).balanceOf(depositor), 147e18, 1e18); + assertApproxEqAbs( + sqth.balanceOf(mm1), toMint, 0.001e18, "All minted not sold, check if we sold only what we took for" + ); + assertApproxEqAbs( + weth.balanceOf(depositor) - initDepositorBalance, 23e17, 1e17, "deposit not refunded enough eth" + ); + } + + function _findBorrow(uint256 toFlash, uint256 debt, uint256 collateral) internal returns (uint256) { + // we want a precision of six decimals + // TODo fix the inifinte loop + uint8 decimals = 6; + + uint256 start = 5 * 10 ** decimals; + uint256 end = 30 * 10 ** decimals; + uint256 mid; + uint256 ethToBorrow; + uint256 totDep; + uint256 debtMinted; + uint256 ethReceived; + while (true) { + mid = (start + end) / 2; + ethToBorrow = (toFlash * mid) / 10 ** (decimals + 1); + totDep = toFlash + ethToBorrow; + debtMinted = (totDep * debt) / collateral; + + // get quote for debt minted and check if eth value is > borrowed but within deviation + // if eth value is lesser, then we borrow less so end = mid; else start = mid + ethReceived = _getSqthPrice(debtMinted); + if (ethReceived >= ethToBorrow && ethReceived <= (ethToBorrow * 10100) / 10000) { + break; + } + // mid is the multiple + else { + if (ethReceived > ethToBorrow) { + start = mid; + } else { + end = mid; + } + } + } + // why is all the eth not being take in + return mid + 1e7; + } + + function _isEnough(uint256 _userETh, uint256 oSqthQuantity, uint256 oSqthPrice, uint256 _totalDep) + internal + pure + returns (bool) + { + uint256 totalAfterSelling = (_userETh + ((oSqthQuantity * oSqthPrice)) / 1e18); + return totalAfterSelling > _totalDep; + } + + function _convertUSDToETH(uint256 _usdc) internal returns (uint256) { + // get the uniswap quoter contract code and address and initiate it + return quoter.quoteExactInputSingle( + address(usdc), + address(weth), + 500, //3000 is 0.3 + _usdc, + 0 + ); + } + + function _getSqthPrice(uint256 _quantity) internal returns (uint256) { + return quoter.quoteExactInputSingle(address(sqth), address(weth), 3000, _quantity, 0); + } +} diff --git a/packages/crab-netting/test/ForkTestNetAtPrice.sol b/packages/crab-netting/test/ForkTestNetAtPrice.sol new file mode 100644 index 000000000..2e30241dd --- /dev/null +++ b/packages/crab-netting/test/ForkTestNetAtPrice.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import "forge-std/Test.sol"; +import {BaseForkSetup} from "./BaseForkSetup.t.sol"; + +contract ForkTestNetAtPrice is BaseForkSetup { + function setUp() public override { + BaseForkSetup.setUp(); + // this is a crab whale, get some crab token from + vm.prank(0x06CECFbac34101aE41C88EbC2450f8602b3d164b); + crab.transfer(withdrawer, 1e18); + + // some WETH and USDC rich address + vm.prank(0x57757E3D981446D585Af0D9Ae4d7DF6D64647806); + usdc.transfer(depositor, 20e6); + } + + function testForkTestNetAtPrice() public { + vm.startPrank(depositor); + usdc.approve(address(netting), 17e6); + netting.depositUSDC(17e6); + vm.stopPrank(); + + vm.startPrank(withdrawer); + crab.approve(address(netting), 1e18); + netting.queueCrabForWithdrawal(1e18); + vm.stopPrank(); + + assertEq(usdc.balanceOf(withdrawer), 0); + assertEq(crab.balanceOf(depositor), 0); + uint256 priceToNet = 1336290000; + uint256 quantityToNet = 16840842; + netting.netAtPrice(priceToNet, quantityToNet); // $1336.29 per crab and nets $16.84 + assertApproxEqAbs(usdc.balanceOf(withdrawer), quantityToNet, 1); // withdrawer gets that amount + uint256 crabReceived = (quantityToNet * 1e18) / priceToNet; + assertEq(crab.balanceOf(depositor), crabReceived); // depositor gets 0.01265755 crab + assertEq(netting.crabBalance(withdrawer), 1e18 - crabReceived); // ensure crab remains + } +} diff --git a/packages/crab-netting/test/Netting.t.sol b/packages/crab-netting/test/Netting.t.sol new file mode 100644 index 000000000..baf8ef693 --- /dev/null +++ b/packages/crab-netting/test/Netting.t.sol @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import {BaseForkSetup} from "./BaseForkSetup.t.sol"; + +contract NettingTest is BaseForkSetup { + function setUp() public override { + BaseForkSetup.setUp(); // gives you netting, depositor, withdrawer, usdc, crab + vm.startPrank(0x57757E3D981446D585Af0D9Ae4d7DF6D64647806); + usdc.transfer(depositor, 400e6); + vm.stopPrank(); + + vm.prank(0x06CECFbac34101aE41C88EbC2450f8602b3d164b); + crab.transfer(withdrawer, 40e18); + + vm.startPrank(depositor); // makes some USDC deposits + usdc.approve(address(netting), 280 * 1e6); + netting.depositUSDC(20 * 1e6); + netting.depositUSDC(100 * 1e6); + netting.depositUSDC(80 * 1e6); + assertEq(netting.usdBalance(depositor), 200e6); + vm.stopPrank(); + + vm.startPrank(withdrawer); // queue some crab + crab.approve(address(netting), 200 * 1e18); + netting.queueCrabForWithdrawal(5 * 1e18); + netting.queueCrabForWithdrawal(4 * 1e18); + netting.queueCrabForWithdrawal(11 * 1e18); + assertEq(netting.crabBalance(withdrawer), 20e18); + vm.stopPrank(); + + // withdrawer has 20 queued and depositor 200 + } + + function testNettingAmountEqlsDeposit() public { + uint256 price = 1330e6; + uint256 quantity = 20e6; + netting.netAtPrice(price, quantity); + assertEq(netting.usdBalance(depositor), 180e6); + uint256 crabReceived = ((quantity * 1e18) / price); + assertEq(crab.balanceOf(depositor), crabReceived); + } + + function testNettingAmountEqlsZero() public { + uint256 price = 1330e6; + uint256 quantity = 0; + netting.netAtPrice(price, quantity); + assertEq(netting.usdBalance(depositor), 200e6); + } + + function testNettingAmountGreaterThanBalance() public { + uint256 price = 1330e6; + uint256 quantity = 30e10; + vm.expectRevert(); + netting.netAtPrice(price, quantity); + } + + function testNetting() public { + // TODO turn this into a fuzzing test + assertEq(usdc.balanceOf(withdrawer), 0, "starting balance"); + assertEq(crab.balanceOf(depositor), 0, "depositor got their crab"); + uint256 price = 1330e6; + uint256 quantity = 100e6; + netting.netAtPrice(price, quantity); // net for 100 USD where 1 crab is 10 USD, so 10 crab + assertApproxEqAbs(usdc.balanceOf(withdrawer), quantity, 1, "withdrawer did not get their usdc"); + assertEq(crab.balanceOf(depositor), (quantity * 1e18) / price, "depositor did not get their crab"); + } + + function testNettingWithMultipleDeposits() public { + assertEq(usdc.balanceOf(withdrawer), 0, "withdrawer starting balance"); + assertEq(crab.balanceOf(depositor), 0, "depositor starting balance"); + uint256 price = 1330e6; + uint256 quantity = 200e6; + netting.netAtPrice(price, quantity); // net for 200 USD where 1 crab is 10 USD, so 20 crab + assertApproxEqAbs(usdc.balanceOf(withdrawer), quantity, 1, "withdrawer did not get their usdc"); + assertEq(crab.balanceOf(depositor), (quantity * 1e18) / price, "depositor did not get their crab"); + } + + function testNettingWithPartialReceipt() public { + assertEq(usdc.balanceOf(withdrawer), 0, "withdrawer starting balance"); + assertEq(crab.balanceOf(depositor), 0, "depositor starting balance"); + uint256 price = 1330e6; + uint256 quantity = 30e6; + netting.netAtPrice(price, quantity); // 20 from first desposit and 10 from second (partial) + assertEq(netting.depositsQueued(), 170e6, "receipts were not updated correctly"); + netting.netAtPrice(price, 170e6); + assertEq(crab.balanceOf(depositor), (200e6 * 1e18) / price, "depositor got their crab"); + } + + function testNettingAfterWithdraw() public { + assertEq(usdc.balanceOf(withdrawer), 0, "withdrawer starting balance"); + assertEq(crab.balanceOf(depositor), 0, "depositor starting balance"); + vm.prank(depositor); + uint256 withdrawQuantity = 50e6; + netting.withdrawUSDC(withdrawQuantity); + uint256 price = 1330e6; + uint256 quantity = 200e6 - withdrawQuantity; + netting.netAtPrice(price, quantity); + assertEq(crab.balanceOf(depositor), (quantity * 1e18) / price, "depositor got their crab"); + } + + function testNettingAfterARun() public { + uint256 price = 1330e6; + uint256 quantity = 200e6; + vm.startPrank(depositor); + netting.withdrawUSDC(80e6); + netting.depositUSDC(80e6); + vm.stopPrank(); + + vm.prank(withdrawer); + netting.dequeueCrab(20e18 - (quantity * 1e18) / price); + netting.netAtPrice(price, 200e6); // net for 100 USD where 1 crab is 10 USD, so 10 crab + assertEq(netting.crabBalance(withdrawer), 0, "crab balance not zero after first netting"); + + // queue more + vm.startPrank(depositor); + usdc.approve(address(netting), 200 * 1e6); + netting.depositUSDC(20 * 1e6); + netting.depositUSDC(100 * 1e6); + netting.depositUSDC(80 * 1e6); + assertEq(netting.usdBalance(depositor), 200e6, "usd balance not reflecting correctly"); + vm.stopPrank(); + + console.log("no issues till here 2"); + vm.startPrank(withdrawer); + crab.approve(address(netting), 200 * 1e18); + netting.queueCrabForWithdrawal(5 * 1e18); + netting.queueCrabForWithdrawal(4 * 1e18); + netting.queueCrabForWithdrawal(11 * 1e18); + assertEq(netting.crabBalance(withdrawer), 20e18, "crab balance not reflecting correctly"); + vm.stopPrank(); + + console.log("no issues till here 3"); + netting.netAtPrice(price, 200e6); // net for 100 USD where 1 crab is 10 USD, so 10 crab + console.log("no issues till here 4"); + assertApproxEqAbs(usdc.balanceOf(withdrawer), 400e6, 2, "witadrawer got their usdc"); + assertEq(crab.balanceOf(depositor), (400e6 * 1e18) / price, "depositor got their crab"); + } + + function testCannotWithdrawMoreThanDeposited() public { + vm.startPrank(depositor); + vm.expectRevert(stdError.arithmeticError); + netting.withdrawUSDC(210e6); + vm.stopPrank(); + } +} diff --git a/packages/crab-netting/test/PriceChecks.sol b/packages/crab-netting/test/PriceChecks.sol new file mode 100644 index 000000000..504b74773 --- /dev/null +++ b/packages/crab-netting/test/PriceChecks.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import {BaseForkSetup} from "./BaseForkSetup.t.sol"; + +contract PriceChecks is BaseForkSetup { + uint256 crabsToWithdraw = 40e18; + uint256 price = 1269e6; // 1335 bounds are 1267 and 1401 + uint256 totalUSDCRequired = (crabsToWithdraw * price) / 1e18; + + function setUp() public override { + BaseForkSetup.setUp(); // gives you netting, depositor, withdrawer, usdc, crab + vm.prank(0x57757E3D981446D585Af0D9Ae4d7DF6D64647806); + usdc.transfer(depositor, totalUSDCRequired); + + vm.prank(0x06CECFbac34101aE41C88EbC2450f8602b3d164b); + crab.transfer(withdrawer, crabsToWithdraw); + + // make multiple deposits from depositor + vm.startPrank(depositor); + usdc.approve(address(netting), totalUSDCRequired); + netting.depositUSDC(totalUSDCRequired); + vm.stopPrank(); + + // queue multiple crabs from withdrawer + vm.startPrank(withdrawer); + crab.approve(address(netting), crabsToWithdraw); + netting.queueCrabForWithdrawal(crabsToWithdraw); + vm.stopPrank(); + } + + function testCrabPriceHigh() public { + console.log("expecting a high crdab price"); + vm.expectRevert(bytes("Crab Price too high")); + netting.netAtPrice(1500e6, totalUSDCRequired / 2); + } + + function testCrabPriceLow() public { + vm.expectRevert(bytes("Crab Price too low")); + netting.netAtPrice(1100e6, totalUSDCRequired / 2); + } +} diff --git a/packages/crab-netting/test/QueuedBalances.t.sol b/packages/crab-netting/test/QueuedBalances.t.sol new file mode 100644 index 000000000..c95059d70 --- /dev/null +++ b/packages/crab-netting/test/QueuedBalances.t.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {BaseForkSetup} from "./BaseForkSetup.t.sol"; + +contract QueuedBalancesTest is BaseForkSetup { + uint256 crabsToWithdraw = 40e18; + uint256 price = 1279e6; // 1338 bounds are 1271 and 1404 + uint256 totalUSDCRequired = (crabsToWithdraw * price) / 1e18; + + function setUp() public override { + BaseForkSetup.setUp(); // gives you netting, depositor, withdrawer, usdc, crab + vm.startPrank(0x57757E3D981446D585Af0D9Ae4d7DF6D64647806); + usdc.transfer(depositor, totalUSDCRequired); + vm.stopPrank(); + + vm.prank(0x06CECFbac34101aE41C88EbC2450f8602b3d164b); + crab.transfer(withdrawer, crabsToWithdraw); + + // make multiple deposits from depositor + vm.startPrank(depositor); + usdc.approve(address(netting), totalUSDCRequired); + netting.depositUSDC((totalUSDCRequired * 10) / 100); + netting.depositUSDC((totalUSDCRequired * 50) / 100); + netting.depositUSDC((totalUSDCRequired * 40) / 100); + assertEq(netting.usdBalance(depositor), totalUSDCRequired); + vm.stopPrank(); + + // queue multiple crabs from withdrawer + vm.startPrank(withdrawer); + crab.approve(address(netting), crabsToWithdraw); + netting.queueCrabForWithdrawal((crabsToWithdraw * 25) / 100); + netting.queueCrabForWithdrawal((crabsToWithdraw * 20) / 100); + netting.queueCrabForWithdrawal((crabsToWithdraw * 55) / 100); + assertEq(netting.crabBalance(withdrawer), crabsToWithdraw); + vm.stopPrank(); + + netting.netAtPrice(price, totalUSDCRequired / 2); // net for 100 USD where 1 crab is 10 USD, so 10 crab + } + + function testcrabBalanceQueued() public { + assertEq(netting.depositsQueued(), totalUSDCRequired / 2); + } + + function testWithdrawsQueued() public { + assertEq(netting.withdrawsQueued(), crabsToWithdraw / 2); + } +} diff --git a/packages/crab-netting/test/WithdrawAuction.t.sol b/packages/crab-netting/test/WithdrawAuction.t.sol new file mode 100644 index 000000000..c15a9323e --- /dev/null +++ b/packages/crab-netting/test/WithdrawAuction.t.sol @@ -0,0 +1,221 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import "forge-std/Test.sol"; + +import {Order, WithdrawAuctionParams} from "../src/CrabNetting.sol"; +import {ICrabStrategyV2} from "../src/interfaces/ICrabStrategyV2.sol"; + +import {UniswapQuote} from "./utils/UniswapQuote.sol"; +import {BaseForkSetup} from "./BaseForkSetup.t.sol"; + +import {ERC20} from "openzeppelin/token/ERC20/ERC20.sol"; +import {IWETH} from "../src/interfaces/IWETH.sol"; + +import {SigUtils} from "./utils/SigUtils.sol"; + +struct TimeBalances { + uint256 start; + uint256 end; +} + +struct Portion { + uint256 collateral; + uint256 debt; +} + +struct Sign { + uint8 v; + bytes32 r; + bytes32 s; +} + +contract TestWithdrawAuction is BaseForkSetup { + SigUtils sig; + + function setUp() public override { + BaseForkSetup.setUp(); + sig = new SigUtils(netting.DOMAIN_SEPARATOR()); + + // this is a crab whale, get some crab token from + vm.prank(0x06CECFbac34101aE41C88EbC2450f8602b3d164b); + crab.transfer(withdrawer, 20e18); + + // send sqth to market makers todo + vm.startPrank(0x56178a0d5F301bAf6CF3e1Cd53d9863437345Bf9); + sqth.transfer(mm1, 1000e18); + sqth.transfer(mm2, 1000e18); + vm.stopPrank(); + + // deposit crab for withdrawing + vm.startPrank(withdrawer); + crab.approve(address(netting), 19 * 1e18); + netting.queueCrabForWithdrawal(2 * 1e18); + netting.queueCrabForWithdrawal(3 * 1e18); + netting.queueCrabForWithdrawal(6 * 1e18); + vm.stopPrank(); + // 11 crabs queued for withdrawal + } + + function testWithdrawAuction() public { + WithdrawAuctionParams memory params; + // find the sqth to buy to make the trade + params.crabToWithdraw = 10e18; + uint256 sqthToBuy = crab.getWsqueethFromCrabAmount(params.crabToWithdraw); + UniswapQuote quote = new UniswapQuote(); + uint256 sqthPrice = quote.getSqthPrice(1e18); + params.clearingPrice = (sqthPrice * 1001) / 1000; + + // get the orders for that sqth + vm.prank(mm1); + sqth.approve(address(netting), 1000000e18); + + Order memory order0 = Order(0, mm1, 1e18, params.clearingPrice, false, block.timestamp, 2, 1, 0x00, 0x00); + Sign memory s0; + (s0.v, s0.r, s0.s) = vm.sign(mm1Pk, sig.getTypedDataHash(order0)); + order0.v = s0.v; + order0.r = s0.r; + order0.s = s0.s; + orders.push(order0); + + Order memory order = + Order(0, mm1, sqthToBuy - 1e18, params.clearingPrice, false, block.timestamp, 0, 1, 0x00, 0x00); + Sign memory s1; + (s1.v, s1.r, s1.s) = vm.sign(mm1Pk, sig.getTypedDataHash(order)); + order.v = s1.v; + order.r = s1.r; + order.s = s1.s; + orders.push(order); + params.orders = orders; + + // find the minUSDC to receive + // get col and wsqth from crab amount, find the equity value in eth + (,, uint256 collateral,) = crab.getVaultDetails(); + Portion memory p; + p.collateral = (params.crabToWithdraw * collateral) / crab.totalSupply(); + p.debt = crab.getWsqueethFromCrabAmount(params.crabToWithdraw); + uint256 equityInEth = p.collateral - (p.debt * params.clearingPrice) / 1e18; + + params.minUSDC = (quote.convertWETHToUSDC(equityInEth) * 999) / 1000; + params.ethUSDFee = 500; + // get equivalent usdc quote with slippage and send + + // call withdrawAuction on netting contract + TimeBalances memory timeUSDC; + TimeBalances memory timeWETH; + timeUSDC.start = ERC20(usdc).balanceOf(withdrawer); + timeWETH.start = IWETH(weth).balanceOf(mm1); + + netting.withdrawAuction(params); + + timeUSDC.end = ERC20(usdc).balanceOf(withdrawer); + timeWETH.end = IWETH(weth).balanceOf(mm1); + assertGe(timeUSDC.end - timeUSDC.start, params.minUSDC); + assertGe(timeWETH.end - timeWETH.start, (sqthToBuy * sqthPrice) / 1e18); + + // and eth recevied for mm + assertEq(address(netting).balance, 0); + assertEq(ERC20(sqth).balanceOf(address(netting)), 0, "sqth balance"); + assertLe(ERC20(usdc).balanceOf(address(netting)), 1, "usdc balance"); + assertEq(ICrabStrategyV2(crab).balanceOf(address(netting)), 1e18, "crab balance"); + assertEq(netting.crabBalance(address(withdrawer)), 11e18 - params.crabToWithdraw); + assertEq(IWETH(weth).balanceOf(address(netting)), 0, "weth balance"); + } + + function testWithdrawAuctionAfterFullWithdraw() public { + vm.startPrank(withdrawer); + netting.dequeueCrab(6e18); + netting.queueCrabForWithdrawal(6e18); + vm.stopPrank(); + + WithdrawAuctionParams memory params; + // find the sqth to buy to make the trade + params.crabToWithdraw = 10e18; + uint256 sqthToBuy = crab.getWsqueethFromCrabAmount(params.crabToWithdraw); + UniswapQuote quote = new UniswapQuote(); + uint256 sqthPrice = quote.getSqthPrice(1e18); + params.clearingPrice = (sqthPrice * 1001) / 1000; + + // get the orders for that sqth + vm.prank(mm1); + sqth.approve(address(netting), 1000000e18); + + Order memory order0 = Order(0, mm1, 1e18, params.clearingPrice, false, block.timestamp, 2, 1, 0x00, 0x00); + Sign memory s0; + (s0.v, s0.r, s0.s) = vm.sign(mm1Pk, sig.getTypedDataHash(order0)); + order0.v = s0.v; + order0.r = s0.r; + order0.s = s0.s; + orders.push(order0); + + Order memory order = + Order(0, mm1, sqthToBuy - 1e18, params.clearingPrice, false, block.timestamp, 0, 1, 0x00, 0x00); + Sign memory s1; + (s1.v, s1.r, s1.s) = vm.sign(mm1Pk, sig.getTypedDataHash(order)); + order.v = s1.v; + order.r = s1.r; + order.s = s1.s; + orders.push(order); + params.orders = orders; + + // find the minUSDC to receive + // get col and wsqth from crab amount, find the equity value in eth + (,, uint256 collateral,) = crab.getVaultDetails(); + Portion memory p; + p.collateral = (params.crabToWithdraw * collateral) / crab.totalSupply(); + p.debt = crab.getWsqueethFromCrabAmount(params.crabToWithdraw); + uint256 equityInEth = p.collateral - (p.debt * params.clearingPrice) / 1e18; + + params.minUSDC = (quote.convertWETHToUSDC(equityInEth) * 999) / 1000; + params.ethUSDFee = 500; + // get equivalent usdc quote with slippage and send + + // call withdrawAuction on netting contract + TimeBalances memory timeUSDC; + TimeBalances memory timeWETH; + timeUSDC.start = ERC20(usdc).balanceOf(withdrawer); + timeWETH.start = IWETH(weth).balanceOf(mm1); + + netting.withdrawAuction(params); + + timeUSDC.end = ERC20(usdc).balanceOf(withdrawer); + timeWETH.end = IWETH(weth).balanceOf(mm1); + assertGe(timeUSDC.end - timeUSDC.start, params.minUSDC); + assertGe(timeWETH.end - timeWETH.start, (sqthToBuy * sqthPrice) / 1e18); + + // and eth recevied for mm + assertEq(address(netting).balance, 0); + assertEq(ERC20(sqth).balanceOf(address(netting)), 0, "sqth balance"); + assertLe(ERC20(usdc).balanceOf(address(netting)), 1, "usdc balance"); + assertEq(ICrabStrategyV2(crab).balanceOf(address(netting)), 1e18, "crab balance"); + assertEq(netting.crabBalance(address(withdrawer)), 11e18 - params.crabToWithdraw); + assertEq(IWETH(weth).balanceOf(address(netting)), 0, "weth balance"); + } + + function testSqthPriceAboveThreshold() public { + WithdrawAuctionParams memory params; + // find the sqth to buy to make the trade + params.crabToWithdraw = 10e18; + uint256 sqthToBuy = 1e6; + UniswapQuote quote = new UniswapQuote(); + uint256 sqthPrice = quote.getSqthPrice(1e18); + params.clearingPrice = (sqthPrice * 106) / 100; + + // get the orders for that sqth + + Order memory order = Order(0, mm1, sqthToBuy, params.clearingPrice, false, block.timestamp, 0, 1, 0x00, 0x00); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(mm1Pk, sig.getTypedDataHash(order)); + order.v = v; + order.r = r; + order.s = s; + orders.push(order); + params.orders = orders; + + params.minUSDC = 1e6; + params.ethUSDFee = 500; + // get equivalent usdc quote with slippage and send + + vm.expectRevert(bytes("Price too high relative to Uniswap twap.")); + netting.withdrawAuction(params); + } +} diff --git a/packages/crab-netting/test/utils/SigUtils.sol b/packages/crab-netting/test/utils/SigUtils.sol new file mode 100644 index 000000000..c9085048a --- /dev/null +++ b/packages/crab-netting/test/utils/SigUtils.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import {console} from "forge-std/console.sol"; + +import {Order} from "../../src/CrabNetting.sol"; + +contract SigUtils { + bytes32 internal DOMAIN_SEPARATOR; + + constructor(bytes32 _DOMAIN_SEPARATOR) { + DOMAIN_SEPARATOR = _DOMAIN_SEPARATOR; + } + + // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + bytes32 public constant _CRAB_NETTING_TYPEHASH = keccak256( + "Order(uint256 bidId,address trader,uint256 quantity,uint256 price,bool isBuying,uint256 expiry,uint256 nonce)" + ); + + // computes the hash of a permit + function getStructHash(Order memory _order) internal pure returns (bytes32) { + return keccak256( + abi.encode( + _CRAB_NETTING_TYPEHASH, + _order.bidId, + _order.trader, + _order.quantity, + _order.price, + _order.isBuying, + _order.expiry, + _order.nonce + ) + ); + } + + // computes the hash of the fully encoded EIP-712 message for the domain, which can be used to recover the signer + function getTypedDataHash(Order memory _order) public view returns (bytes32) { + return keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, getStructHash(_order))); + } +} diff --git a/packages/crab-netting/test/utils/UniswapQuote.sol b/packages/crab-netting/test/utils/UniswapQuote.sol new file mode 100644 index 000000000..59c1e42f8 --- /dev/null +++ b/packages/crab-netting/test/utils/UniswapQuote.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import {IQuoter} from "@uniswap/v3-periphery/contracts/interfaces/IQuoter.sol"; + +contract UniswapQuote { + address usdc = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + address sqth = 0xf1B99e3E573A1a9C5E6B2Ce818b617F0E664E86B; + address weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + IQuoter public immutable quoter = IQuoter(0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6); + + function convertUSDToETH(uint256 _usdc) external returns (uint256) { + // get the uniswap quoter contract code and address and initiate it + return quoter.quoteExactInputSingle( + (usdc), + (weth), + 500, //3000 is 0.3 + _usdc, + 0 + ); + } + + function convertWETHToUSDC(uint256 _weth) external returns (uint256) { + // get the uniswap quoter contract code and address and initiate it + return quoter.quoteExactInputSingle( + (weth), + (usdc), + 500, //3000 is 0.3 + _weth, + 0 + ); + } + + function getSqthPrice(uint256 _quantity) external returns (uint256) { + return quoter.quoteExactInputSingle((sqth), (weth), 3000, _quantity, 0); + } +}