diff --git a/contracts/governance/PausableGuardian_0_8.sol b/contracts/governance/PausableGuardian_0_8.sol index c49dea41..cae9a036 100644 --- a/contracts/governance/PausableGuardian_0_8.sol +++ b/contracts/governance/PausableGuardian_0_8.sol @@ -59,4 +59,4 @@ contract PausableGuardian_0_8 is Ownable { guardian := sload(Pausable_GuardianAddress) } } -} +} \ No newline at end of file diff --git a/contracts/interfaces/IWeth.sol b/contracts/interfaces/IWeth.sol index c2061dbf..548cc84f 100644 --- a/contracts/interfaces/IWeth.sol +++ b/contracts/interfaces/IWeth.sol @@ -3,7 +3,7 @@ * Licensed under the Apache License, Version 2.0. */ -pragma solidity >=0.5.0 <0.6.0; +pragma solidity >=0.5.0 <0.9.0; interface IWeth { diff --git a/contracts/orderbook/Events/OrderBookEvents.sol b/contracts/orderbook/Events/OrderBookEvents.sol new file mode 100644 index 00000000..f0b38c40 --- /dev/null +++ b/contracts/orderbook/Events/OrderBookEvents.sol @@ -0,0 +1,23 @@ +pragma solidity ^0.8.0; +import "../IOrderBook.sol"; + +contract OrderBookEvents { + event OrderCancelled(address indexed trader, bytes32 orderID); + event OrderPlaced( + address indexed trader, + IOrderBook.OrderType indexed OrderType, + uint256 indexed execPrice, + bytes32 orderID, + address collateralTokenAddress, + address loanTokenAddress + ); + event OrderExecuted(address indexed trader, bytes32 orderID); + event OrderAmended( + address indexed trader, + IOrderBook.OrderType indexed OrderType, + uint256 indexed execPrice, + bytes32 orderID, + address collateralTokenAddress, + address loanTokenAddress + ); +} \ No newline at end of file diff --git a/contracts/orderbook/IOrderBook.sol b/contracts/orderbook/IOrderBook.sol new file mode 100644 index 00000000..3a0894d5 --- /dev/null +++ b/contracts/orderbook/IOrderBook.sol @@ -0,0 +1,228 @@ +pragma solidity ^0.8.0; + +interface IOrderBook { + enum OrderType { + LIMIT_OPEN, + LIMIT_CLOSE, + MARKET_STOP + } + + enum OrderStatus { + ACTIVE, + CANCELLED, + EXECUTED + } + + /* + Used values for different order types: + LIMIT_OPEN: + loanID + orderID + amountReceived + leverage + loanTokenAmount + collateralTokenAmount + trader + iToken + loanTokenAddress + base + orderType + status + timeTillExpiration + loanDataBytes + LIMIT_CLOSE and MARKET_STOP: + loanID + orderID + amountReceived + loanTokenAmount + collateralTokenAmount + trader + iToken + loanTokenAddress + base + orderType + status + timeTillExpiration + loanDataBytes + */ + struct Order { + bytes32 loanID; //ID of the loan on OOKI protocol + bytes32 orderID; //order ID + uint256 amountReceived; //amount received from the trade executing. Denominated in base for limit open and loanTokenAddress for limit close and market stop + uint256 leverage; //leverage amount + uint256 loanTokenAmount; //loan token amount denominated in loanTokenAddress + uint256 collateralTokenAmount; //collateral token amount denominated in base + address trader; //trader placing order + address iToken; //iToken being interacted with + address loanTokenAddress; //loan token + address base; //collateral token + OrderType orderType; //order type + OrderStatus status; //order status + uint64 timeTillExpiration; //Time till expiration. Useful for GTD and time-based cancellation + bytes loanDataBytes; //data passed for margin trades + } + + /// Returns proxy owner + /// @return owner Contract owner + function owner() external view returns(address owner); + + /// Returns guardian + /// @return guardian Protocol guardian address + function getGuardian() external view returns(address guardian); + + /// Returns Deposits contract address + /// @return vault Deposits Contract + function VAULT() external view returns(address vault); + + /// Returns Protocol contract address + /// @return protocol ooki protocol contract + function PROTOCOL() external view returns(address protocol); + + /// Returns minimum trade size in USDC + /// @return size USDC amount + function MIN_AMOUNT_IN_USDC() external view returns(uint256 size); + + /// Places new Order + /// @param order Order Struct + function placeOrder(Order calldata order) external; + + /// Amends Order + /// @param order Order Struct + function amendOrder(Order calldata order) external; + + /// Cancels Order + /// @param orderID ID of order to be canceled + function cancelOrder(bytes32 orderID) external; + + /// Cancels Order + /// @param orderID ID of order to be canceled + function cancelOrderProtocol(bytes32 orderID) external returns (uint256); + + /// Force cancels order + /// @param orderID ID of order to be canceled + function cancelOrderGuardian(bytes32 orderID) external; + + /// Changes stop type between index and dex price + /// @param stopType true = index, false = dex price + function changeStopType(bool stopType) external; + + /// Set price feed contract address + /// @param newFeed new price feed contract + function setPriceFeed(address newFeed) external; + + /// Set gas price to be used for incentives (if price feed does not already contain it) + /// @param gasPrice gas price in gwei + function setGasPrice(uint256 gasPrice) external; + + /// Return price feed contract address + /// @return priceFeed Price Feed Contract Address + function priceFeed() external view returns (address priceFeed); + + /// Returns gas price used for incentive calculations + /// @return gasPrice gas price in gwei + function getGasPrice() external view returns (uint256 gasPrice); + + /// Deposit Gas Token to pay out incentives for orders to be executed + /// @param amount when depositing wrapped token, this is amount to be deposited (leave as 0 if sending native token) + function depositGasFeeToken(uint256 amount) external payable; + + /// Withdraw Gas Token (received as native token) + /// @param amount amount to be withdrawn + function withdrawGasFeeToken(uint256 amount) external; + + /// Return amount received through a specified swap + /// @param srcToken source token address + /// @param destToken destination token address + /// @param payload loanDataBytes passed for margin trades + /// @param amountIn amount in for the swap + function getDexRate(address srcToken, address destToken, bytes calldata payload, uint256 amountIn) external returns(uint256); + + /// Checks if order is able to be cleared from books due to failing to meet all requirements + /// @param orderID order ID + function clearOrder(bytes32 orderID) external view returns (bool); + + /// Returns list of orders that are up to be cleared. Used for Chainlink Keepers + /// @param start starting index + /// @param end ending index + /// @return hasOrders true if the payload contains any orders + /// @return payload bytes32[] encoded with the order IDs up for clearing from books + function getClearOrderList(uint start, uint end) external view returns (bool hasOrders, bytes memory payload); + + /// Returns an order ID available for execution. Used for Chainlink Keepers + /// @param start starting index + /// @param end ending index + /// @return ID order ID up for execution. If equal to 0 there is no order ID up for execution in the specified index range + function getExecuteOrder(uint start, uint end) external returns (bytes32 ID); + + /// Checks if order meets requirements for execution + /// @param orderID order ID of order being checked + function prelimCheck(bytes32 orderID) external returns (bool); + + /// Returns oracle rate for a swap + /// @param srcToken source token address + /// @param destToken destination token address + /// @param amount swap amount + function queryRateReturn(address srcToken, address destToken, uint256 amount) external view returns(uint256); + + /// Checks if dex rate is within acceptable bounds from oracle rate + /// @param srcToken source token address + /// @param destToken destination token address + /// @param payload loanDataBytes used for margin trade + function priceCheck(address srcToken, address destToken, bytes calldata payload) external returns(bool); + + /// Executes Order + /// @param orderID order ID + /// @return incentiveAmountReceived amount received in gas token from exeuction of order + function executeOrder(bytes32 orderID) external returns(uint256 incentiveAmountReceived); + + /// sets token allowances + /// @param spenders addresses that will be given allowance + /// @param tokens token addresses + function adjustAllowance(address[] calldata spenders, address[] calldata tokens) external; + + /// revokes token allowances + /// @param spenders addresses that will have allowance revoked + /// @param tokens token addresses + function revokeAllowance(address[] calldata spenders, address[] calldata tokens) external; + + /// Retrieves active orders for a trader + /// @param trader address of trader + function getUserOrders(address trader) external view returns (Order[] memory); + + /// Retrieves active orders for a trader + /// @param trader address of trader + /// @param start starting index + /// @param end ending index + function getUserOrdersLimited(address trader, uint256 start, uint256 end) external view returns (Order[] memory); + + /// Retrieves order corresponding to an order ID + /// @param orderID order ID + function getOrderByOrderID(bytes32 orderID) external view returns (Order memory); + + /// Retrieves active order IDs for a trader + /// @param trader address of trader + function getUserOrderIDs(address trader) external view returns (bytes32[] memory); + + /// Returns total active orders count for a trader + /// @param trader address of trader + function getUserOrdersCount(address trader) external view returns (uint256); + + /// Returns total active orders count + function getGlobalOrdersCount() external view returns (uint256); + + /// Returns total active order IDs + function getGlobalOrderIDs() external view returns (bytes32[] memory); + + /// Returns total active orders + function getGlobalOrders() external view returns (Order[] memory); + + /// Returns active order IDs + /// @param start starting index + /// @param end ending index + function getGlobalOrderIDsLimited(uint256 start, uint256 end) external view returns (bytes32[] memory); + + /// Returns active orders + /// @param start starting index + /// @param end ending index + function getGlobalOrdersLimited(uint256 start, uint256 end) external view returns (Order[] memory); +} diff --git a/contracts/orderbook/Keepers/OrderKeeper.sol b/contracts/orderbook/Keepers/OrderKeeper.sol new file mode 100644 index 00000000..0d744e36 --- /dev/null +++ b/contracts/orderbook/Keepers/OrderKeeper.sol @@ -0,0 +1,43 @@ +pragma solidity ^0.8.0; +import "../IOrderBook.sol"; +import "@openzeppelin-4.7.0/token/ERC20/extensions/IERC20Metadata.sol"; +import "../../governance/PausableGuardian_0_8.sol"; + +contract OrderKeeper is PausableGuardian_0_8 { + address public implementation; + IERC20Metadata public constant WRAPPED_TOKEN = IERC20Metadata(0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270); + IOrderBook public orderBook; + + function checkUpkeep(bytes calldata checkData) + external + returns (bool upkeepNeeded, bytes memory performData) + { + (uint256 start, uint256 end) = abi.decode(checkData, (uint256, uint256)); + uint256 orderIDLength = orderBook.getGlobalOrdersCount(); + if (start > orderIDLength) { + return (upkeepNeeded, performData); + } + if(end > orderIDLength) { + end = orderIDLength; + } + bytes32 orderIDForExec = orderBook + .getExecuteOrder(start, end); + return (orderIDForExec != 0, abi.encode(orderIDForExec)); + } + + function performUpkeep(bytes calldata performData) external pausable { + bytes32 orderId = abi.decode(performData, (bytes32)); + //emit OrderExecuted(trader,orderId); + try orderBook.executeOrder(orderId) { + + } catch(bytes memory){} catch Error (string memory) {} + } + + function setOrderBook(IOrderBook contractAddress) external onlyOwner { + orderBook = contractAddress; + } + + function withdrawIncentivesReceived(address receiver) external onlyOwner { + WRAPPED_TOKEN.transfer(receiver, WRAPPED_TOKEN.balanceOf(address(this))); + } +} diff --git a/contracts/orderbook/Keepers/OrderKeeperClear.sol b/contracts/orderbook/Keepers/OrderKeeperClear.sol new file mode 100644 index 00000000..76cf2a7b --- /dev/null +++ b/contracts/orderbook/Keepers/OrderKeeperClear.sol @@ -0,0 +1,47 @@ +pragma solidity ^0.8.0; +import "../IOrderBook.sol"; +import "@openzeppelin-4.7.0/token/ERC20/extensions/IERC20Metadata.sol"; +import "../../governance/PausableGuardian_0_8.sol"; + +contract OrderKeeperClear is PausableGuardian_0_8 { + address public implementation; + IERC20Metadata public constant WRAPPED_TOKEN = IERC20Metadata(0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270); + IOrderBook public orderBook; + + function checkUpkeep(bytes calldata checkData) + external + view + returns (bool upkeepNeeded, bytes memory performData) + { + (uint256 start, uint256 end) = abi.decode(checkData, (uint256, uint256)); + uint256 orderIDLength = orderBook.getGlobalOrdersCount(); + if (start > orderIDLength) { + return (upkeepNeeded, performData); + } + if(end > orderIDLength) { + end = orderIDLength; + } + return orderBook.getClearOrderList(start, end); + } + + function performUpkeep(bytes calldata performData) external pausable { + bytes32[] memory orderId = abi.decode(performData, (bytes32[])); + //emit OrderExecuted(trader,orderId); + for (uint i;i order.collateralTokenAmount + ? (order.loanTokenAddress, order.loanTokenAmount) + : (order.base, order.collateralTokenAmount); + IERC20(usedToken).transfer(order.trader, amount); + } + } + + function _executeTradeClose( + address trader, + bytes32 loanID, + uint256 amount, + bytes memory loanDataBytes + ) internal { + address(PROTOCOL).call( + abi.encodeWithSelector( + PROTOCOL.closeWithSwap.selector, + loanID, + trader, + amount, + false, + loanDataBytes + ) + ); + } + + function _isActiveLoan(bytes32 ID) internal view returns (bool) { + return PROTOCOL.loans(ID).active; + } + + function getGasPrice() public view returns (uint256) { + return chainGasPrice; + } + + function _gasToSend(uint256 gasUsed) internal view returns (uint256) { + return gasUsed*getGasPrice()*2; + } + + function depositGasFeeToken(uint256 amount) external payable { + require(msg.value==0||amount==0, "cant be both"); + if(msg.value==0){ + IERC20(WRAPPED_TOKEN).safeTransferFrom(msg.sender, address(this), amount); + IWeth(WRAPPED_TOKEN).withdraw(amount); + } + bytes32 orderID = keccak256(abi.encode(msg.sender, 0)); + IDeposits(VAULT).depositGasToken{value:amount}(msg.sender); + } + + function withdrawGasFeeToken(uint256 amount) external { + bytes32 orderID = keccak256(abi.encode(msg.sender, 0)); + require(IDeposits(VAULT).getDeposit(orderID)-amount>(_histOrders[msg.sender].length())*_gasToSend(2500000), "too little gas left"); + IDeposits(VAULT).partialWithdraw(address(this), orderID, amount); + IWeth(WRAPPED_TOKEN).withdraw(amount); + msg.sender.call{value:amount}(""); + } + + function _spendGasFeeToken(uint256 amount, address trader, address receiver) internal returns (uint256) { + bytes32 orderID = keccak256(abi.encode(trader, 0)); + uint256 amountStored = IDeposits(VAULT).getDeposit(orderID); + if (amountStored < amount) { + amount = amountStored; + } + IDeposits(VAULT).partialWithdraw(receiver, orderID, amount); + return amount; + } + + function clearOrder(bytes32 orderID) public view returns (bool) { + IOrderBook.Order memory order = _allOrders[orderID]; + if (order.timeTillExpiration < block.timestamp) { + return true; + } + uint256 amountUsed = order.collateralTokenAmount + + order.loanTokenAmount; + uint256 swapRate; + if (order.orderType == IOrderBook.OrderType.LIMIT_OPEN) { + swapRate = queryRateReturn( + order.loanTokenAddress, + order.base, + amountUsed + ); + } else { + swapRate = queryRateReturn( + order.base, + order.loanTokenAddress, + amountUsed + ); + } + if ( + ( + order.amountReceived > swapRate + ? (order.amountReceived - swapRate) > + (order.amountReceived * 25) / 100 + : (swapRate - order.amountReceived) > (swapRate * 25) / 100 + ) + ) { + return true; + } + return false; + } + + function getClearOrderList(uint start, uint end) external view returns (bool hasOrders, bytes memory payload) { + require(end<=_allOrderIDs.length(), "OrderBook: end is past max orders"); + bytes32[] memory fullList = new bytes32[](7); + uint iter = 0; + bytes32 ID; + for (uint256 i = start; (i < end && iter < 7);) { + ID = _allOrderIDs.at(i); + if (clearOrder(ID)) { + hasOrders = true; + fullList[iter] = ID; + ++iter; + } + unchecked { ++i; } + } + payload = abi.encode(fullList); + } + + function getExecuteOrder(uint start, uint end) external returns (bytes32 ID) { + require(end<=_allOrderIDs.length(), "OrderBook: end is past max orders"); + bytes32 tempID; + for (uint256 i = start; i < end;) { + tempID = _allOrderIDs.at(i); + (bool result, bytes memory returnData) = address(this).call( + abi.encodeWithSelector(this.prelimCheck.selector, tempID) + ); + if (!result) { + unchecked { ++i; } + continue; + } + if (abi.decode(returnData, (bool))) { + return tempID; + } + unchecked { ++i; } + } + } + + function queryRateReturn( + address start, + address end, + uint256 amount + ) public view returns (uint256) { + return IPriceFeeds(priceFeed) + .queryReturn(start, end, amount); + } + + function prelimCheck(bytes32 orderID) public returns (bool) { + IOrderBook.Order memory order = _allOrders[orderID]; + uint256 amountUsed; + address srcToken; + srcToken = order.collateralTokenAmount > order.loanTokenAmount + ? order.base + : order.loanTokenAddress; + if (order.timeTillExpiration < block.timestamp || order.status != IOrderBook.OrderStatus.ACTIVE) { + return false; + } + if (IDeposits(VAULT).getDeposit(keccak256(abi.encode(order.trader, 0))) < _gasToSend(2500000)) { + return false; + } + if (order.orderType == IOrderBook.OrderType.LIMIT_OPEN) { + if (order.loanID != 0 && !_isActiveLoan(order.loanID)) { + return false; + } + uint256 dSwapValue; + if (srcToken == order.loanTokenAddress) { + amountUsed = + order.loanTokenAmount + + (order.loanTokenAmount * order.leverage) / + 10**18; //adjusts leverage + } else { + amountUsed = queryRateReturn( + order.base, + order.loanTokenAddress, + order.collateralTokenAmount + ); + amountUsed = (amountUsed * order.leverage) / 10**18; + } + dSwapValue = + order.collateralTokenAmount + + PROTOCOL.getSwapExpectedReturn( + order.trader, + order.loanTokenAddress, + order.base, + amountUsed, + order.loanDataBytes + ); + + if (order.amountReceived <= dSwapValue) { + return true; + } + } else if (order.orderType == IOrderBook.OrderType.LIMIT_CLOSE) { + if (!_isActiveLoan(order.loanID)) { + return false; + } + uint256 dSwapValue; + dSwapValue = PROTOCOL.getSwapExpectedReturn( + order.trader, + order.base, + order.loanTokenAddress, + order.collateralTokenAmount, + order.loanDataBytes + ); + if (order.amountReceived <= dSwapValue) { + return true; + } + } else if (order.orderType == IOrderBook.OrderType.MARKET_STOP) { + if (!_isActiveLoan(order.loanID)) { + return false; + } + //order.leverage is repurposed to be min amount received. optional + bool operand; + uint256 dexSwapReceived = PROTOCOL.getSwapExpectedReturn( + order.trader, + order.base, + order.loanTokenAddress, + order.collateralTokenAmount, + order.loanDataBytes + ); + if (_useOracle[order.trader]) { + operand = + order.amountReceived >= + queryRateReturn( + order.base, + order.loanTokenAddress, + order.collateralTokenAmount + ); + } else { + operand = order.amountReceived >= dexSwapReceived; + } + if (order.leverage <= dexSwapReceived && operand) { + return true; + } + } + return false; + } + + function getDexRate( + address srcToken, + address destToken, + bytes memory payload, + uint256 amountIn + ) public returns (uint256 rate) { + uint256 tradeSize = 10**IERC20Metadata(srcToken).decimals(); + rate = PROTOCOL.getSwapExpectedReturn( + address(this), + srcToken, + destToken, + amountIn, + payload + ); + rate = (rate * amountIn) / tradeSize; + } + + function priceCheck( + address srcToken, + address destToken, + bytes memory payload + ) public returns (bool) { + uint256 tradeSize = 10**IERC20Metadata(srcToken).decimals(); + uint256 dexRate = getDexRate( + srcToken, + destToken, + payload, + tradeSize + ); + uint256 indexRate = queryRateReturn(srcToken, destToken, tradeSize); + if (dexRate >= indexRate) { + if (((dexRate - indexRate) * 1000) / dexRate <= 9) { + return true; + } else { + return false; + } + } else { + if (((indexRate - dexRate) * 1000) / indexRate <= 9) { + return true; + } else { + return false; + } + } + } + + function executeOrder(bytes32 orderID) external pausable returns (uint256) { + uint256 gasStart = gasleft(); + IOrderBook.Order memory order = _allOrders[orderID]; + require( + order.status == IOrderBook.OrderStatus.ACTIVE, + "OrderBook: non active" + ); + address srcToken; + uint256 amountUsed; + srcToken = order.collateralTokenAmount > order.loanTokenAmount + ? order.base + : order.loanTokenAddress; + require( + order.timeTillExpiration > block.timestamp, + "OrderBook: Order Expired" + ); + if (order.orderType == IOrderBook.OrderType.LIMIT_OPEN) { + uint256 dSwapValue; + if (srcToken == order.loanTokenAddress) { + amountUsed = + order.loanTokenAmount + + (order.loanTokenAmount * order.leverage) / + 10**18; //adjusts leverage + } else { + amountUsed = queryRateReturn( + order.base, + order.loanTokenAddress, + order.collateralTokenAmount + ); + amountUsed = (amountUsed * order.leverage) / 10**18; + } + dSwapValue = + order.collateralTokenAmount + + PROTOCOL.getSwapExpectedReturn( + order.trader, + order.loanTokenAddress, + order.base, + amountUsed, + order.loanDataBytes + ); + + require( + order.amountReceived <= dSwapValue, + "OrderBook: amountOut too low" + ); + _executeTradeOpen(order); + _allOrders[orderID].status = IOrderBook.OrderStatus.EXECUTED; + _allOrderIDs.remove(orderID); + _histOrders[order.trader].remove(orderID); + emit OrderExecuted(order.trader, orderID); + } + else if (order.orderType == IOrderBook.OrderType.LIMIT_CLOSE) { + uint256 dSwapValue; + dSwapValue = PROTOCOL.getSwapExpectedReturn( + order.trader, + order.base, + order.loanTokenAddress, + order.collateralTokenAmount, + order.loanDataBytes + ); + require( + order.amountReceived <= dSwapValue, + "OrderBook: amountOut too low" + ); + _executeTradeClose( + order.trader, + order.loanID, + order.collateralTokenAmount, + order.loanDataBytes + ); + _allOrders[orderID].status = IOrderBook.OrderStatus.EXECUTED; + _allOrderIDs.remove(orderID); + _histOrders[order.trader].remove(orderID); + emit OrderExecuted(order.trader, orderID); + } + else if (order.orderType == IOrderBook.OrderType.MARKET_STOP) { + //order.leverage is repurposed to be min amount received. optional + bool operand; + uint256 dexSwapReceived = PROTOCOL.getSwapExpectedReturn( + order.trader, + order.base, + order.loanTokenAddress, + order.collateralTokenAmount, + order.loanDataBytes + ); + if (_useOracle[order.trader]) { + operand = + order.amountReceived >= + queryRateReturn( + order.base, + order.loanTokenAddress, + order.collateralTokenAmount + ); + } else { + operand = order.amountReceived >= dexSwapReceived; + } + require( + order.leverage <= dexSwapReceived && + operand && + priceCheck( + order.base, + order.loanTokenAddress, + order.loanDataBytes + ), + "OrderBook: invalid swap rate" + ); + _executeTradeClose( + order.trader, + order.loanID, + order.collateralTokenAmount, + order.loanDataBytes + ); + _allOrders[orderID].status = IOrderBook.OrderStatus.EXECUTED; + _allOrderIDs.remove(orderID); + _histOrders[order.trader].remove(orderID); + emit OrderExecuted(order.trader, orderID); + } + return(_spendGasFeeToken(_gasToSend(gasStart-gasleft()), order.trader, msg.sender)); + } + + function setPriceFeed(address newFeed) external onlyOwner { + priceFeed = newFeed; + } + + function setGasPrice(uint256 price) external onlyGuardian { + chainGasPrice = price; + } +} diff --git a/contracts/orderbook/Logic/OrderBookData.sol b/contracts/orderbook/Logic/OrderBookData.sol new file mode 100644 index 00000000..bb7c7453 --- /dev/null +++ b/contracts/orderbook/Logic/OrderBookData.sol @@ -0,0 +1,162 @@ +pragma solidity ^0.8.0; + +import "../Events/OrderBookEvents.sol"; +import "../Storage/OrderBookStorage.sol"; +import "@openzeppelin-4.7.0/token/ERC20/utils/SafeERC20.sol"; + +contract OrderBookData is OrderBookEvents, OrderBookStorage { + using EnumerableSet for EnumerableSet.Bytes32Set; + using SafeERC20 for IERC20; + + function initialize(address target) public onlyOwner { + _setTarget(this.adjustAllowance.selector, target); + _setTarget(this.revokeAllowance.selector, target); + _setTarget(this.getUserOrders.selector, target); + _setTarget(this.getUserOrdersLimited.selector, target); + _setTarget(this.getOrderByOrderID.selector, target); + _setTarget(this.getUserOrderIDs.selector, target); + _setTarget(this.getUserOrdersCount.selector, target); + _setTarget(this.getGlobalOrderIDs.selector, target); + _setTarget(this.getGlobalOrdersCount.selector, target); + _setTarget(this.getGlobalOrders.selector, target); + _setTarget(this.getGlobalOrderIDsLimited.selector, target); + _setTarget(this.getGlobalOrdersLimited.selector, target); + } + + function adjustAllowance(address[] memory spenders, address[] memory tokens) external onlyOwner { + address spender; + address token; + for (uint i; i < spenders.length;) { + spender = spenders[i]; + for (uint y; y < tokens.length;) { + token = tokens[y]; + require( + PROTOCOL.isLoanPool(spender) || + address(PROTOCOL) == spender || + VAULT == spender, + "OrderBook: invalid spender" + ); + IERC20(token).safeApprove(spender, type(uint256).max); + unchecked { ++y; } + } + unchecked { ++i; } + } + + } + + function revokeAllowance(address[] memory spenders, address[] memory tokens) external onlyOwner { + address spender; + address token; + for (uint i; i < spenders.length;) { + spender = spenders[i]; + for (uint y; y < tokens.length;) { + token = tokens[y]; + require( + PROTOCOL.isLoanPool(spender) || + address(PROTOCOL) == spender || + VAULT == spender, + "OrderBook: invalid spender" + ); + IERC20(token).safeApprove(spender, 0); + unchecked { ++y; } + } + unchecked { ++i; } + } + + } + function getUserOrders(address trader) + external + view + returns (IOrderBook.Order[] memory fullList) + { + bytes32[] memory idSet = _histOrders[trader].values(); + + fullList = new IOrderBook.Order[](idSet.length); + for (uint256 i = 0; i < idSet.length;) { + fullList[i] = _allOrders[idSet[i]]; + unchecked { ++i; } + } + return fullList; + } + + function getUserOrdersLimited(address trader, uint start, uint end) + external + view + returns (IOrderBook.Order[] memory fullList) + { + require(end<=_histOrders[trader].length(), "OrderBook: end is past max orders"); + fullList = new IOrderBook.Order[](end-start); + for (uint256 i = start; i < end;) { + fullList[i] = _allOrders[_histOrders[trader].at(i)]; + unchecked { ++i; } + } + return fullList; + } + + function getOrderByOrderID(bytes32 orderID) + public + view + returns (IOrderBook.Order memory) + { + return _allOrders[orderID]; + } + + function getUserOrderIDs(address trader) + external + view + returns (bytes32[] memory) + { + return _histOrders[trader].values(); + } + + function getUserOrdersCount(address trader) external view returns (uint256) { + return _histOrders[trader].length(); + } + + function getGlobalOrderIDs() external view returns (bytes32[] memory) { + return _allOrderIDs.values(); + } + + function getGlobalOrdersCount() external view returns (uint256) { + return _allOrderIDs.length(); + } + + function getGlobalOrderIDsLimited(uint start, uint end) external view returns (bytes32[] memory fullList) { + require(end<=_allOrderIDs.length(), "OrderBook: end is past max orders"); + fullList = new bytes32[](end-start); + for (uint256 i = start; i < end;) { + fullList[i] = _allOrderIDs.at(i); + unchecked { ++i; } + } + return fullList; + } + + function getGlobalOrders() + external + view + returns (IOrderBook.Order[] memory fullList) + { + bytes32[] memory idSet = _allOrderIDs.values(); + + fullList = new IOrderBook.Order[](idSet.length); + for (uint256 i = 0; i < idSet.length;) { + fullList[i] = getOrderByOrderID(idSet[i]); + unchecked { ++i; } + } + return fullList; + } + + function getGlobalOrdersLimited(uint start, uint end) + external + view + returns (IOrderBook.Order[] memory fullList) + { + require(end<=_allOrderIDs.length(), "OrderBook: end is past max orders"); + fullList = new IOrderBook.Order[](end-start); + for (uint256 i = start; i < end;) { + fullList[i] = _allOrders[_allOrderIDs.at(i)]; + unchecked { ++i; } + } + return fullList; + } +} diff --git a/contracts/orderbook/Logic/OrderBookOrderPlace.sol b/contracts/orderbook/Logic/OrderBookOrderPlace.sol new file mode 100644 index 00000000..4156b1bd --- /dev/null +++ b/contracts/orderbook/Logic/OrderBookOrderPlace.sol @@ -0,0 +1,250 @@ +pragma solidity ^0.8.0; +import "../Events/OrderBookEvents.sol"; +import "../Storage/OrderBookStorage.sol"; +import "../OrderVault/IDeposits.sol"; + +contract OrderBookOrderPlace is OrderBookEvents, OrderBookStorage { + using EnumerableSet for EnumerableSet.Bytes32Set; + + function initialize(address target) public onlyOwner { + _setTarget(this.placeOrder.selector, target); + _setTarget(this.amendOrder.selector, target); + _setTarget(this.cancelOrder.selector, target); + _setTarget(this.cancelOrderProtocol.selector, target); + _setTarget(this.changeStopType.selector, target); + _setTarget(this.cancelOrderGuardian.selector, target); + } + + function _caseChecks(bytes32 ID, address collateral, address loanToken) + internal + view + returns (bool) + { + IBZx.LoanReturnData memory data = PROTOCOL.getLoan(ID); + return + data.loanToken == loanToken && + data.collateralToken == collateral && + PROTOCOL.delegatedManagers(ID, address(this)); + } + + function _isActiveLoan(bytes32 ID) internal view returns (bool) { + return PROTOCOL.loans(ID).active; + } + + function _commonChecks(IOrderBook.Order calldata order) internal { + require(!(order.collateralTokenAmount>0) || !(order.loanTokenAmount >0), "OrderBook: collateral and loan token cannot be non-zero"); + require(PROTOCOL.supportedTokens(order.loanTokenAddress), "OrderBook: Unsupported loan token"); + require(PROTOCOL.supportedTokens(order.base), "OrderBook: Unsupported collateral"); + require(order.loanID != 0 + ? _caseChecks(order.loanID, order.base, order.loanTokenAddress) + : true, + "OrderBook: case checks failed"); + require(order.orderType == IOrderBook.OrderType.LIMIT_OPEN + ? PROTOCOL.loanPoolToUnderlying(order.iToken) == order.loanTokenAddress + : true, + "OrderBook: Not a loan pool"); + require(order.orderType == IOrderBook.OrderType.LIMIT_OPEN + ? order.loanID == 0 || + _isActiveLoan(order.loanID) + : _isActiveLoan(order.loanID), + "OrderBook: non-active loan specified"); + require(order.trader == msg.sender, "OrderBook: invalid trader"); + require(order.loanDataBytes.length < 2500, "OrderBook: too much data"); + } + + function _getGasPrice() internal view returns (uint256 gasPrice) { + gasPrice = chainGasPrice; + } + + function _gasToSend(uint256 gasUsed) internal view returns (uint256) { + return gasUsed*_getGasPrice()*2; + } + + function placeOrder(IOrderBook.Order calldata order) external pausable { + _commonChecks(order); + (uint256 amountUsed, address usedToken) = order.loanTokenAmount > order.collateralTokenAmount + ? (order.loanTokenAmount, order.loanTokenAddress) + : (order.collateralTokenAmount, order.base); + uint256 tradeSize; + if (order.orderType == IOrderBook.OrderType.LIMIT_OPEN) { + if (usedToken == order.base) { + tradeSize = IPriceFeeds(priceFeed).queryReturn(order.base, order.loanTokenAddress, amountUsed)*order.leverage/10**18; + } else { + tradeSize = amountUsed*(order.leverage+1e18)/1e18; + } + } else { + tradeSize = IPriceFeeds(priceFeed).queryReturn(order.base, order.loanTokenAddress, amountUsed); + } + require(IPriceFeeds(priceFeed).queryReturn(order.loanTokenAddress, USDC, tradeSize) > MIN_AMOUNT_IN_USDC, "OrderBook: trade too small"); + require(order.status==IOrderBook.OrderStatus.ACTIVE, "OrderBook: invalid order state"); + require(IDeposits(VAULT).getDeposit(keccak256(abi.encode(order.trader,0)))>(_histOrders[order.trader].length()+1)*_gasToSend(4000000), "too little gas left"); + mainOBID++; + bytes32 ID = keccak256(abi.encode(msg.sender, mainOBID)); + require(IDeposits(VAULT).getTokenUsed(ID) == address(0), "Orderbook: collision"); //in the very unlikely chance of collision on ID error is thrown + _allOrders[ID] = order; + _allOrders[ID].orderID = ID; + _histOrders[msg.sender].add(ID); + _allOrderIDs.add(ID); + if (order.orderType == IOrderBook.OrderType.LIMIT_OPEN) { + IDeposits(VAULT).deposit(ID, amountUsed, msg.sender, usedToken); + } + emit OrderPlaced( + msg.sender, + order.orderType, + order.amountReceived, + ID, + order.base, + order.loanTokenAddress + ); + } + + function amendOrder(IOrderBook.Order calldata order) external pausable { + _commonChecks(order); + require( + order.base == _allOrders[order.orderID].base && + order.loanTokenAddress == + _allOrders[order.orderID].loanTokenAddress, + "OrderBook: invalid tokens" + ); + require( + _allOrders[order.orderID].status==IOrderBook.OrderStatus.ACTIVE, + "OrderBook: inactive order specified" + ); + (uint256 amountUsed, address usedToken) = order.loanTokenAmount > order.collateralTokenAmount + ? (order.loanTokenAmount, order.loanTokenAddress) + : (order.collateralTokenAmount, order.base); + uint256 tradeSize; + if (order.orderType == IOrderBook.OrderType.LIMIT_OPEN) { + if (usedToken == order.base) { + tradeSize = IPriceFeeds(priceFeed).queryReturn(order.base, order.loanTokenAddress, amountUsed)*order.leverage/10**18; + } else { + tradeSize = amountUsed*(order.leverage+1e18)/1e18; + } + } else { + tradeSize = IPriceFeeds(priceFeed).queryReturn(order.base, order.loanTokenAddress, amountUsed); + } + require(IPriceFeeds(priceFeed).queryReturn(order.loanTokenAddress, USDC, tradeSize) > MIN_AMOUNT_IN_USDC, "OrderBook: trade too small"); + + + if (order.orderType == IOrderBook.OrderType.LIMIT_OPEN) { + uint256 storedAmount = IDeposits(VAULT).getDeposit( + order.orderID + ); + require( + usedToken == + IDeposits(VAULT).getTokenUsed(order.orderID), + "OrderBook: invalid used token" + ); + uint256 amountUsedOld = _allOrders[order.orderID].loanTokenAmount + + _allOrders[order.orderID].collateralTokenAmount; + if (amountUsedOld > amountUsed) { + IDeposits(VAULT).partialWithdraw( + msg.sender, + order.orderID, + amountUsedOld - amountUsed + ); + } else { + IDeposits(VAULT).deposit( + order.orderID, + amountUsed - amountUsedOld, + msg.sender, + usedToken + ); + } + } + _allOrders[order.orderID] = order; + emit OrderAmended( + msg.sender, + order.orderType, + order.amountReceived, + order.orderID, + order.base, + order.loanTokenAddress + ); + } + + function cancelOrder(bytes32 orderID) external pausable { + require(_allOrders[orderID].status==IOrderBook.OrderStatus.ACTIVE, "OrderBook: inactive order"); + _allOrders[orderID].status = IOrderBook.OrderStatus.CANCELLED; + require(_histOrders[msg.sender].remove(orderID), "OrderBook: not owner of order"); + _allOrderIDs.remove(orderID); + if (_allOrders[orderID].orderType == IOrderBook.OrderType.LIMIT_OPEN) { + address usedToken = IDeposits(VAULT).getTokenUsed( + orderID + ); + IDeposits(VAULT).withdrawToTrader(msg.sender, orderID); + } + emit OrderCancelled(msg.sender, orderID); + } + + function cancelOrderGuardian(bytes32 orderID) external onlyGuardian { + _allOrders[orderID].status = IOrderBook.OrderStatus.CANCELLED; + address trader = _allOrders[orderID].trader; + _histOrders[trader].remove(orderID); + _allOrderIDs.remove(orderID); + if (_allOrders[orderID].orderType == IOrderBook.OrderType.LIMIT_OPEN) { + address usedToken = IDeposits(VAULT).getTokenUsed( + orderID + ); + IDeposits(VAULT).withdrawToTrader(trader, orderID); + } + emit OrderCancelled(trader, orderID); + } + + function _spendGasFeeToken(uint256 amount, address trader, address receiver) internal returns (uint256) { + bytes32 orderID = keccak256(abi.encode(trader, 0)); + uint256 amountStored = IDeposits(VAULT).getDeposit(orderID); + if (amountStored < amount) { + amount = amountStored; + } + IDeposits(VAULT).partialWithdraw(receiver, orderID, amount); + return amount; + } + + function cancelOrderProtocol(bytes32 orderID) external pausable returns (uint256) { + uint gasStart = gasleft(); + IOrderBook.Order memory order = _allOrders[orderID]; + address trader = order.trader; + require(order.status==IOrderBook.OrderStatus.ACTIVE, "OrderBook: inactive order"); + uint256 amountUsed = order.collateralTokenAmount + + order.loanTokenAmount; + uint256 swapRate; + if (order.orderType == IOrderBook.OrderType.LIMIT_OPEN) { + swapRate = IPriceFeeds(priceFeed).queryReturn( + order.loanTokenAddress, + order.base, + amountUsed + ); + } else { + swapRate = IPriceFeeds(priceFeed).queryReturn( + order.base, + order.loanTokenAddress, + amountUsed + ); + } + require( + ( + order.amountReceived > swapRate + ? (order.amountReceived - swapRate) > + (order.amountReceived * 25) / 100 + : (swapRate - order.amountReceived) > + (swapRate * 25) / 100 + ) || order.timeTillExpiration < block.timestamp, + "OrderBook: no conditions met" + ); + + _allOrders[orderID].status = IOrderBook.OrderStatus.CANCELLED; + _histOrders[trader].remove(orderID); + _allOrderIDs.remove(orderID); + if (order.orderType == IOrderBook.OrderType.LIMIT_OPEN) { + address usedToken = IDeposits(VAULT).getTokenUsed(orderID); + IDeposits(VAULT).withdrawToTrader(trader, orderID); + } + emit OrderCancelled(trader, orderID); + return(_spendGasFeeToken(_gasToSend(gasStart-gasleft()), trader, msg.sender)); + } + + function changeStopType(bool stop) external pausable { + _useOracle[msg.sender] = stop; + } +} diff --git a/contracts/orderbook/OrderBookProxy.sol b/contracts/orderbook/OrderBookProxy.sol new file mode 100644 index 00000000..99ae5273 --- /dev/null +++ b/contracts/orderbook/OrderBookProxy.sol @@ -0,0 +1,62 @@ +pragma solidity ^0.8.0; +import "./Storage/OrderBookStorage.sol"; +import "./Events/OrderBookEvents.sol"; + +contract OrderBookProxy is OrderBookEvents, OrderBookStorage { + + fallback() external payable { + if (gasleft() <= 2300) { + return; + } + + address target = logicTargets[msg.sig]; + require(target != address(0) || msg.value != 0, "target not active"); + + bytes memory data = msg.data; + assembly { + let result := delegatecall( + gas(), + target, + add(data, 0x20), + mload(data), + 0, + 0 + ) + let size := returndatasize() + let ptr := mload(0x40) + returndatacopy(ptr, 0, size) + switch result + case 0 { + revert(ptr, size) + } + default { + return(ptr, size) + } + } + } + + function replaceContract(address target) external onlyOwner { + (bool success, ) = target.delegatecall( + abi.encodeWithSignature("initialize(address)", target) + ); + require(success, "setup failed"); + } + + function setTargets( + string[] calldata sigsArr, + address[] calldata targetsArr + ) external onlyOwner { + require(sigsArr.length == targetsArr.length, "count mismatch"); + + for (uint256 i = 0; i < sigsArr.length; i++) { + _setTarget( + bytes4(keccak256(abi.encodePacked(sigsArr[i]))), + targetsArr[i] + ); + } + } + + function getTarget(string calldata sig) external view returns (address) { + return logicTargets[bytes4(keccak256(abi.encodePacked(sig)))]; + } +} diff --git a/contracts/orderbook/OrderVault/Deposits.sol b/contracts/orderbook/OrderVault/Deposits.sol new file mode 100644 index 00000000..2aa24eb1 --- /dev/null +++ b/contracts/orderbook/OrderVault/Deposits.sol @@ -0,0 +1,107 @@ +pragma solidity ^0.8.0; +import "../../governance/PausableGuardian_0_8.sol"; +import "@openzeppelin-4.7.0/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin-4.7.0/token/ERC20/extensions/IERC20Metadata.sol"; +import "../../interfaces/IWeth.sol"; + +contract Deposits is PausableGuardian_0_8 { + struct DepositInfo { + address depositToken; + uint256 depositAmount; + } + mapping(bytes32 => DepositInfo) internal _depositInfo; + address public constant WRAPPED_TOKEN = 0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270; + address public orderBook = address(0); + + function deposit( + bytes32 orderID, + uint256 tokenAmount, + address trader, + address token + ) external pausable { + require(msg.sender == orderBook, "unauthorized"); + require(_depositInfo[orderID].depositToken == address(0)|| _depositInfo[orderID].depositToken == token, + "Deposits: invalid token specified"); + _depositInfo[orderID].depositToken = token; + _depositInfo[orderID].depositAmount += tokenAmount; + SafeERC20.safeTransferFrom( + IERC20(token), + trader, + address(this), + tokenAmount + ); + } + + function depositGasToken(address trader) external pausable payable { + require(msg.sender == orderBook, "unauthorized"); + IWeth(WRAPPED_TOKEN).deposit{value:msg.value}(); + bytes32 orderID = keccak256(abi.encode(trader, 0)); + _depositInfo[orderID].depositToken = WRAPPED_TOKEN; + _depositInfo[orderID].depositAmount += msg.value; + } + + function setOrderBook(address n) external onlyOwner { + orderBook = n; + } + + function withdraw(bytes32 orderID) external pausable { + require(msg.sender == orderBook, "unauthorized"); + SafeERC20.safeTransfer( + IERC20(_depositInfo[orderID].depositToken), + msg.sender, + _depositInfo[orderID].depositAmount + ); + _depositInfo[orderID].depositAmount = 0; + } + + function withdrawToTrader(address trader, bytes32 orderID) external pausable { + require(msg.sender == orderBook, "unauthorized"); + SafeERC20.safeTransfer( + IERC20(_depositInfo[orderID].depositToken), + trader, + _depositInfo[orderID].depositAmount + ); + _depositInfo[orderID].depositAmount = 0; + } + + function refund(bytes32 orderID, uint256 amount) external pausable { + require(msg.sender == orderBook, "unauthorized"); + SafeERC20.safeTransferFrom( + IERC20(_depositInfo[orderID].depositToken), + orderBook, + address(this), + amount + ); + _depositInfo[orderID].depositAmount += amount; + } + + function partialWithdraw( + address trader, + bytes32 orderID, + uint256 amount + ) external pausable { + require(msg.sender == orderBook, "unauthorized"); + SafeERC20.safeTransfer( + IERC20(_depositInfo[orderID].depositToken), + trader, + amount + ); + _depositInfo[orderID].depositAmount -= amount; + } + + function getDeposit(bytes32 orderID) + external + view + returns (uint256) + { + return _depositInfo[orderID].depositAmount; + } + + function getTokenUsed(bytes32 orderID) + external + view + returns (address) + { + return _depositInfo[orderID].depositToken; + } +} diff --git a/contracts/orderbook/OrderVault/IDeposits.sol b/contracts/orderbook/OrderVault/IDeposits.sol new file mode 100644 index 00000000..4dd3ef79 --- /dev/null +++ b/contracts/orderbook/OrderVault/IDeposits.sol @@ -0,0 +1,34 @@ +pragma solidity ^0.8.0; + +interface IDeposits { + function deposit( + bytes32 orderID, + uint256 tokenAmount, + address trader, + address token + ) external; + + function depositGasToken(address trader) external payable; + + function withdraw(bytes32 orderID) external; + + function withdrawToTrader(address trader, bytes32 orderID) external; + + function refund(bytes32 orderID, uint256 amount) external; + + function partialWithdraw( + address trader, + bytes32 orderID, + uint256 amount + ) external; + + function getDeposit(bytes32 orderID) + external + view + returns (uint256); + + function getTokenUsed(bytes32 orderID) + external + view + returns (address); +} diff --git a/contracts/orderbook/Storage/OrderBookConstants.sol b/contracts/orderbook/Storage/OrderBookConstants.sol new file mode 100644 index 00000000..e4fb4470 --- /dev/null +++ b/contracts/orderbook/Storage/OrderBookConstants.sol @@ -0,0 +1,14 @@ +pragma solidity ^0.8.0; + +import "../../../interfaces/IBZx.sol"; + + +contract OrderBookConstants { + address public constant WRAPPED_TOKEN = + 0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270; + uint256 public constant MIN_AMOUNT_IN_USDC = 1e6; + address public constant USDC = 0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174; + address public constant VAULT = 0xFA6485ec4Aa9AF504adb4ed47b567E1875E21e85; + IBZx public constant PROTOCOL = + IBZx(0x059D60a9CEfBc70b9Ea9FFBb9a041581B1dFA6a8); +} \ No newline at end of file diff --git a/contracts/orderbook/Storage/OrderBookStorage.sol b/contracts/orderbook/Storage/OrderBookStorage.sol new file mode 100644 index 00000000..9766c7d6 --- /dev/null +++ b/contracts/orderbook/Storage/OrderBookStorage.sol @@ -0,0 +1,32 @@ +pragma solidity ^0.8.0; + +import "../IOrderBook.sol"; +import "../../governance/PausableGuardian_0_8.sol"; +import "@openzeppelin-4.7.0/utils/structs/EnumerableSet.sol"; +import "../../../interfaces/IPriceFeeds.sol"; +import "../../../interfaces/IToken.sol"; +import "@openzeppelin-4.7.0/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin-4.7.0/token/ERC20/extensions/IERC20Metadata.sol"; +import "./OrderBookConstants.sol"; + +contract OrderBookStorage is OrderBookConstants, PausableGuardian_0_8 { + + using EnumerableSet for EnumerableSet.Bytes32Set; + mapping(bytes32 => IOrderBook.Order) internal _allOrders; + mapping(bytes32 => uint256) internal _orderExpiration; + mapping(address => EnumerableSet.Bytes32Set) internal _histOrders; + mapping(address => bool) internal _useOracle; + EnumerableSet.Bytes32Set internal _allOrderIDs; + + mapping(bytes4 => address) public logicTargets; + + uint256 public mainOBID; + + address public priceFeed = address(0); + + uint256 public chainGasPrice; + + function _setTarget(bytes4 sig, address target) internal { + logicTargets[sig] = target; + } +} diff --git a/contracts/utils/ExponentMath.sol b/contracts/utils/ExponentMath.sol new file mode 100644 index 00000000..4bb14e71 --- /dev/null +++ b/contracts/utils/ExponentMath.sol @@ -0,0 +1,15 @@ +pragma solidity ^0.8.0; + + +library ExponentMath{ + + function tenExp(uint256 number, int8 pow) public pure returns (uint256){ + if(pow < 0){ + number=number/10**(uint8(pow*-1)); + }else{ + number=number*10**uint8(pow); + } + return number; + } + +} \ No newline at end of file diff --git a/testspolygon/limit-orders/test_Orders.py b/testspolygon/limit-orders/test_Orders.py new file mode 100644 index 00000000..f21ee533 --- /dev/null +++ b/testspolygon/limit-orders/test_Orders.py @@ -0,0 +1,70 @@ +from brownie import * +from eth_abi import encode_abi + +def test_t(): + ORDERBOOK = interface.IOrderBook('0x043582611B2d62Ee084D72f0E731883653f837CE') + upgrade_contracts(ORDERBOOK) + print(ORDERBOOK.getDexRate.call( + "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619", "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", b'', 1e18)) + print(ORDERBOOK.queryRateReturn.call( + "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619", "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", 1e18)) + + print(ORDERBOOK.priceCheck.call( + "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619", "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", b'')) + USDC = interface.IERC20('0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174') + iUSDC = interface.IERC20('0xC3f6816C860e7d7893508C8F8568d5AF190f6d7d') + ETH = interface.IERC20('0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619') + USDC.transfer(accounts[0], 20e6, { + 'from': '0xf977814e90da44bfa03b6295a0616a897441acec'}) + USDC.approve(ORDERBOOK.VAULT(), 2e10, {'from': accounts[0]}) + interface.IERC20('0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270').approve(ORDERBOOK.VAULT(), 2e18, {'from':accounts[0]}) + ORDERBOOK.depositGasFeeToken(0, {'from':accounts[0], 'amount':1e18}) + print(interface.IDeposits(ORDERBOOK.VAULT()).getDeposit(web3.keccak(encode_abi(['address','uint256'],[accounts[0].address,0])))) + print(ORDERBOOK.getGasPrice()) + data = encode_abi(['address'],[ORDERBOOK.address]) + data = encode_abi(['uint256','bytes[]'],[6,(encode_abi(['uint256','bytes'],[1,encode_abi(['address','address'],[USDC.address,ETH.address])]),data)]) + print(ORDERBOOK.placeOrder([0, 0, 1e6, 1e18, int(1.1e6), 0, accounts[0], + iUSDC, USDC, ETH, 0, 0, 1000000000000, data], {'from': accounts[0]}).gas_used) + print(ORDERBOOK.getOrders.call()[0]) + print(ORDERBOOK.executeOrder(ORDERBOOK.getOrders.call()[0][1],{'from':accounts[0]}).gas_used) + print(interface.IDeposits(ORDERBOOK.VAULT()).getDeposit(web3.keccak(encode_abi(['address','uint256'],[accounts[0].address,0])))) + protocolRank = interface.IBZx(ORDERBOOK.PROTOCOL.call()) + ll = protocolRank.getUserLoans.call(accounts[0],0,10,0,False,False) + print(ll) + print(ORDERBOOK.placeOrder([ll[0][0],0,1e18,0,0,ll[0][5],accounts[0], + iUSDC, USDC, ETH, 2, 0, 1000000000000, b''], {'from': accounts[0]}).gas_used) + ORDERBOOK.changeStopType(True, {'from':accounts[0]}) + print(ORDERBOOK.getActiveOrders.call(accounts[0])) + print(ORDERBOOK.getActiveOrdersLimited.call(accounts[0],0,ORDERBOOK.getTotalOrders.call(accounts[0]))) + print(ORDERBOOK.getActiveOrderIDs.call(accounts[0])) + print(ORDERBOOK.getTotalOrderIDs.call()) + print(ORDERBOOK.getOrderIDs.call()) + print(ORDERBOOK.getOrderIDsLimited.call(0,ORDERBOOK.getTotalOrderIDs.call())) + print(ORDERBOOK.getOrdersLimited.call(0,ORDERBOOK.getTotalOrderIDs.call())) + print(ORDERBOOK.clearOrder.call(ORDERBOOK.getOrderIDs.call()[0])) + keeper = deploy_keeper(ORDERBOOK) + print(keeper.checkUpkeep(encode_abi(['uint256','uint256'],[0,1]),{'from':accounts[0]}).gas_used) + (needed, data) = keeper.checkUpkeep.call(encode_abi(['uint256','uint256'],[0,1])) + if(needed): + keeper.performUpkeep(data, {'from':accounts[0]}) + print(ORDERBOOK.getOrders.call()) + ll = protocolRank.getUserLoans.call(accounts[0],0,10,0,False,False) + print(ll) + assert(False) + +def upgrade_contracts(ORDERBOOK): + main = OrderBook.deploy({'from':accounts[0]}) + data = OrderBookData.deploy({'from':accounts[0]}) + placement = OrderBookOrderPlace.deploy({'from':accounts[0]}) + ORDERBOOK = Contract.from_abi('',ORDERBOOK.address,OrderBookProxy.abi) + ORDERBOOK.replaceContract(main,{'from':ORDERBOOK.owner()}) + ORDERBOOK.replaceContract(data,{'from':ORDERBOOK.owner()}) + ORDERBOOK.replaceContract(placement,{'from':ORDERBOOK.owner()}) + interface.IOrderBook(ORDERBOOK).setPriceFeed('0x600F8E7B10CF6DA18871Ff79e4A61B13caCEd9BC',{'from':ORDERBOOK.owner()}) + +def deploy_keeper(ORDERBOOK): + keeper = OrderKeeper.deploy({'from':accounts[0]}) + proxy = Proxy_0_8.deploy(keeper, {'from':accounts[0]}) + keeper = Contract.from_abi('',proxy.address,OrderKeeper.abi) + keeper.setOrderBook(ORDERBOOK,{'from':accounts[0]}) + return keeper \ No newline at end of file diff --git a/testspolygon/limit-orders/test_Orders_Full.py b/testspolygon/limit-orders/test_Orders_Full.py new file mode 100644 index 00000000..c948e4e2 --- /dev/null +++ b/testspolygon/limit-orders/test_Orders_Full.py @@ -0,0 +1,222 @@ +import pytest +from brownie import * +from eth_abi import encode_abi + +@pytest.fixture(scope="module") +def DEPOSITS(interface, ORDERBOOK): + return interface.IDeposits(ORDERBOOK.VAULT()) + +@pytest.fixture(scope="module") +def TOKEN_REGISTRY(TokenRegistry): + return Contract.from_abi("TOKEN_REGISTRY", "0x4B234781Af34E9fD756C27a47675cbba19DC8765", TokenRegistry.abi) + +@pytest.fixture(scope="module") +def ORDERBOOK(interface): + return Contract.from_abi("ORDERBOOK", "0x043582611b2d62ee084d72f0e731883653f837ce", interface.IOrderBook.abi) + +@pytest.fixture(scope="module") +def USDC(TestToken): + return Contract.from_abi("USDC","0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174",TestToken.abi) + +@pytest.fixture(scope="module") +def ETH(TestToken): + return Contract.from_abi("ETH","0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619",TestToken.abi) + +@pytest.fixture(scope="module") +def WMATIC(TestToken): + return Contract.from_abi("WMATIC","0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270",TestToken.abi) + +@pytest.fixture(scope="module") +def IUSDC(interface): + return Contract.from_abi("IUSDC","0xC3f6816C860e7d7893508C8F8568d5AF190f6d7d",interface.IToken.abi) + +@pytest.fixture(scope="module") +def IBZX(interface): + return interface.IBZx("0x059D60a9CEfBc70b9Ea9FFBb9a041581B1dFA6a8") + +#test case 1. place limit open order and execute as well as limit close +def test_case1(ORDERBOOK, DEPOSITS, USDC, IUSDC, ETH, WMATIC, IBZX): + ob = ORDERBOOK + deposits = DEPOSITS + + #get some USDC + USDC.transfer(accounts[0], 10000e6, {'from':"0xf977814e90da44bfa03b6295a0616a897441acec"}) + USDC.approve(deposits, 10000e6, {'from':accounts[0]}) + + #deposit gas token + WMATIC.approve(deposits, 100e18, {"from":accounts[0]}) + ob.depositGasFeeToken(0, {'from':accounts[0], 'value':100e18}) + + #place open order + setManager = encode_abi(['address'],[ob.address]) + dex_payload = encode_abi(['address','address'],[USDC.address,ETH.address]) + dex_selector = encode_abi(['uint256','bytes'],[1,dex_payload]) + loanDataBytes = encode_abi(['uint256','bytes[]'],[6,(dex_selector,setManager)]) + ob.placeOrder([0, 0, 1e6, 1e18, int(100e6), 0, accounts[0], + IUSDC, USDC, ETH, 0, 0, 1000000000000, loanDataBytes], {'from': accounts[0]}) + assert(ob.getUserOrdersCount(accounts[0]) == 1) + assert(ob.getGlobalOrdersCount() > 1) + orderID = ob.getUserOrderIDs(accounts[0])[0] + assert(ob.prelimCheck.call(orderID) == True) + + balanceBefore = WMATIC.balanceOf(accounts[0]) + #execute order + ob.executeOrder(orderID, {'from':accounts[0]}) + assert(WMATIC.balanceOf(accounts[0]) > balanceBefore) + assert(IBZX.getUserLoansCount(accounts[0], False) == 1) + + #place close order + orderToClose = IBZX.getUserLoans(accounts[0],0,10,0,False,False)[0] + ob.placeOrder([orderToClose[0], 0, orderToClose[4], 0, 0, orderToClose[5], accounts[0], IUSDC, USDC, ETH, 1, 0, 1000000000000, b''], {'from': accounts[0]}) + assert(ob.getUserOrdersCount(accounts[0]) == 1) + assert(ob.getGlobalOrdersCount() > 1) + orderID = ob.getUserOrderIDs(accounts[0])[0] + assert(ob.prelimCheck.call(orderID) == True) + + balanceBefore = WMATIC.balanceOf(accounts[0]) + #execute order + ob.executeOrder(orderID, {'from':accounts[0]}) + assert(WMATIC.balanceOf(accounts[0]) > balanceBefore) + assert(IBZX.getUserLoansCount(accounts[0], False) == 0) + + +#test case 2. place limit open order and execute as well as market stop w/o oracle +def test_case2(ORDERBOOK, DEPOSITS, USDC, IUSDC, ETH, WMATIC, IBZX): + ob = ORDERBOOK + deposits = DEPOSITS + + #get some USDC + USDC.transfer(accounts[0], 10000e6, {'from':"0xf977814e90da44bfa03b6295a0616a897441acec"}) + USDC.approve(deposits, 10000e6, {'from':accounts[0]}) + + #deposit gas token + WMATIC.approve(deposits, 100e18, {"from":accounts[0]}) + ob.depositGasFeeToken(0, {'from':accounts[0], 'value':100e18}) + + #place open order + setManager = encode_abi(['address'],[ob.address]) + dex_payload = encode_abi(['address','address'],[USDC.address,ETH.address]) + dex_selector = encode_abi(['uint256','bytes'],[1,dex_payload]) + loanDataBytes = encode_abi(['uint256','bytes[]'],[6,(dex_selector,setManager)]) + ob.placeOrder([0, 0, 1e6, 1e18, int(100e6), 0, accounts[0], + IUSDC, USDC, ETH, 0, 0, 1000000000000, loanDataBytes], {'from': accounts[0]}) + assert(ob.getUserOrdersCount(accounts[0]) == 1) + assert(ob.getGlobalOrdersCount() > 1) + orderID = ob.getUserOrderIDs(accounts[0])[0] + assert(ob.prelimCheck.call(orderID) == True) + + balanceBefore = WMATIC.balanceOf(accounts[0]) + #execute order + ob.executeOrder(orderID, {'from':accounts[0]}) + assert(WMATIC.balanceOf(accounts[0]) > balanceBefore) + assert(IBZX.getUserLoansCount(accounts[0], False) == 1) + + #place close order + orderToClose = IBZX.getUserLoans(accounts[0],0,10,0,False,False)[0] + ob.placeOrder([orderToClose[0], 0, orderToClose[4]*10, 0, 0, orderToClose[5], accounts[0], IUSDC, USDC, ETH, 2, 0, 1000000000000, b''], {'from': accounts[0]}) + assert(ob.getUserOrdersCount(accounts[0]) == 1) + assert(ob.getGlobalOrdersCount() > 1) + orderID = ob.getUserOrderIDs(accounts[0])[0] + assert(ob.prelimCheck.call(orderID) == True) + + balanceBefore = WMATIC.balanceOf(accounts[0]) + #execute order + ob.executeOrder(orderID, {'from':accounts[0]}) + assert(WMATIC.balanceOf(accounts[0]) > balanceBefore) + assert(IBZX.getUserLoansCount(accounts[0], False) == 0) + +#test case 3. place limit open order and execute as well as market stop w/ oracle +def test_case3(ORDERBOOK, DEPOSITS, USDC, IUSDC, ETH, WMATIC, IBZX): + ob = ORDERBOOK + deposits = DEPOSITS + + #get some USDC + USDC.transfer(accounts[0], 10000e6, {'from':"0xf977814e90da44bfa03b6295a0616a897441acec"}) + USDC.approve(deposits, 10000e6, {'from':accounts[0]}) + + #deposit gas token + WMATIC.approve(deposits, 100e18, {"from":accounts[0]}) + ob.depositGasFeeToken(0, {'from':accounts[0], 'value':100e18}) + + #place open order + setManager = encode_abi(['address'],[ob.address]) + dex_payload = encode_abi(['address','address'],[USDC.address,ETH.address]) + dex_selector = encode_abi(['uint256','bytes'],[1,dex_payload]) + loanDataBytes = encode_abi(['uint256','bytes[]'],[6,(dex_selector,setManager)]) + ob.placeOrder([0, 0, 1e6, 1e18, int(100e6), 0, accounts[0], + IUSDC, USDC, ETH, 0, 0, 1000000000000, loanDataBytes], {'from': accounts[0]}) + assert(ob.getUserOrdersCount(accounts[0]) == 1) + assert(ob.getGlobalOrdersCount() > 1) + orderID = ob.getUserOrderIDs(accounts[0])[0] + assert(ob.prelimCheck.call(orderID) == True) + + balanceBefore = WMATIC.balanceOf(accounts[0]) + #execute order + ob.executeOrder(orderID, {'from':accounts[0]}) + assert(WMATIC.balanceOf(accounts[0]) > balanceBefore) + assert(IBZX.getUserLoansCount(accounts[0], False) == 1) + + #change stop type + ob.changeStopType(True, {'from':accounts[0]}) + + #place close order + orderToClose = IBZX.getUserLoans(accounts[0],0,10,0,False,False)[0] + ob.placeOrder([orderToClose[0], 0, orderToClose[4]*10, 0, 0, orderToClose[5], accounts[0], IUSDC, USDC, ETH, 2, 0, 1000000000000, b''], {'from': accounts[0]}) + assert(ob.getUserOrdersCount(accounts[0]) == 1) + assert(ob.getGlobalOrdersCount() > 1) + orderID = ob.getUserOrderIDs(accounts[0])[0] + assert(ob.prelimCheck.call(orderID) == True) + + balanceBefore = WMATIC.balanceOf(accounts[0]) + #execute order + ob.executeOrder(orderID, {'from':accounts[0]}) + assert(WMATIC.balanceOf(accounts[0]) > balanceBefore) + assert(IBZX.getUserLoansCount(accounts[0], False) == 0) + +#test case 4. place limit open order and execute as well as market stop w/ oracle and min amount out +def test_case4(ORDERBOOK, DEPOSITS, USDC, IUSDC, ETH, WMATIC, IBZX): + ob = ORDERBOOK + deposits = DEPOSITS + + #get some USDC + USDC.transfer(accounts[0], 10000e6, {'from':"0xf977814e90da44bfa03b6295a0616a897441acec"}) + USDC.approve(deposits, 10000e6, {'from':accounts[0]}) + + #deposit gas token + WMATIC.approve(deposits, 100e18, {"from":accounts[0]}) + ob.depositGasFeeToken(0, {'from':accounts[0], 'value':100e18}) + + #place open order + setManager = encode_abi(['address'],[ob.address]) + dex_payload = encode_abi(['address','address'],[USDC.address,ETH.address]) + dex_selector = encode_abi(['uint256','bytes'],[1,dex_payload]) + loanDataBytes = encode_abi(['uint256','bytes[]'],[6,(dex_selector,setManager)]) + ob.placeOrder([0, 0, 1e6, 1e18, int(100e6), 0, accounts[0], + IUSDC, USDC, ETH, 0, 0, 1000000000000, loanDataBytes], {'from': accounts[0]}) + assert(ob.getUserOrdersCount(accounts[0]) == 1) + assert(ob.getGlobalOrdersCount() > 1) + orderID = ob.getUserOrderIDs(accounts[0])[0] + assert(ob.prelimCheck.call(orderID) == True) + + balanceBefore = WMATIC.balanceOf(accounts[0]) + #execute order + ob.executeOrder(orderID, {'from':accounts[0]}) + assert(WMATIC.balanceOf(accounts[0]) > balanceBefore) + assert(IBZX.getUserLoansCount(accounts[0], False) == 1) + + #change stop type + ob.changeStopType(True, {'from':accounts[0]}) + + #place close order + orderToClose = IBZX.getUserLoans(accounts[0],0,10,0,False,False)[0] + ob.placeOrder([orderToClose[0], 0, orderToClose[4]*10, orderToClose[4], 0, orderToClose[5], accounts[0], IUSDC, USDC, ETH, 2, 0, 1000000000000, b''], {'from': accounts[0]}) + assert(ob.getUserOrdersCount(accounts[0]) == 1) + assert(ob.getGlobalOrdersCount() > 1) + orderID = ob.getUserOrderIDs(accounts[0])[0] + assert(ob.prelimCheck.call(orderID) == True) + + balanceBefore = WMATIC.balanceOf(accounts[0]) + #execute order + ob.executeOrder(orderID, {'from':accounts[0]}) + assert(WMATIC.balanceOf(accounts[0]) > balanceBefore) + assert(IBZX.getUserLoansCount(accounts[0], False) == 0) \ No newline at end of file