diff --git a/.avalanche-cli.json b/.avalanche-cli.json new file mode 100644 index 0000000000..163cb14496 --- /dev/null +++ b/.avalanche-cli.json @@ -0,0 +1,6 @@ +{ + "node-config": { + "log-level": "info" + }, + "SingleNodeEnabled": false +} \ No newline at end of file diff --git a/.github/workflows/ci-push-image-aylin.yml b/.github/workflows/ci-push-image-aylin.yml new file mode 100644 index 0000000000..440e0f49c3 --- /dev/null +++ b/.github/workflows/ci-push-image-aylin.yml @@ -0,0 +1,54 @@ +name: Build + Push aylin image + +on: + push: + branches: + - aylin + +defaults: + run: + shell: bash + +jobs: + build_fuji_image_aylin: + name: Build Docker Image + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Get Current Tag + id: get_tag + run: echo ::set-output name=tag::$(git describe --abbrev=0 --tags) + + - name: Login to Docker hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASS }} + + - name: Build Dockerfile and Push it + run: | + TAG_END=$GITHUB_SHA + + if [ -n "$GITHUB_TAG" ]; then + TAG_END=$GITHUB_TAG + fi + + source scripts/versions.sh + + export BUILD_IMAGE_ID="${AVALANCHE_VERSION}-aylin-${TAG_END}" + + # Copy binary to the correct Fuji VM ID respository + echo "COPY --from=builder /build/jvrKsTB9MfYGnAXtxbzFYpXKceXr9J8J8ej6uWGrYM5tXswhJ /root/.avalanchego/plugins/jvrKsTB9MfYGnAXtxbzFYpXKceXr9J8J8ej6uWGrYM5tXswhJ" >> Dockerfile + + # Copy binary to the correct Mainnet VM ID respository + echo "COPY --from=builder /build/jvrKsTB9MfYGnAXtxbzFYpXKceXr9J8J8ej6uWGrYM5tXswhJ /root/.avalanchego/plugins/o1Fg94YujMqL75Ebrdkos95MTVjZpPpdeAp5ocEsp2X9c2FSz" >> Dockerfile + + ./scripts/build_image.sh + env: + CURRENT_BRANCH: ${{ github.head_ref || github.ref_name }} + PUSH_DOCKER_IMAGE: true + DOCKERHUB_REPO: hubbleexchange/hubblenet + GITHUB_TAG: ${{ steps.get_tag.outputs.tag }} + GITHUB_SHA: ${{ github.sha }} diff --git a/.github/workflows/push-image-release.yml b/.github/workflows/push-image-release.yml new file mode 100644 index 0000000000..fc0946ebc0 --- /dev/null +++ b/.github/workflows/push-image-release.yml @@ -0,0 +1,85 @@ +name: Build + Push release image + +on: + workflow_dispatch: + inputs: + release_tag: + description: 'Release tag' + required: true + type: string + +defaults: + run: + shell: bash + +jobs: + build_release_image: + name: Build Docker Image + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Login to Docker hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASS }} + + - name: Create the Dockerfile + env: + HUBBLENET_RELEASE_TAG: ${{ inputs.release_tag }} + AVALANCHE_VERSION: ${{ vars.AVALANCHE_VERSION }} + run: | + if [ "${HUBBLENET_RELEASE_TAG:0:1}" = "v" ]; then + HUBBLENET_VERSION="${HUBBLENET_RELEASE_TAG:1}"; + HUBBLENET_RELEASE_TAG="${HUBBLENET_RELEASE_TAG}"; + else + HUBBLENET_VERSION="${HUBBLENET_RELEASE_TAG}"; + fi + + multiline_text=$(cat < Dockerfile-release + cat Dockerfile-release + + - name: Build and push release image for the mainnet + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile-release + push: true + tags: "hubbleexchange/hubblenet:${{ vars.AVALANCHE_VERSION }}-${{ inputs.release_tag }}" + build-args: | + VM_ID=o1Fg94YujMqL75Ebrdkos95MTVjZpPpdeAp5ocEsp2X9c2FSz + + + - name: Build and push release image for the fuji + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile-release + push: true + tags: "hubbleexchange/hubblenet:${{ vars.AVALANCHE_VERSION }}-fuji-${{ inputs.release_tag }}" + build-args: | + VM_ID=jvrKsTB9MfYGnAXtxbzFYpXKceXr9J8J8ej6uWGrYM5tXswhJ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 48ce7c6109..93caac732d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,6 +10,10 @@ on: tags: - "*" +permissions: + contents: write + packages: write + jobs: release: # needs: [lint_test, unit_test, e2e_test, simulator_test] @@ -19,7 +23,7 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 - path: subnet-evm + path: hubblenet - name: Set up Go uses: actions/setup-go@v5 with: @@ -48,7 +52,7 @@ jobs: distribution: goreleaser version: latest args: release --clean - workdir: ./subnet-evm/ + workdir: ./hubblenet/ env: # https://docs.github.com/en/actions/security-guides/automatic-token-authentication#about-the-github_token-secret GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000000..8ab8039bae --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,30 @@ +name: Build + test + release + +on: + push: + branches: + - '*' + + tags: + - "*" + # pull_request: + +jobs: + unit_test: + name: Golang Unit Tests v${{ matrix.go }} (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + matrix: + go: ["1.22.0"] + os: [ubuntu-22.04] + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v3 + with: + go-version: ${{ matrix.go }} + - run: go mod download + shell: bash + - run: go test github.com/ava-labs/subnet-evm/plugin/evm/... + shell: bash + - run: go test github.com/ava-labs/subnet-evm/precompile/... + shell: bash diff --git a/.gitignore b/.gitignore index 65fe98d2e3..8d6f0ebd91 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,10 @@ cmd/simulator/simulator # goreleaser dist/ + +# orderbook tests dependencies +tests/orderbook/node_modules + +*.bin +local_status.sh +networks/*/*.env diff --git a/.goreleaser.yml b/.goreleaser.yml index bec5952578..155182d1b6 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,8 +1,8 @@ # ref. https://goreleaser.com/customization/build/ builds: - - id: subnet-evm + - id: hubblenet main: ./plugin - binary: subnet-evm + binary: hubblenet-{{.Version}} flags: - -v ldflags: -X github.com/ava-labs/subnet-evm/plugin/evm.Version=v{{.Version}} @@ -33,5 +33,5 @@ release: # Repo in which the release will be created. # Default is extracted from the origin remote URL or empty if its private hosted. github: - owner: ava-labs - name: subnet-evm + owner: hubble-exchange + name: hubblenet diff --git a/Dockerfile b/Dockerfile index 1b47e883db..5b399200d7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,10 +23,10 @@ COPY . . ARG SUBNET_EVM_COMMIT ARG CURRENT_BRANCH -RUN export SUBNET_EVM_COMMIT=$SUBNET_EVM_COMMIT && export CURRENT_BRANCH=$CURRENT_BRANCH && ./scripts/build.sh /build/srEXiWaHuhNyGwPUi444Tu47ZEDwxTWrbQiuD7FmgSAQ6X7Dy +RUN export SUBNET_EVM_COMMIT=$SUBNET_EVM_COMMIT && export CURRENT_BRANCH=$CURRENT_BRANCH && ./scripts/build.sh /build/jvrKsTB9MfYGnAXtxbzFYpXKceXr9J8J8ej6uWGrYM5tXswhJ # ============= Cleanup Stage ================ FROM avaplatform/avalanchego:$AVALANCHE_VERSION AS builtImage # Copy the evm binary into the correct location in the container -COPY --from=builder /build/srEXiWaHuhNyGwPUi444Tu47ZEDwxTWrbQiuD7FmgSAQ6X7Dy /avalanchego/build/plugins/srEXiWaHuhNyGwPUi444Tu47ZEDwxTWrbQiuD7FmgSAQ6X7Dy +COPY --from=builder /build/jvrKsTB9MfYGnAXtxbzFYpXKceXr9J8J8ej6uWGrYM5tXswhJ /avalanchego/build/plugins/jvrKsTB9MfYGnAXtxbzFYpXKceXr9J8J8ej6uWGrYM5tXswhJ diff --git a/README.md b/README.md index f25f8ff207..a502f877fa 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,17 @@ +# Hubble v2 + +To run a fresh local network - run `./scripts/run_local.sh` +To run on the same network with updated evm code(it preserves all evm state) - run `./scripts/upgrade_local.sh` +To kill network - run `avalanche network stop && avalanche network clean` + +To see logs - run `./scripts/show_logs.sh` + +# Accounts +var userAddress1 = "0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC" +var privateKey1 = "56289e99c94b6912bfc12adc093c9b51124f0dc54ac7a766b2bc5ccf558d8027" +var userAddress2 = "0x4Cf2eD3665F6bFA95cE6A11CFDb7A2EF5FC1C7E4" +var privateKey2 = "31b571bf6894a248831ff937bb49f7754509fe93bbd2517c9c73c4144c0e97dc" + # Subnet EVM [![Build + Test + Release](https://github.com/ava-labs/subnet-evm/actions/workflows/lint-tests-release.yml/badge.svg)](https://github.com/ava-labs/subnet-evm/actions/workflows/lint-tests-release.yml) diff --git a/accounts/abi/solidity.go b/accounts/abi/solidity.go new file mode 100644 index 0000000000..64612707ae --- /dev/null +++ b/accounts/abi/solidity.go @@ -0,0 +1,143 @@ +package abi + +import ( + "encoding/json" + "strings" + + "github.com/ethereum/go-ethereum/log" +) + +type SolidityJSON struct { + ContractName string `json:"contractName"` + SourceName string `json:"sourceName"` + Abi []Abi `json:"abi"` +} + +type Abi struct { + Inputs []Input `json:"inputs"` + StateMutability string `json:"stateMutability,omitempty"` + Type string `json:"type"` + Anonymous bool `json:"anonymous,omitempty"` + Name string `json:"name,omitempty"` + Outputs []Input `json:"outputs,omitempty"` +} + +type Input struct { + Components []Input `json:"components"` + InternalType string `json:"internalType"` + Name string `json:"name"` + Type string `json:"type"` + Indexed bool `json:"indexed"` +} + +func getFunctionType(type_ string) FunctionType { + typeMap := map[string]FunctionType{ + "function": Function, + "receive": Receive, + "fallback": Fallback, + "constructor": Constructor, + } + return typeMap[type_] +} + +func FromSolidityJson(abiJsonInput string) (ABI, error) { + solidityJson := SolidityJSON{} + err := json.Unmarshal([]byte(abiJsonInput), &solidityJson) + if err != nil { + log.Error("Error in decoding ABI json", "err", err) + return ABI{}, err + } + + var constructor Method + var receive Method + var fallback Method + + methods := map[string]Method{} + events := map[string]Event{} + errors := map[string]Error{} + + for _, method := range solidityJson.Abi { + inputs := []Argument{} + for _, input := range method.Inputs { + components := []ArgumentMarshaling{} + if strings.HasPrefix(input.Type, "tuple") { // covers "tuple", "tuple[2]", "tuple[]" + for _, component := range input.Components { + components = append(components, ArgumentMarshaling{ + Name: component.Name, + Type: component.Type, + InternalType: component.InternalType, + }) + } + } + + type_, _ := NewType(input.Type, input.InternalType, components) + inputs = append(inputs, Argument{ + Name: input.Name, + Type: type_, + Indexed: input.Indexed, + }) + } + + if method.Type == "event" { + abiEvent := NewEvent(method.Name, method.Name, method.Anonymous, inputs) + events[method.Name] = abiEvent + continue + } + + if method.Type == "error" { + abiError := NewError(method.Name, inputs) + errors[method.Name] = abiError + continue + } + + outputs := []Argument{} + for _, output := range method.Outputs { + components := []ArgumentMarshaling{} + if output.Type == "tuple" || output.Type == "tuple[2]" { + for _, component := range output.Components { + components = append(components, ArgumentMarshaling{ + Name: component.Name, + Type: component.Type, + InternalType: component.InternalType, + }) + } + } + type_, _ := NewType(output.Type, output.InternalType, components) + outputs = append(outputs, Argument{ + Name: output.Name, + Type: type_, + Indexed: output.Indexed, + }) + } + + methodType := getFunctionType(method.Type) + abiMethod := NewMethod(method.Name, method.Name, methodType, method.StateMutability, false, method.StateMutability == "payable", inputs, outputs) + + // don't include the method in the list if it's a constructor + if methodType == Constructor { + constructor = abiMethod + continue + } + if methodType == Fallback { + fallback = abiMethod + continue + } + if methodType == Receive { + receive = abiMethod + continue + } + methods[method.Name] = abiMethod + } + + Abi := ABI{ + Constructor: constructor, + Methods: methods, + Events: events, + Errors: errors, + + Receive: receive, + Fallback: fallback, + } + + return Abi, nil +} diff --git a/chain.json b/chain.json new file mode 100644 index 0000000000..1a9c2111e5 --- /dev/null +++ b/chain.json @@ -0,0 +1,24 @@ +{ + "snowman-api-enabled": true, + "local-txs-enabled": true, + "priority-regossip-frequency": "1s", + "tx-regossip-max-size": 32, + "priority-regossip-max-txs": 500, + "priority-regossip-txs-per-address": 200, + "priority-regossip-addresses": [ + "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC" + ], + "validator-private-key-file": "/tmp/validator.pk", + "is-validator": true, + "trading-api-enabled": true, + "testing-api-enabled": true, + "load-from-snapshot-enabled": true, + "snapshot-file-path": "/tmp/snapshot", + "makerbook-database-path": "/tmp/makerbook", + "order-gossip-num-validators": 10, + "order-gossip-num-non-validators": 5, + "order-gossip-num-peers": 15 +} diff --git a/consensus/dummy/consensus.go b/consensus/dummy/consensus.go index 173f1e8d53..3212e8fe26 100644 --- a/consensus/dummy/consensus.go +++ b/consensus/dummy/consensus.go @@ -330,7 +330,7 @@ func (self *DummyEngine) verifyBlockFee( // by [baseFee]. if blockGas.Cmp(requiredBlockGasCost) < 0 { return fmt.Errorf( - "insufficient gas (%d) to cover the block cost (%d) at base fee (%d) (total block fee: %d)", + "BLOCK_GAS_TOO_LOW: insufficient gas (%d) to cover the block cost (%d) at base fee (%d) (total block fee: %d)", blockGas, requiredBlockGasCost, baseFee, totalBlockFee, ) } diff --git a/constants/hubble.go b/constants/hubble.go new file mode 100644 index 0000000000..7d85f94221 --- /dev/null +++ b/constants/hubble.go @@ -0,0 +1,7 @@ +package constants + +const OrderBookContractAddress = "0x03000000000000000000000000000000000000b0" +const MarginAccountContractAddress = "0x03000000000000000000000000000000000000b1" +const ClearingHouseContractAddress = "0x03000000000000000000000000000000000000b2" +const LimitOrderBookContractAddress = "0x03000000000000000000000000000000000000b3" +const IOCOrderBookContractAddress = "0x03000000000000000000000000000000000000b4" diff --git a/contracts/.gitignore b/contracts/.gitignore index f98939c1cf..956b216e1b 100644 --- a/contracts/.gitignore +++ b/contracts/.gitignore @@ -144,3 +144,5 @@ dist .pnp.* local_rpc.json + +artifacts diff --git a/contracts/contracts/hubble-v2/ClearingHouse.sol b/contracts/contracts/hubble-v2/ClearingHouse.sol new file mode 100644 index 0000000000..783cdc4ea0 --- /dev/null +++ b/contracts/contracts/hubble-v2/ClearingHouse.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity 0.8.9; + +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + +contract ClearingHouse { + using SafeCast for uint256; + using SafeCast for int256; + + uint256[12] private __gap; // slot 0-11 + int256 public numMarkets; // slot 12 + + + function getUnderlyingPrice() public pure returns(uint[] memory prices) { + prices = new uint[](1); + prices[0] = 10000000; // 10 + } +} diff --git a/contracts/contracts/hubble-v2/GenesisTUP.sol b/contracts/contracts/hubble-v2/GenesisTUP.sol new file mode 100644 index 0000000000..a37b6946a6 --- /dev/null +++ b/contracts/contracts/hubble-v2/GenesisTUP.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity 0.8.9; + +// unused import; required for a forced contract compilation +import { ProxyAdmin } from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; +import { TransparentUpgradeableProxy } from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +contract GenesisTUP is TransparentUpgradeableProxy { + // since this goes as a genesis contract, we cannot pass vars in the constructor + constructor() TransparentUpgradeableProxy(address(0), address(0), "") {} + + function setGenesisAdmin(address admin_) external { + // it is a known issue that this can be frontran, but we do not worry about it at the moment + require(_admin() == address(0), "already initialized"); + _changeAdmin(admin_); + } +} diff --git a/contracts/contracts/hubble-v2/OrderBook.sol b/contracts/contracts/hubble-v2/OrderBook.sol new file mode 100644 index 0000000000..c4048b2520 --- /dev/null +++ b/contracts/contracts/hubble-v2/OrderBook.sol @@ -0,0 +1,219 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity 0.8.9; + +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import { ECDSAUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/cryptography/ECDSAUpgradeable.sol"; +import { EIP712Upgradeable } from "@openzeppelin/contracts-upgradeable/utils/cryptography/draft-EIP712Upgradeable.sol"; + +import { IOrderBook } from "./interfaces/IOrderBook.sol"; + +contract OrderBook is IOrderBook, EIP712Upgradeable { + using SafeCast for uint256; + using SafeCast for int256; + + // keccak256("Order(uint256 ammIndex,address trader,int256 baseAssetQuantity,uint256 price,uint256 salt,bool reduceOnly)"); + bytes32 public constant ORDER_TYPEHASH = 0x0a2e4d36552888a97d5a8975ad22b04e90efe5ea0a8abb97691b63b431eb25d2; + + struct OrderInfo { + uint blockPlaced; + int256 filledAmount; + OrderStatus status; + } + mapping(bytes32 => OrderInfo) public orderInfo; + + struct Position { + int256 size; + uint256 openNotional; + } + + // following vars are used to mock clearingHouse + // ammIndex => address => Position + mapping(uint => mapping(address => Position)) public positions; + mapping(uint => uint) public lastPrices; + uint public numAmms; + + function initialize(string memory name, string memory version) initializer public { + __EIP712_init(name, version); + setNumAMMs(1); + } + + /** + * Execute matched orders + * @param orders It is required that orders[0] is a LONG and orders[1] is a short + * @param fillAmount Amount to be filled for each order. This is to support partial fills. + * Should be > 0 and min(unfilled amount in both orders) + */ + function executeMatchedOrders( + Order[2] memory orders, + int256 fillAmount + ) external + { + // Checks and Effects + require(orders[0].baseAssetQuantity > 0, "OB_order_0_is_not_long"); + require(orders[1].baseAssetQuantity < 0, "OB_order_1_is_not_short"); + require(fillAmount > 0, "OB_fillAmount_is_neg"); + require(orders[0].price /* buy */ >= orders[1].price /* sell */, "OB_orders_do_not_match"); + + bytes32 orderHash0 = getOrderHash(orders[0]); + bytes32 orderHash1 = getOrderHash(orders[1]); + // // Effects + _updateOrder(orderHash0, fillAmount, orders[0].baseAssetQuantity); + _updateOrder(orderHash1, -fillAmount, orders[1].baseAssetQuantity); + + // // Interactions + uint fulfillPrice = orders[0].price; + _openPosition(orders[0], fillAmount, fulfillPrice); + _openPosition(orders[1], -fillAmount, fulfillPrice); + + emit OrdersMatched(orderHash0, orderHash1, fillAmount.toUint256(), fulfillPrice, fillAmount.toUint256() * fulfillPrice, msg.sender, block.timestamp); + } + + /** + * @dev mocked version of clearingHouse.openPosition + */ + function _openPosition(Order memory order, int fillAmount, uint fulfillPrice) internal { + // update open notional + uint delta = abs(fillAmount).toUint256() * fulfillPrice / 1e18; + address trader = order.trader; + uint ammIndex = order.ammIndex; + require(ammIndex < numAmms, "OB_please_whitelist_new_amm"); + if (positions[ammIndex][trader].size * fillAmount >= 0) { // increase position + positions[ammIndex][trader].openNotional += delta; + } else { // reduce position + if (positions[ammIndex][trader].openNotional >= delta) { + positions[ammIndex][trader].openNotional -= delta; // position reduced + } else { // open reverse position + positions[ammIndex][trader].openNotional = (delta - positions[ammIndex][trader].openNotional); + } + } + // update position size + positions[ammIndex][trader].size += fillAmount; + // update latest price + lastPrices[ammIndex] = fulfillPrice; + } + + function placeOrder(Order memory order) external { + bytes32 orderHash = getOrderHash(order); + // order should not exist in the orderStatus map already + // require(orderInfo[orderHash].status == OrderStatus.Invalid, "OB_Order_already_exists"); + orderInfo[orderHash] = OrderInfo(block.number, 0, OrderStatus.Placed); + // @todo assert margin requirements for placing the order + // @todo min size requirement while placing order + + emit OrderAccepted(order.trader, orderHash, order, block.timestamp); + } + + function cancelOrder(Order memory order) external { + require(msg.sender == order.trader, "OB_sender_is_not_trader"); + bytes32 orderHash = getOrderHash(order); + // order status should be placed + require(orderInfo[orderHash].status == OrderStatus.Placed, "OB_Order_does_not_exist"); + orderInfo[orderHash].status = OrderStatus.Cancelled; + + emit OrderCancelled(order.trader, orderHash, block.timestamp); + } + + /** + * @dev is a no-op here but works in the implementation in the protocol repo + */ + function settleFunding() external {} + + /** + @dev assuming one order is in liquidation zone and other is out of it + @notice liquidate trader + @param trader trader to liquidate + @param order order to match when liuidating for a particular amm + @param signature signature corresponding to order + @param toLiquidate baseAsset amount being traded/liquidated. -ve if short position is being liquidated, +ve if long + */ + function liquidateAndExecuteOrder(address trader, Order memory order, bytes memory signature, uint256 toLiquidate) external { + // liquidate + positions[order.ammIndex][trader].openNotional -= (order.price * toLiquidate / 1e18); + positions[order.ammIndex][trader].size -= toLiquidate.toInt256(); + + (bytes32 orderHash,) = _verifyOrder(order, signature, toLiquidate.toInt256()); + _updateOrder(orderHash, toLiquidate.toInt256(), order.baseAssetQuantity); + _openPosition(order, toLiquidate.toInt256(), order.price); + emit LiquidationOrderMatched(trader, orderHash, signature, toLiquidate, order.price, order.price * toLiquidate, msg.sender, block.timestamp); + } + + /* ****************** */ + /* View */ + /* ****************** */ + + function getLastTradePrices() external view returns(uint[] memory lastTradePrices) { + lastTradePrices = new uint[](numAmms); + for (uint i; i < numAmms; i++) { + lastTradePrices[i] = lastPrices[i]; + } + } + + function verifySigner(Order memory order, bytes memory /* signature */) public view returns (address, bytes32) { + bytes32 orderHash = getOrderHash(order); + + // removed because verification is not required + // address signer = ECDSAUpgradeable.recover(orderHash, signature); + // OB_SINT: Signer Is Not Trader + // require(signer == order.trader, "OB_SINT"); + + return (order.trader, orderHash); + } + + function getOrderHash(Order memory order) public view returns (bytes32) { + return _hashTypedDataV4(keccak256(abi.encode(ORDER_TYPEHASH, order))); + } + + /* ****************** */ + /* Internal */ + /* ****************** */ + + function _verifyOrder(Order memory order, bytes memory signature, int256 fillAmount) + internal + view + returns (bytes32 /* orderHash */, uint /* blockPlaced */) + { + (, bytes32 orderHash) = verifySigner(order, signature); + // order should be in placed status + require(orderInfo[orderHash].status == OrderStatus.Placed, "OB_invalid_order"); + // order.baseAssetQuantity and fillAmount should have same sign + require(order.baseAssetQuantity * fillAmount > 0, "OB_fill_and_base_sign_not_match"); + // fillAmount[orderHash] should be strictly increasing or strictly decreasing + require(orderInfo[orderHash].filledAmount * fillAmount >= 0, "OB_invalid_fillAmount"); + require(abs(orderInfo[orderHash].filledAmount) <= abs(order.baseAssetQuantity), "OB_filled_amount_higher_than_order_base"); + return (orderHash, orderInfo[orderHash].blockPlaced); + } + + function _updateOrder(bytes32 orderHash, int256 fillAmount, int256 baseAssetQuantity) internal { + orderInfo[orderHash].filledAmount += fillAmount; + // update order status if filled + if (orderInfo[orderHash].filledAmount == baseAssetQuantity) { + orderInfo[orderHash].status = OrderStatus.Filled; + } + } + + /* ****************** */ + /* Pure */ + /* ****************** */ + + function abs(int x) internal pure returns (int) { + return x >= 0 ? x : -x; + } + + /* ****************** */ + /* Mocks */ + /* ****************** */ + + /** + * @dev only for testing with evm + */ + function executeTestOrder(Order memory order, bytes memory signature) external { + (bytes32 orderHash0,) = _verifyOrder(order, signature, order.baseAssetQuantity); + _updateOrder(orderHash0, order.baseAssetQuantity, order.baseAssetQuantity); + _openPosition(order, order.baseAssetQuantity, order.price); + } + + function setNumAMMs(uint _num) public { + numAmms = _num; + } +} diff --git a/contracts/contracts/hubble-v2/interfaces/IClearingHouse.sol b/contracts/contracts/hubble-v2/interfaces/IClearingHouse.sol new file mode 100644 index 0000000000..9005d608cf --- /dev/null +++ b/contracts/contracts/hubble-v2/interfaces/IClearingHouse.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +interface IClearingHouse { + enum OrderExecutionMode { + Taker, + Maker, + SameBlock, // not used + Liquidation + } + + /** + * @param ammIndex Market id to place the order. In Hubble, market ids are sequential and start from 0 + * @param trader Address of the trader + * @param mode Whether to be executed as a Maker, Taker or Liquidation + */ + struct Instruction { + uint256 ammIndex; + address trader; + bytes32 orderHash; + OrderExecutionMode mode; + } + + enum Mode { Maintenance_Margin, Min_Allowable_Margin } +} diff --git a/contracts/contracts/hubble-v2/interfaces/IHubbleBibliophile.sol b/contracts/contracts/hubble-v2/interfaces/IHubbleBibliophile.sol new file mode 100644 index 0000000000..78a9bade5a --- /dev/null +++ b/contracts/contracts/hubble-v2/interfaces/IHubbleBibliophile.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IHubbleBibliophile { + struct Order { + uint256 ammIndex; + address trader; + int256 baseAssetQuantity; + uint256 price; + uint256 salt; + bool reduceOnly; + } + + enum OrderExecutionMode { + Taker, + Maker, + SameBlock, + Liquidation + } + + function getNotionalPositionAndMargin(address trader, bool includeFundingPayments, uint8 mode) + external + view + returns(uint256 notionalPosition, int256 margin); + + function getPositionSizes(address trader) external view returns(int[] memory posSizes); + + function validateOrdersAndDetermineFillPrice( + Order[2] memory orders, + bytes32[2] memory orderHashes, + int256 fillAmount + ) external view returns(uint256 fillPrice, OrderExecutionMode mode0, OrderExecutionMode mode1); + + function validateLiquidationOrderAndDetermineFillPrice( + Order memory order, + int256 fillAmount + ) external view returns(uint256 fillPrice); +} diff --git a/contracts/contracts/hubble-v2/interfaces/IJuror.sol b/contracts/contracts/hubble-v2/interfaces/IJuror.sol new file mode 100644 index 0000000000..03330eb182 --- /dev/null +++ b/contracts/contracts/hubble-v2/interfaces/IJuror.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { IOrderHandler } from "./IOrderHandler.sol"; + +interface IJuror { + enum BadElement { Order0, Order1, Generic, NoError } + + // Order Matching + function validateOrdersAndDetermineFillPrice( + bytes[2] calldata data, + int256 fillAmount + ) external + view + returns(string memory err, BadElement element, IOrderHandler.MatchingValidationRes memory res); + + function validateLiquidationOrderAndDetermineFillPrice(bytes calldata data, uint256 liquidationAmount) + external + view + returns(string memory err, BadElement element, IOrderHandler.LiquidationMatchingValidationRes memory res); + + // Limit Orders + function validatePlaceLimitOrder(ILimitOrderBook.Order calldata order, address sender) + external + view + returns (string memory err, bytes32 orderhash, IOrderHandler.PlaceOrderRes memory res); + + function validateCancelLimitOrder(ILimitOrderBook.Order memory order, address sender, bool assertLowMargin) + external + view + returns (string memory err, bytes32 orderHash, IOrderHandler.CancelOrderRes memory res); + + // IOC Orders + function validatePlaceIOCOrder(IImmediateOrCancelOrders.Order memory order, address sender) external view returns(string memory err, bytes32 orderHash); + + // other methods + function getNotionalPositionAndMargin(address trader, bool includeFundingPayments, uint8 mode) external view returns(uint256 notionalPosition, int256 margin); +} + +interface ILimitOrderBook { + struct Order { + uint256 ammIndex; + address trader; + int256 baseAssetQuantity; + uint256 price; + uint256 salt; + bool reduceOnly; + bool postOnly; + } +} + +interface IImmediateOrCancelOrders { + struct Order { + uint8 orderType; + uint256 expireAt; + uint256 ammIndex; + address trader; + int256 baseAssetQuantity; + uint256 price; + uint256 salt; + bool reduceOnly; + } +} diff --git a/contracts/contracts/hubble-v2/interfaces/IMarginAccount.sol b/contracts/contracts/hubble-v2/interfaces/IMarginAccount.sol new file mode 100644 index 0000000000..5605fdb8ed --- /dev/null +++ b/contracts/contracts/hubble-v2/interfaces/IMarginAccount.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity 0.8.9; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +interface IMarginAccount { + struct Collateral { + IERC20 token; + uint weight; + uint8 decimals; + } + + enum LiquidationStatus { + IS_LIQUIDATABLE, + OPEN_POSITIONS, + NO_DEBT, + ABOVE_THRESHOLD + } + + function addMargin(uint idx, uint amount) external; + function addMarginFor(uint idx, uint amount, address to) external; + function removeMargin(uint idx, uint256 amount) external; + function getSpotCollateralValue(address trader) external view returns(int256 spot); + function weightedAndSpotCollateral(address trader) external view returns(int256, int256); + function getNormalizedMargin(address trader) external view returns(int256); + function realizePnL(address trader, int256 realizedPnl) external; + function isLiquidatable(address trader, bool includeFunding) external view returns(LiquidationStatus, uint, uint); + function supportedAssetsLen() external view returns(uint); + function supportedAssets() external view returns (Collateral[] memory); + function margin(uint idx, address trader) external view returns(int256); + function transferOutVusd(address recipient, uint amount) external; + function liquidateExactRepay(address trader, uint repay, uint idx, uint minSeizeAmount) external; + function oracle() external view returns(address); // interface in the protocol repo returns IOracle + function removeMarginFor(address trader, uint idx, uint256 amount) external; +} diff --git a/contracts/contracts/hubble-v2/interfaces/IOrderBook.sol b/contracts/contracts/hubble-v2/interfaces/IOrderBook.sol new file mode 100644 index 0000000000..df12652bee --- /dev/null +++ b/contracts/contracts/hubble-v2/interfaces/IOrderBook.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.0; + +interface IOrderBook { + enum OrderStatus { + Invalid, + Placed, + Filled, + Cancelled + } + + enum OrderExecutionMode { + Taker, + Maker, + SameBlock, + Liquidation + } + + struct Order { + uint256 ammIndex; + address trader; + int256 baseAssetQuantity; + uint256 price; + uint256 salt; + bool reduceOnly; + bool postOnly; + } + + struct MatchInfo { + bytes32 orderHash; + uint blockPlaced; + OrderExecutionMode mode; + } + + event OrderAccepted(address indexed trader, bytes32 indexed orderHash, Order order, uint timestamp); + event OrderCancelled(address indexed trader, bytes32 indexed orderHash, uint timestamp); + event OrdersMatched(bytes32 indexed orderHash0, bytes32 indexed orderHash1, uint256 fillAmount, uint price, uint openInterestNotional, address relayer, uint timestamp); + event LiquidationOrderMatched(address indexed trader, bytes32 indexed orderHash, bytes signature, uint256 fillAmount, uint price, uint openInterestNotional, address relayer, uint timestamp); + event OrderMatchingError(bytes32 indexed orderHash, string err); + event LiquidationError(address indexed trader, bytes32 indexed orderHash, string err, uint256 toLiquidate); + + function executeMatchedOrders(Order[2] memory orders, int256 fillAmount) external; + function settleFunding() external; + function liquidateAndExecuteOrder(address trader, Order memory order, bytes memory signature, uint256 toLiquidate) external; + function getLastTradePrices() external view returns(uint[] memory lastTradePrices); +} diff --git a/contracts/contracts/hubble-v2/interfaces/IOrderHandler.sol b/contracts/contracts/hubble-v2/interfaces/IOrderHandler.sol new file mode 100644 index 0000000000..f288338589 --- /dev/null +++ b/contracts/contracts/hubble-v2/interfaces/IOrderHandler.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.0; + +import { IClearingHouse } from "./IClearingHouse.sol"; + +interface IOrderHandler { + enum OrderStatus { + Invalid, + Placed, + Filled, + Cancelled + } + + struct PlaceOrderRes { + uint reserveAmount; + address amm; + } + + struct CancelOrderRes { + int unfilledAmount; + address amm; + } + + struct MatchingValidationRes { + IClearingHouse.Instruction[2] instructions; + uint8[2] orderTypes; + bytes[2] encodedOrders; + uint256 fillPrice; + } + + struct LiquidationMatchingValidationRes { + IClearingHouse.Instruction instruction; + uint8 orderType; + bytes encodedOrder; + uint256 fillPrice; + int256 fillAmount; + } +} diff --git a/contracts/contracts/hubble-v2/interfaces/ITicks.sol b/contracts/contracts/hubble-v2/interfaces/ITicks.sol new file mode 100644 index 0000000000..b573451ec5 --- /dev/null +++ b/contracts/contracts/hubble-v2/interfaces/ITicks.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface ITicks { + function getPrevTick(address amm, bool isBid, uint tick) external view returns (uint prevTick); + function sampleImpactBid(address amm) external view returns (uint impactBid); + function sampleImpactAsk(address amm) external view returns (uint impactAsk); + function getQuote(address amm, int256 baseAssetQuantity) external view returns (uint256 rate); + function getBaseQuote(address amm, int256 quoteQuantity) external view returns (uint256 rate); +} diff --git a/contracts/hardhat.config.ts b/contracts/hardhat.config.ts index a132a2c89a..ec3146e810 100644 --- a/contracts/hardhat.config.ts +++ b/contracts/hardhat.config.ts @@ -23,8 +23,8 @@ export default { version: "0.7.0" }, { - version: "0.8.0" - }, + version: "0.8.9" + } ] }, networks: { diff --git a/contracts/scripts/deployERC20NativeMinter.ts b/contracts/scripts/deployERC20NativeMinter.ts index c5ace6a5bd..4d4d975979 100644 --- a/contracts/scripts/deployERC20NativeMinter.ts +++ b/contracts/scripts/deployERC20NativeMinter.ts @@ -6,10 +6,9 @@ import { ethers } from "hardhat" const main = async (): Promise => { const Token: ContractFactory = await ethers.getContractFactory("ERC20NativeMinter") - const token: Contract = await Token.deploy() + const token: Contract = await Token.deploy(5000) await token.deployed() - console.log(`Token deployed to: ${token.address}`) } main() diff --git a/contracts/scripts/deployOrderBook.ts b/contracts/scripts/deployOrderBook.ts new file mode 100644 index 0000000000..4f560d6ada --- /dev/null +++ b/contracts/scripts/deployOrderBook.ts @@ -0,0 +1,22 @@ +import { + Contract, + ContractFactory + } from "ethers" + import { ethers } from "hardhat" + + const main = async (): Promise => { + const contractFactory: ContractFactory = await ethers.getContractFactory("OrderBook") + contractFactory.attach + const contract: Contract = await contractFactory.deploy('orderBook', 1) + + await contract.deployed() + console.log(`OrderBook Contract deployed to: ${contract.address}`) + } + + main() + .then(() => process.exit(0)) + .catch(error => { + console.error(error) + process.exit(1) + }) + \ No newline at end of file diff --git a/contracts/test/hubble-v2/OrderBook.ts b/contracts/test/hubble-v2/OrderBook.ts new file mode 100644 index 0000000000..6a09d81b3c --- /dev/null +++ b/contracts/test/hubble-v2/OrderBook.ts @@ -0,0 +1,181 @@ +import { expect } from "chai"; +import { ethers } from "hardhat" +import { BigNumber } from "ethers" +// import * as _ from "lodash"; + +// make sure this is always an admin for minter precompile +const adminAddress: string = "0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC" +const GENESIS_ORDERBOOK_ADDRESS = '0x03000000000000000000000000000000000000b0' + +describe.only('Order Book', function () { + let orderBook, alice, bob, longOrder, shortOrder, domain, orderType, signature + + before(async function () { + const signers = await ethers.getSigners() + ;([, alice, bob] = signers) + + console.log({alice: alice.address, bob: bob.address}) + // 1. set proxyAdmin + // const genesisTUP = await ethers.getContractAt('GenesisTUP', GENESIS_ORDERBOOK_ADDRESS) + // let _admin = await ethers.provider.getStorageAt(GENESIS_ORDERBOOK_ADDRESS, '0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103') + // // console.log({ _admin }) + // let proxyAdmin + // if (_admin == '0x' + '0'.repeat(64)) { // because we don't run a fresh subnet everytime + // const ProxyAdmin = await ethers.getContractFactory('ProxyAdmin') + // proxyAdmin = await ProxyAdmin.deploy() + // await genesisTUP.init(proxyAdmin.address) + // console.log('genesisTUP.init done...') + // await delay(2000) + // } else { + // proxyAdmin = await ethers.getContractAt('ProxyAdmin', '0x' + _admin.slice(26)) + // } + // // _admin = await ethers.provider.getStorageAt(GENESIS_ORDERBOOK_ADDRESS, '0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103') + // // console.log({ _admin }) + + // // 2. set implementation + // const OrderBook = await ethers.getContractFactory('OrderBook') + // const orderBookImpl = await OrderBook.deploy() + + // await delay(2000) + // orderBook = await ethers.getContractAt('OrderBook', GENESIS_ORDERBOOK_ADDRESS) + // let _impl = await ethers.provider.getStorageAt(GENESIS_ORDERBOOK_ADDRESS, '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc') + + // if (_impl != '0x' + '0'.repeat(64)) { + // await proxyAdmin.upgrade(GENESIS_ORDERBOOK_ADDRESS, orderBookImpl.address) + // } else { + // await proxyAdmin.upgradeAndCall( + // GENESIS_ORDERBOOK_ADDRESS, + // orderBookImpl.address, + // orderBookImpl.interface.encodeFunctionData('initialize', ['Hubble', '2.0']) + // ) + // } + // await delay(2000) + + // _impl = await ethers.provider.getStorageAt(GENESIS_ORDERBOOK_ADDRESS, '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc') + // // console.log({ _impl }) + // expect(ethers.utils.getAddress('0x' + _impl.slice(26))).to.eq(orderBookImpl.address) + }) + + it('verify signer', async function() { + + orderBook = await ethers.getContractAt('OrderBook', GENESIS_ORDERBOOK_ADDRESS) + domain = { + name: 'Hubble', + version: '2.0', + chainId: (await ethers.provider.getNetwork()).chainId, + verifyingContract: orderBook.address + } + + orderType = { + Order: [ + // field ordering must be the same as LIMIT_ORDER_TYPEHASH + { name: "trader", type: "address" }, + { name: "baseAssetQuantity", type: "int256" }, + { name: "price", type: "uint256" }, + { name: "salt", type: "uint256" }, + ] + } + shortOrder = { + trader: alice.address, + baseAssetQuantity: ethers.utils.parseEther('-5'), + price: ethers.utils.parseUnits('15', 6), + salt: Date.now() + } + + signature = await alice._signTypedData(domain, orderType, shortOrder) + const signer = (await orderBook.verifySigner(shortOrder, signature))[0] + expect(signer).to.eq(alice.address) + }) + + it('place an order', async function() { + const tx = await orderBook.placeOrder(shortOrder, signature) + await expect(tx).to.emit(orderBook, "OrderPlaced").withArgs( + shortOrder.trader, + shortOrder, + signature + ) + }) + + it('matches orders with same price and opposite base asset quantity', async function() { + // long order with same price and baseAssetQuantity + longOrder = { + trader: bob.address, + baseAssetQuantity: ethers.utils.parseEther('5'), + price: ethers.utils.parseUnits('15', 6), + salt: Date.now() + } + let signature = await bob._signTypedData(domain, orderType, longOrder) + const tx = await orderBook.placeOrder(longOrder, signature) + + await delay(6000) + + const filter = orderBook.filters + let events = await orderBook.queryFilter(filter) + console.log({events}); + + let matchedOrderEvent = events[events.length -1] + // expect(matchedOrderEvent.event).to.eq('OrderMatched') + }) + + it.skip('make lots of orders', async function() { + const signers = await ethers.getSigners() + + // long order with same price and baseAssetQuantity + longOrder = { + trader: _.sample(signers).address, + baseAssetQuantity: ethers.utils.parseEther('5'), + price: ethers.utils.parseUnits('15', 6), + salt: Date.now() + } + shortOrder = { + trader: _.sample(signers).address, + baseAssetQuantity: ethers.utils.parseEther('-5'), + price: ethers.utils.parseUnits('15', 6), + salt: Date.now() + } + let signature = await bob._signTypedData(domain, orderType, longOrder) + const tx = await orderBook.placeOrder(longOrder, signature) + + signature = await bob._signTypedData(domain, orderType, shortOrder) + const tx = await orderBook.placeOrder(longOrder, signature) + + await delay(6000) + + const filter = orderBook.filters + let events = await orderBook.queryFilter(filter) + let matchedOrderEvent = events[events.length -1] + // expect(matchedOrderEvent.event).to.eq('OrderMatched') + }) + + it.skip('matches multiple long orders with same price and opposite base asset quantity with short orders', async function() { + longOrder.salt = Date.now() + signature = await bob._signTypedData(domain, orderType, longOrder) + const longOrderTx1 = await orderBook.placeOrder(longOrder, signature) + + longOrder.salt = Date.now() + signature = await bob._signTypedData(domain, orderType, longOrder) + const longOrderTx2 = await orderBook.placeOrder(longOrder, signature) + + shortOrder.salt = Date.now() + signature = await alice._signTypedData(domain, orderType, shortOrder) + let shortOrderTx1 = await orderBook.placeOrder(shortOrder, signature) + + shortOrder.salt = Date.now() + signature = await alice._signTypedData(domain, orderType, shortOrder) + let shortOrderTx2 = await orderBook.placeOrder(shortOrder, signature) + + // waiting for next buildblock call + await delay(6000) + const filter = orderBook.filters + let events = await orderBook.queryFilter(filter) + + expect(events[events.length - 1].event).to.eq('OrderMatched') + expect(events[events.length - 2].event).to.eq('OrderMatched') + expect(events[events.length - 3].event).to.eq('OrderPlaced') + expect(events[events.length - 4].event).to.eq('OrderPlaced') + }) +}) + +function delay(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} diff --git a/core/blockchain.go b/core/blockchain.go index 4f85ed5887..25463c7cd4 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -230,6 +230,7 @@ type BlockChain struct { chainHeadFeed event.Feed chainAcceptedFeed event.Feed logsFeed event.Feed + hubbleFeed event.Feed logsAcceptedFeed event.Feed blockProcFeed event.Feed txAcceptedFeed event.Feed @@ -1034,6 +1035,15 @@ func (bc *BlockChain) setPreference(block *types.Block) error { // the head block. Many internal aysnc processes rely on // receiving these events (i.e. the TxPool). bc.chainHeadFeed.Send(ChainHeadEvent{Block: block}) + + // when a reorg is triggered, rebirth logs for the new head are not emitted + // this can be confirmed in blockchain .reorg, where the loop for collecting rebirth logs is written as: + // for i := len(newChain) - 1; i >= 1; i-- { + // hence we emit them here + logs := bc.collectLogs(block, false) + if len(logs) > 0 { + bc.hubbleFeed.Send(logs) + } return nil } @@ -1178,6 +1188,7 @@ func (bc *BlockChain) writeCanonicalBlockWithLogs(block *types.Block, logs []*ty bc.chainFeed.Send(ChainEvent{Block: block, Hash: block.Hash(), Logs: logs}) if len(logs) > 0 { bc.logsFeed.Send(logs) + bc.hubbleFeed.Send(logs) } bc.chainHeadFeed.Send(ChainHeadEvent{Block: block}) } @@ -1551,7 +1562,7 @@ func (bc *BlockChain) reorg(oldHead *types.Header, newHead *types.Block) error { // Ensure the user sees large reorgs if len(oldChain) > 0 && len(newChain) > 0 { logFn := log.Info - msg := "Resetting chain preference" + msg := "#### Resetting chain preference" if len(oldChain) > 63 { msg = "Large chain preference change detected" logFn = log.Warn @@ -1602,11 +1613,13 @@ func (bc *BlockChain) reorg(oldHead *types.Header, newHead *types.Block) error { } if len(deletedLogs) > 512 { bc.rmLogsFeed.Send(RemovedLogsEvent{deletedLogs}) + bc.hubbleFeed.Send(deletedLogs) deletedLogs = nil } } if len(deletedLogs) > 0 { bc.rmLogsFeed.Send(RemovedLogsEvent{deletedLogs}) + bc.hubbleFeed.Send(deletedLogs) } // New logs: @@ -1617,11 +1630,13 @@ func (bc *BlockChain) reorg(oldHead *types.Header, newHead *types.Block) error { } if len(rebirthLogs) > 512 { bc.logsFeed.Send(rebirthLogs) + bc.hubbleFeed.Send(rebirthLogs) rebirthLogs = nil } } if len(rebirthLogs) > 0 { bc.logsFeed.Send(rebirthLogs) + bc.hubbleFeed.Send(rebirthLogs) } return nil } diff --git a/core/blockchain_reader.go b/core/blockchain_reader.go index 0dc3d20f44..39c0686015 100644 --- a/core/blockchain_reader.go +++ b/core/blockchain_reader.go @@ -323,6 +323,11 @@ func (bc *BlockChain) SubscribeLogsEvent(ch chan<- []*types.Log) event.Subscript return bc.scope.Track(bc.logsFeed.Subscribe(ch)) } +// SubscribeHubbleLogsEvent registers a subscription of []*types.Log. +func (bc *BlockChain) SubscribeHubbleLogsEvent(ch chan<- []*types.Log) event.Subscription { + return bc.scope.Track(bc.hubbleFeed.Subscribe(ch)) +} + // SubscribeBlockProcessingEvent registers a subscription of bool where true means // block processing has started while false means it has stopped. func (bc *BlockChain) SubscribeBlockProcessingEvent(ch chan<- bool) event.Subscription { diff --git a/core/txpool/txpool.go b/core/txpool/txpool.go index 20f11ddc39..1f33febc0a 100644 --- a/core/txpool/txpool.go +++ b/core/txpool/txpool.go @@ -38,6 +38,7 @@ import ( "github.com/ava-labs/subnet-evm/commontype" "github.com/ava-labs/subnet-evm/consensus/dummy" + "github.com/ava-labs/subnet-evm/constants" "github.com/ava-labs/subnet-evm/core" "github.com/ava-labs/subnet-evm/core/state" "github.com/ava-labs/subnet-evm/core/types" @@ -295,11 +296,12 @@ type TxPool struct { locals *accountSet // Set of local transaction to exempt from eviction rules journal *journal // Journal of local transaction to back up to disk - pending map[common.Address]*list // All currently processable transactions - queue map[common.Address]*list // Queued but non-processable transactions - beats map[common.Address]time.Time // Last heartbeat from each known account - all *lookup // All transactions to allow lookups - priced *pricedList // All transactions sorted by price + orderBookTxs OrderBookTxs + pending map[common.Address]*list // All currently processable transactions + queue map[common.Address]*list // Queued but non-processable transactions + beats map[common.Address]time.Time // Last heartbeat from each known account + all *lookup // All transactions to allow lookups + priced *pricedList // All transactions sorted by price chainHeadCh chan core.ChainHeadEvent chainHeadSub event.Subscription @@ -316,6 +318,11 @@ type TxPool struct { changesSinceReorg int // A counter for how many drops we've performed in-between reorg. } +type OrderBookTxs struct { + txs map[common.Address]*list + blockNumber uint64 // the intended block number for these txs +} + type txpoolResetRequest struct { oldHead, newHead *types.Header } @@ -326,12 +333,17 @@ func NewTxPool(config Config, chainconfig *params.ChainConfig, chain blockChain) // Sanitize the input to ensure no vulnerable gas prices are set config = (&config).sanitize() + orderBookTxs := OrderBookTxs{ + txs: make(map[common.Address]*list), + } + // Create the transaction pool with its initial settings pool := &TxPool{ config: config, chainconfig: chainconfig, chain: chain, signer: types.LatestSigner(chainconfig), + orderBookTxs: orderBookTxs, pending: make(map[common.Address]*list), queue: make(map[common.Address]*list), beats: make(map[common.Address]time.Time), @@ -1092,6 +1104,82 @@ func (pool *TxPool) promoteTx(addr common.Address, hash common.Hash, tx *types.T return true } +func (pool *TxPool) GetOrderBookTxs() map[common.Address]types.Transactions { + pool.mu.RLock() + defer pool.mu.RUnlock() + + txs := map[common.Address]types.Transactions{} + for from, txList := range pool.orderBookTxs.txs { + txs[from] = txList.Flatten() + } + return txs +} + +func (pool *TxPool) GetOrderBookTxsCount() uint64 { + pool.mu.RLock() + defer pool.mu.RUnlock() + + count := 0 + for _, txList := range pool.orderBookTxs.txs { + count += txList.Len() + } + return uint64(count) +} + +func (pool *TxPool) PurgeOrderBookTxs() { + pool.mu.Lock() + defer pool.mu.Unlock() + + for from, _ := range pool.orderBookTxs.txs { + delete(pool.orderBookTxs.txs, from) + } + + pool.orderBookTxs.blockNumber = 0 +} + +func (pool *TxPool) GetOrderBookTxNonce(address common.Address) uint64 { + nonce := pool.Nonce(address) + val, ok := pool.orderBookTxs.txs[address] + if ok { + return nonce + uint64(val.Len()) + } + return nonce +} + +func (pool *TxPool) AddOrderBookTx(tx *types.Transaction) error { + pool.mu.Lock() + defer pool.mu.Unlock() + + if from, err := types.Sender(pool.signer, tx); err == nil { + val, ok := pool.orderBookTxs.txs[from] + if !ok { + val = newList(false) + pool.orderBookTxs.txs[from] = val + } + ok, _ = val.Add(tx, 0) + if !ok { + return errors.New("error adding tx to orderbookQueue") + } + } else { + return fmt.Errorf("AddOrderBookTx: error getting sender: %w", err) + } + return nil +} + +func (pool *TxPool) SetOrderBookTxsBlockNumber(blockNumber uint64) { + pool.mu.Lock() + defer pool.mu.Unlock() + + pool.orderBookTxs.blockNumber = blockNumber +} + +func (pool *TxPool) GetOrderBookTxsBlockNumber() uint64 { + pool.mu.RLock() + defer pool.mu.RUnlock() + + return pool.orderBookTxs.blockNumber +} + // AddLocals enqueues a batch of transactions into the pool if they are valid, marking the // senders as a local ones, ensuring they go around the local pricing constraints. // @@ -1552,7 +1640,13 @@ func (pool *TxPool) reset(oldHead, newHead *types.Header) { return } } - reinject = types.TxDifference(discarded, included) + difference := types.TxDifference(discarded, included) + for _, tx := range difference { + log.Info("txpool", "to", tx.To(), "orderbook", constants.OrderBookContractAddress, "clearing", constants.ClearingHouseContractAddress) + if tx.To() == nil || (tx.To().Hex() != constants.OrderBookContractAddress && tx.To().Hex() != constants.ClearingHouseContractAddress) { + reinject = append(reinject, tx) + } + } } } } diff --git a/core/txpool/txpool_test.go b/core/txpool/txpool_test.go index 0e4d438f35..11bb991ac7 100644 --- a/core/txpool/txpool_test.go +++ b/core/txpool/txpool_test.go @@ -51,6 +51,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/event" + "github.com/stretchr/testify/assert" ) var ( @@ -2634,3 +2635,97 @@ func BenchmarkMultiAccountBatchInsert(b *testing.B) { pool.AddRemotesSync([]*types.Transaction{tx}) } } + +func TestAddOrderBookTx(t *testing.T) { + t.Run("when adding only one tx to orderbook queue", func(t *testing.T) { + pool, _ := setupTxPool() + defer pool.Stop() + + key, _ := crypto.GenerateKey() + account := crypto.PubkeyToAddress(key.PublicKey) + pool.currentState.AddBalance(account, big.NewInt(1000000)) + tx := transaction(uint64(0), 100000, key) + + pool.AddOrderBookTx(tx) + actualTxs := pool.OrderBookTxMap[account].Flatten() + assert.Equal(t, 1, actualTxs.Len()) + assert.Equal(t, tx, actualTxs[0]) + + }) + t.Run("when adding more than one tx to orderbook queue", func(t *testing.T) { + pool, _ := setupTxPool() + defer pool.Stop() + + key, _ := crypto.GenerateKey() + account := crypto.PubkeyToAddress(key.PublicKey) + tx1 := transaction(uint64(0), 100000, key) + pool.AddOrderBookTx(tx1) + tx2 := transaction(uint64(1), 100000, key) + pool.AddOrderBookTx(tx2) + actualTxs := pool.OrderBookTxMap[account].Flatten() + assert.Equal(t, 2, actualTxs.Len()) + assert.Equal(t, tx1, actualTxs[0]) + assert.Equal(t, tx2, actualTxs[1]) + }) +} + +func TestGetOrderBookTxNonce(t *testing.T) { + pool, _ := setupTxPool() + defer pool.Stop() + key, _ := crypto.GenerateKey() + account := crypto.PubkeyToAddress(key.PublicKey) + t.Run("when no tx exists in orderBookTx queue", func(t *testing.T) { + nonce := pool.GetOrderBookTxNonce(account) + assert.Equal(t, uint64(0), nonce) + }) + t.Run("when txs exists in orderBookTx queue", func(t *testing.T) { + tx1 := transaction(uint64(0), 100000, key) + pool.AddOrderBookTx(tx1) + nonce := pool.GetOrderBookTxNonce(account) + assert.Equal(t, uint64(1), nonce) + }) +} + +func TestGetOrderBookTxs(t *testing.T) { + pool, _ := setupTxPool() + defer pool.Stop() + key, _ := crypto.GenerateKey() + account := crypto.PubkeyToAddress(key.PublicKey) + + t.Run("when there are no tx in orderBookTxMap", func(t *testing.T) { + actualTxList := pool.OrderBookTxMap[account] + assert.Equal(t, nil, actualTxList) + }) + t.Run("when there are txs in orderBookTxMap", func(t *testing.T) { + tx1 := transaction(uint64(0), 100000, key) + pool.AddOrderBookTx(tx1) + tx2 := transaction(uint64(1), 100000, key) + pool.AddOrderBookTx(tx2) + actualTxs := pool.OrderBookTxMap[account].Flatten() + assert.Equal(t, types.Transactions{tx1, tx2}, actualTxs) + }) +} + +func TestPurgeOrderBookTxs(t *testing.T) { + pool, _ := setupTxPool() + defer pool.Stop() + key, _ := crypto.GenerateKey() + account := crypto.PubkeyToAddress(key.PublicKey) + + t.Run("when there is no tx for an account in orderBookTxMap", func(t *testing.T) { + txList := pool.OrderBookTxMap[account] + assert.Nil(t, txList) + }) + t.Run("when there is tx for an account in orderBookTxMap", func(t *testing.T) { + tx1 := transaction(uint64(0), 100000, key) + pool.AddOrderBookTx(tx1) + actualTxs := pool.OrderBookTxMap[account].Flatten() + assert.Equal(t, types.Transactions{tx1}, actualTxs) + + pool.PurgeOrderBookTxs() + + txList := pool.OrderBookTxMap[account] + assert.Nil(t, txList) + }) + +} diff --git a/core/types/transaction.go b/core/types/transaction.go index 34c185a7b7..0a85d34e99 100644 --- a/core/types/transaction.go +++ b/core/types/transaction.go @@ -639,6 +639,22 @@ func (t *TransactionsByPriceAndNonce) Pop() { heap.Pop(&t.heads) } +func (t *TransactionsByPriceAndNonce) Copy() *TransactionsByPriceAndNonce { + txs := make(map[common.Address]Transactions, len(t.txs)) + for acc, accTxs := range t.txs { + txs[acc] = make(Transactions, len(accTxs)) + copy(txs[acc], accTxs) + } + heads := make(TxByPriceAndTime, len(t.heads)) + copy(heads, t.heads) + return &TransactionsByPriceAndNonce{ + txs: txs, + heads: heads, + signer: t.signer, + baseFee: big.NewInt(0).Set(t.baseFee), + } +} + // copyAddressPtr copies an address. func copyAddressPtr(a *common.Address) *common.Address { if a == nil { diff --git a/eth/api.go b/eth/api.go index 993f8faf4e..55f41c3265 100644 --- a/eth/api.go +++ b/eth/api.go @@ -69,6 +69,50 @@ func (api *EthereumAPI) Coinbase() (common.Address, error) { return api.Etherbase() } +func (api *EthereumAPI) GetTransactionStatus(ctx context.Context, hash common.Hash) (map[string]interface{}, error) { + currentBlock := api.e.APIBackend.eth.blockchain.GetBlockByHash(api.e.APIBackend.CurrentBlock().Hash()) + accepted := api.e.APIBackend.eth.blockchain.GetBlockByHash(api.e.APIBackend.LastAcceptedBlock().Hash()) + + // first check if the tx is accepted + lookup := api.e.blockchain.GetTransactionLookup(hash) + if lookup != nil { + return map[string]interface{}{ + "status": "ACCEPTED", + "blockNumber": lookup.BlockIndex, + }, nil + } + + // iterate backwards from the current block to the accepted block and check if the tx is in any of the blocks + i := 0 + for { + // limit backward lookup to 128 blocks + if currentBlock.Hash() == accepted.Hash() || i >= 128 { + return map[string]interface{}{ + "status": "NOT_FOUND", + }, nil + + } + + for _, tx := range currentBlock.Transactions() { + if tx.Hash() == hash { + return map[string]interface{}{ + "status": "HEAD_BLOCK", + "blockNumber": currentBlock.NumberU64(), + }, nil + } + } + var err error + currentBlock, err = api.e.APIBackend.BlockByHash(ctx, currentBlock.ParentHash()) + if err != nil { + return map[string]interface{}{ + "status": "NOT_FOUND", + }, nil + } + + i += 1 + } +} + // AdminAPI is the collection of Ethereum full node related APIs for node // administration. type AdminAPI struct { diff --git a/eth/api_backend.go b/eth/api_backend.go index 5e660bebad..bf0594d550 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -324,6 +324,10 @@ func (b *EthAPIBackend) SubscribeLogsEvent(ch chan<- []*types.Log) event.Subscri return b.eth.BlockChain().SubscribeLogsEvent(ch) } +func (b *EthAPIBackend) SubscribeHubbleLogsEvent(ch chan<- []*types.Log) event.Subscription { + return b.eth.BlockChain().SubscribeHubbleLogsEvent(ch) +} + func (b *EthAPIBackend) SubscribeAcceptedLogsEvent(ch chan<- []*types.Log) event.Subscription { return b.eth.BlockChain().SubscribeAcceptedLogsEvent(ch) } diff --git a/eth/backend.go b/eth/backend.go index a8de24fad9..71bfae0ab4 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -335,6 +335,11 @@ func (s *Ethereum) SetEtherbase(etherbase common.Address) { s.miner.SetEtherbase(etherbase) } +func (s *Ethereum) SetOrderbookChecker(orderBookChecker miner.OrderbookChecker) { + s.miner.SetOrderbookChecker(orderBookChecker) + +} + func (s *Ethereum) Miner() *miner.Miner { return s.miner } func (s *Ethereum) AccountManager() *accounts.Manager { return s.accountManager } diff --git a/genesis.json b/genesis.json new file mode 100644 index 0000000000..35e1cb3dfa --- /dev/null +++ b/genesis.json @@ -0,0 +1,100 @@ +{ + "config": { + "chainId": 321123, + "homesteadBlock": 0, + "eip150Block": 0, + "eip150Hash": "0x2086799aeebeae135c246c65021c82b4e15a2c451340993aacfd2751886514f0", + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "petersburgBlock": 0, + "istanbulBlock": 0, + "muirGlacierBlock": 0, + "SubnetEVMTimestamp": 0, + "feeConfig": { + "gasLimit": 500000000, + "targetBlockRate": 1, + "minBaseFee": 30000000000, + "targetGas": 10000000, + "baseFeeChangeDenominator": 50, + "minBlockGasCost": 0, + "maxBlockGasCost": 5000000, + "blockGasCostStep": 0 + }, + "contractNativeMinterConfig": { + "blockTimestamp": 0, + "adminAddresses": ["0x70997970C51812dc3A010C7d01b50e0d17dc79C8","0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"] + }, + "contractDeployerAllowListConfig": { + "blockTimestamp": 0, + "adminAddresses": ["0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC","0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"] + }, + "jurorConfig": { + "blockTimestamp": 0 + }, + "jurorV2Config": { + "blockTimestamp": 0 + }, + "ticksConfig": { + "blockTimestamp": 0 + } + }, + "alloc": { + "835cE0760387BC894E91039a88A00b6a69E65D94": { + "balance": "0xD3C21BCECCEDA1000000" + }, + "8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC": { + "balance": "0xD3C21BCECCEDA1000000" + }, + "55ee05dF718f1a5C1441e76190EB1a19eE2C9430": { + "balance": "0xD3C21BCECCEDA1000000" + }, + "4Cf2eD3665F6bFA95cE6A11CFDb7A2EF5FC1C7E4": { + "balance": "0xD3C21BCECCEDA1000000" + }, + "f39Fd6e51aad88F6F4ce6aB8827279cffFb92266": { + "balance": "0xD3C21BCECCEDA1000000" + }, + "70997970C51812dc3A010C7d01b50e0d17dc79C8": { + "balance": "0xD3C21BCECCEDA1000000" + }, + "3C44CdDdB6a900fa2b585dd299e03d12FA4293BC": { + "balance": "0xD3C21BCECCEDA1000000" + }, + "0x03000000000000000000000000000000000000b0": { + "balance": "0x0", + "code": "0x6080604052600436106100695760003560e01c80635c60da1b116100435780635c60da1b146100d35780638f28397014610111578063f851a4401461013157610078565b80632c6eefd5146100805780633659cfe6146100a05780634f1ef286146100c057610078565b3661007857610076610146565b005b610076610146565b34801561008c57600080fd5b5061007661009b3660046109e4565b610160565b3480156100ac57600080fd5b506100766100bb3660046109e4565b6101f8565b6100766100ce3660046109ff565b610256565b3480156100df57600080fd5b506100e86102e1565b60405173ffffffffffffffffffffffffffffffffffffffff909116815260200160405180910390f35b34801561011d57600080fd5b5061007661012c3660046109e4565b610336565b34801561013d57600080fd5b506100e861037a565b61014e610407565b61015e6101596104f0565b6104fa565b565b600061016a61051e565b73ffffffffffffffffffffffffffffffffffffffff16146101ec576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601360248201527f616c726561647920696e697469616c697a65640000000000000000000000000060448201526064015b60405180910390fd5b6101f581610528565b50565b610200610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561024e576101f5816040518060200160405280600081525060006105c9565b6101f5610146565b61025e610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102d9576102d48383838080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250600192506105c9915050565b505050565b6102d4610146565b60006102eb610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561032b576103266104f0565b905090565b610333610146565b90565b61033e610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561024e576101f581610528565b6000610384610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561032b57610326610589565b60606103e48383604051806060016040528060278152602001610b1c602791396105f4565b9392505050565b73ffffffffffffffffffffffffffffffffffffffff163b151590565b61040f610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561015e576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152604260248201527f5472616e73706172656e745570677261646561626c6550726f78793a2061646d60448201527f696e2063616e6e6f742066616c6c6261636b20746f2070726f7879207461726760648201527f6574000000000000000000000000000000000000000000000000000000000000608482015260a4016101e3565b600061032661071c565b3660008037600080366000845af43d6000803e808015610519573d6000f35b3d6000fd5b6000610326610589565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f610551610589565b6040805173ffffffffffffffffffffffffffffffffffffffff928316815291841660208301520160405180910390a16101f581610744565b60007fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b5473ffffffffffffffffffffffffffffffffffffffff16919050565b6105d283610850565b6000825111806105df5750805b156102d4576105ee83836103bf565b50505050565b606073ffffffffffffffffffffffffffffffffffffffff84163b61069a576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f416464726573733a2064656c65676174652063616c6c20746f206e6f6e2d636f60448201527f6e7472616374000000000000000000000000000000000000000000000000000060648201526084016101e3565b6000808573ffffffffffffffffffffffffffffffffffffffff16856040516106c29190610aae565b600060405180830381855af49150503d80600081146106fd576040519150601f19603f3d011682016040523d82523d6000602084013e610702565b606091505b509150915061071282828661089d565b9695505050505050565b60007f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc6105ad565b73ffffffffffffffffffffffffffffffffffffffff81166107e7576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f455243313936373a206e65772061646d696e20697320746865207a65726f206160448201527f646472657373000000000000000000000000000000000000000000000000000060648201526084016101e3565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b80547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff9290921691909117905550565b610859816108f0565b60405173ffffffffffffffffffffffffffffffffffffffff8216907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a250565b606083156108ac5750816103e4565b8251156108bc5782518084602001fd5b816040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016101e39190610aca565b73ffffffffffffffffffffffffffffffffffffffff81163b610994576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602d60248201527f455243313936373a206e657720696d706c656d656e746174696f6e206973206e60448201527f6f74206120636f6e74726163740000000000000000000000000000000000000060648201526084016101e3565b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61080a565b803573ffffffffffffffffffffffffffffffffffffffff811681146109df57600080fd5b919050565b6000602082840312156109f657600080fd5b6103e4826109bb565b600080600060408486031215610a1457600080fd5b610a1d846109bb565b9250602084013567ffffffffffffffff80821115610a3a57600080fd5b818601915086601f830112610a4e57600080fd5b813581811115610a5d57600080fd5b876020828501011115610a6f57600080fd5b6020830194508093505050509250925092565b60005b83811015610a9d578181015183820152602001610a85565b838111156105ee5750506000910152565b60008251610ac0818460208701610a82565b9190910192915050565b6020815260008251806020840152610ae9816040850160208701610a82565b601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016919091016040019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a26469706673582212208f0bc66b8ee51a4c109376b6c4d7e171cd933a56f45ed508f31a9c3b7aa9d4eb64736f6c63430008090033" + }, + "0x03000000000000000000000000000000000000b1": { + "balance": "0x0", + "code": "0x6080604052600436106100695760003560e01c80635c60da1b116100435780635c60da1b146100d35780638f28397014610111578063f851a4401461013157610078565b80632c6eefd5146100805780633659cfe6146100a05780634f1ef286146100c057610078565b3661007857610076610146565b005b610076610146565b34801561008c57600080fd5b5061007661009b3660046109e4565b610160565b3480156100ac57600080fd5b506100766100bb3660046109e4565b6101f8565b6100766100ce3660046109ff565b610256565b3480156100df57600080fd5b506100e86102e1565b60405173ffffffffffffffffffffffffffffffffffffffff909116815260200160405180910390f35b34801561011d57600080fd5b5061007661012c3660046109e4565b610336565b34801561013d57600080fd5b506100e861037a565b61014e610407565b61015e6101596104f0565b6104fa565b565b600061016a61051e565b73ffffffffffffffffffffffffffffffffffffffff16146101ec576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601360248201527f616c726561647920696e697469616c697a65640000000000000000000000000060448201526064015b60405180910390fd5b6101f581610528565b50565b610200610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561024e576101f5816040518060200160405280600081525060006105c9565b6101f5610146565b61025e610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102d9576102d48383838080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250600192506105c9915050565b505050565b6102d4610146565b60006102eb610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561032b576103266104f0565b905090565b610333610146565b90565b61033e610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561024e576101f581610528565b6000610384610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561032b57610326610589565b60606103e48383604051806060016040528060278152602001610b1c602791396105f4565b9392505050565b73ffffffffffffffffffffffffffffffffffffffff163b151590565b61040f610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561015e576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152604260248201527f5472616e73706172656e745570677261646561626c6550726f78793a2061646d60448201527f696e2063616e6e6f742066616c6c6261636b20746f2070726f7879207461726760648201527f6574000000000000000000000000000000000000000000000000000000000000608482015260a4016101e3565b600061032661071c565b3660008037600080366000845af43d6000803e808015610519573d6000f35b3d6000fd5b6000610326610589565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f610551610589565b6040805173ffffffffffffffffffffffffffffffffffffffff928316815291841660208301520160405180910390a16101f581610744565b60007fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b5473ffffffffffffffffffffffffffffffffffffffff16919050565b6105d283610850565b6000825111806105df5750805b156102d4576105ee83836103bf565b50505050565b606073ffffffffffffffffffffffffffffffffffffffff84163b61069a576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f416464726573733a2064656c65676174652063616c6c20746f206e6f6e2d636f60448201527f6e7472616374000000000000000000000000000000000000000000000000000060648201526084016101e3565b6000808573ffffffffffffffffffffffffffffffffffffffff16856040516106c29190610aae565b600060405180830381855af49150503d80600081146106fd576040519150601f19603f3d011682016040523d82523d6000602084013e610702565b606091505b509150915061071282828661089d565b9695505050505050565b60007f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc6105ad565b73ffffffffffffffffffffffffffffffffffffffff81166107e7576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f455243313936373a206e65772061646d696e20697320746865207a65726f206160448201527f646472657373000000000000000000000000000000000000000000000000000060648201526084016101e3565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b80547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff9290921691909117905550565b610859816108f0565b60405173ffffffffffffffffffffffffffffffffffffffff8216907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a250565b606083156108ac5750816103e4565b8251156108bc5782518084602001fd5b816040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016101e39190610aca565b73ffffffffffffffffffffffffffffffffffffffff81163b610994576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602d60248201527f455243313936373a206e657720696d706c656d656e746174696f6e206973206e60448201527f6f74206120636f6e74726163740000000000000000000000000000000000000060648201526084016101e3565b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61080a565b803573ffffffffffffffffffffffffffffffffffffffff811681146109df57600080fd5b919050565b6000602082840312156109f657600080fd5b6103e4826109bb565b600080600060408486031215610a1457600080fd5b610a1d846109bb565b9250602084013567ffffffffffffffff80821115610a3a57600080fd5b818601915086601f830112610a4e57600080fd5b813581811115610a5d57600080fd5b876020828501011115610a6f57600080fd5b6020830194508093505050509250925092565b60005b83811015610a9d578181015183820152602001610a85565b838111156105ee5750506000910152565b60008251610ac0818460208701610a82565b9190910192915050565b6020815260008251806020840152610ae9816040850160208701610a82565b601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016919091016040019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a26469706673582212208f0bc66b8ee51a4c109376b6c4d7e171cd933a56f45ed508f31a9c3b7aa9d4eb64736f6c63430008090033" + }, + "0x03000000000000000000000000000000000000b2": { + "balance": "0x0", + "code": "0x6080604052600436106100695760003560e01c80635c60da1b116100435780635c60da1b146100d35780638f28397014610111578063f851a4401461013157610078565b80632c6eefd5146100805780633659cfe6146100a05780634f1ef286146100c057610078565b3661007857610076610146565b005b610076610146565b34801561008c57600080fd5b5061007661009b3660046109e4565b610160565b3480156100ac57600080fd5b506100766100bb3660046109e4565b6101f8565b6100766100ce3660046109ff565b610256565b3480156100df57600080fd5b506100e86102e1565b60405173ffffffffffffffffffffffffffffffffffffffff909116815260200160405180910390f35b34801561011d57600080fd5b5061007661012c3660046109e4565b610336565b34801561013d57600080fd5b506100e861037a565b61014e610407565b61015e6101596104f0565b6104fa565b565b600061016a61051e565b73ffffffffffffffffffffffffffffffffffffffff16146101ec576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601360248201527f616c726561647920696e697469616c697a65640000000000000000000000000060448201526064015b60405180910390fd5b6101f581610528565b50565b610200610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561024e576101f5816040518060200160405280600081525060006105c9565b6101f5610146565b61025e610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102d9576102d48383838080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250600192506105c9915050565b505050565b6102d4610146565b60006102eb610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561032b576103266104f0565b905090565b610333610146565b90565b61033e610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561024e576101f581610528565b6000610384610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561032b57610326610589565b60606103e48383604051806060016040528060278152602001610b1c602791396105f4565b9392505050565b73ffffffffffffffffffffffffffffffffffffffff163b151590565b61040f610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561015e576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152604260248201527f5472616e73706172656e745570677261646561626c6550726f78793a2061646d60448201527f696e2063616e6e6f742066616c6c6261636b20746f2070726f7879207461726760648201527f6574000000000000000000000000000000000000000000000000000000000000608482015260a4016101e3565b600061032661071c565b3660008037600080366000845af43d6000803e808015610519573d6000f35b3d6000fd5b6000610326610589565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f610551610589565b6040805173ffffffffffffffffffffffffffffffffffffffff928316815291841660208301520160405180910390a16101f581610744565b60007fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b5473ffffffffffffffffffffffffffffffffffffffff16919050565b6105d283610850565b6000825111806105df5750805b156102d4576105ee83836103bf565b50505050565b606073ffffffffffffffffffffffffffffffffffffffff84163b61069a576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f416464726573733a2064656c65676174652063616c6c20746f206e6f6e2d636f60448201527f6e7472616374000000000000000000000000000000000000000000000000000060648201526084016101e3565b6000808573ffffffffffffffffffffffffffffffffffffffff16856040516106c29190610aae565b600060405180830381855af49150503d80600081146106fd576040519150601f19603f3d011682016040523d82523d6000602084013e610702565b606091505b509150915061071282828661089d565b9695505050505050565b60007f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc6105ad565b73ffffffffffffffffffffffffffffffffffffffff81166107e7576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f455243313936373a206e65772061646d696e20697320746865207a65726f206160448201527f646472657373000000000000000000000000000000000000000000000000000060648201526084016101e3565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b80547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff9290921691909117905550565b610859816108f0565b60405173ffffffffffffffffffffffffffffffffffffffff8216907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a250565b606083156108ac5750816103e4565b8251156108bc5782518084602001fd5b816040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016101e39190610aca565b73ffffffffffffffffffffffffffffffffffffffff81163b610994576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602d60248201527f455243313936373a206e657720696d706c656d656e746174696f6e206973206e60448201527f6f74206120636f6e74726163740000000000000000000000000000000000000060648201526084016101e3565b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61080a565b803573ffffffffffffffffffffffffffffffffffffffff811681146109df57600080fd5b919050565b6000602082840312156109f657600080fd5b6103e4826109bb565b600080600060408486031215610a1457600080fd5b610a1d846109bb565b9250602084013567ffffffffffffffff80821115610a3a57600080fd5b818601915086601f830112610a4e57600080fd5b813581811115610a5d57600080fd5b876020828501011115610a6f57600080fd5b6020830194508093505050509250925092565b60005b83811015610a9d578181015183820152602001610a85565b838111156105ee5750506000910152565b60008251610ac0818460208701610a82565b9190910192915050565b6020815260008251806020840152610ae9816040850160208701610a82565b601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016919091016040019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a26469706673582212208f0bc66b8ee51a4c109376b6c4d7e171cd933a56f45ed508f31a9c3b7aa9d4eb64736f6c63430008090033" + }, + "0x03000000000000000000000000000000000000b3": { + "balance": "0x0", + "code": "0x6080604052600436106100695760003560e01c80635c60da1b116100435780635c60da1b146100d35780638f28397014610111578063f851a4401461013157610078565b80632c6eefd5146100805780633659cfe6146100a05780634f1ef286146100c057610078565b3661007857610076610146565b005b610076610146565b34801561008c57600080fd5b5061007661009b3660046109e4565b610160565b3480156100ac57600080fd5b506100766100bb3660046109e4565b6101f8565b6100766100ce3660046109ff565b610256565b3480156100df57600080fd5b506100e86102e1565b60405173ffffffffffffffffffffffffffffffffffffffff909116815260200160405180910390f35b34801561011d57600080fd5b5061007661012c3660046109e4565b610336565b34801561013d57600080fd5b506100e861037a565b61014e610407565b61015e6101596104f0565b6104fa565b565b600061016a61051e565b73ffffffffffffffffffffffffffffffffffffffff16146101ec576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601360248201527f616c726561647920696e697469616c697a65640000000000000000000000000060448201526064015b60405180910390fd5b6101f581610528565b50565b610200610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561024e576101f5816040518060200160405280600081525060006105c9565b6101f5610146565b61025e610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102d9576102d48383838080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250600192506105c9915050565b505050565b6102d4610146565b60006102eb610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561032b576103266104f0565b905090565b610333610146565b90565b61033e610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561024e576101f581610528565b6000610384610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561032b57610326610589565b60606103e48383604051806060016040528060278152602001610b1c602791396105f4565b9392505050565b73ffffffffffffffffffffffffffffffffffffffff163b151590565b61040f610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561015e576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152604260248201527f5472616e73706172656e745570677261646561626c6550726f78793a2061646d60448201527f696e2063616e6e6f742066616c6c6261636b20746f2070726f7879207461726760648201527f6574000000000000000000000000000000000000000000000000000000000000608482015260a4016101e3565b600061032661071c565b3660008037600080366000845af43d6000803e808015610519573d6000f35b3d6000fd5b6000610326610589565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f610551610589565b6040805173ffffffffffffffffffffffffffffffffffffffff928316815291841660208301520160405180910390a16101f581610744565b60007fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b5473ffffffffffffffffffffffffffffffffffffffff16919050565b6105d283610850565b6000825111806105df5750805b156102d4576105ee83836103bf565b50505050565b606073ffffffffffffffffffffffffffffffffffffffff84163b61069a576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f416464726573733a2064656c65676174652063616c6c20746f206e6f6e2d636f60448201527f6e7472616374000000000000000000000000000000000000000000000000000060648201526084016101e3565b6000808573ffffffffffffffffffffffffffffffffffffffff16856040516106c29190610aae565b600060405180830381855af49150503d80600081146106fd576040519150601f19603f3d011682016040523d82523d6000602084013e610702565b606091505b509150915061071282828661089d565b9695505050505050565b60007f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc6105ad565b73ffffffffffffffffffffffffffffffffffffffff81166107e7576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f455243313936373a206e65772061646d696e20697320746865207a65726f206160448201527f646472657373000000000000000000000000000000000000000000000000000060648201526084016101e3565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b80547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff9290921691909117905550565b610859816108f0565b60405173ffffffffffffffffffffffffffffffffffffffff8216907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a250565b606083156108ac5750816103e4565b8251156108bc5782518084602001fd5b816040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016101e39190610aca565b73ffffffffffffffffffffffffffffffffffffffff81163b610994576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602d60248201527f455243313936373a206e657720696d706c656d656e746174696f6e206973206e60448201527f6f74206120636f6e74726163740000000000000000000000000000000000000060648201526084016101e3565b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61080a565b803573ffffffffffffffffffffffffffffffffffffffff811681146109df57600080fd5b919050565b6000602082840312156109f657600080fd5b6103e4826109bb565b600080600060408486031215610a1457600080fd5b610a1d846109bb565b9250602084013567ffffffffffffffff80821115610a3a57600080fd5b818601915086601f830112610a4e57600080fd5b813581811115610a5d57600080fd5b876020828501011115610a6f57600080fd5b6020830194508093505050509250925092565b60005b83811015610a9d578181015183820152602001610a85565b838111156105ee5750506000910152565b60008251610ac0818460208701610a82565b9190910192915050565b6020815260008251806020840152610ae9816040850160208701610a82565b601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016919091016040019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a26469706673582212208f0bc66b8ee51a4c109376b6c4d7e171cd933a56f45ed508f31a9c3b7aa9d4eb64736f6c63430008090033" + }, + "0x03000000000000000000000000000000000000b4": { + "balance": "0x0", + "code": "0x6080604052600436106100695760003560e01c80635c60da1b116100435780635c60da1b146100d35780638f28397014610111578063f851a4401461013157610078565b80632c6eefd5146100805780633659cfe6146100a05780634f1ef286146100c057610078565b3661007857610076610146565b005b610076610146565b34801561008c57600080fd5b5061007661009b3660046109e4565b610160565b3480156100ac57600080fd5b506100766100bb3660046109e4565b6101f8565b6100766100ce3660046109ff565b610256565b3480156100df57600080fd5b506100e86102e1565b60405173ffffffffffffffffffffffffffffffffffffffff909116815260200160405180910390f35b34801561011d57600080fd5b5061007661012c3660046109e4565b610336565b34801561013d57600080fd5b506100e861037a565b61014e610407565b61015e6101596104f0565b6104fa565b565b600061016a61051e565b73ffffffffffffffffffffffffffffffffffffffff16146101ec576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601360248201527f616c726561647920696e697469616c697a65640000000000000000000000000060448201526064015b60405180910390fd5b6101f581610528565b50565b610200610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561024e576101f5816040518060200160405280600081525060006105c9565b6101f5610146565b61025e610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102d9576102d48383838080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250600192506105c9915050565b505050565b6102d4610146565b60006102eb610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561032b576103266104f0565b905090565b610333610146565b90565b61033e610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561024e576101f581610528565b6000610384610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561032b57610326610589565b60606103e48383604051806060016040528060278152602001610b1c602791396105f4565b9392505050565b73ffffffffffffffffffffffffffffffffffffffff163b151590565b61040f610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561015e576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152604260248201527f5472616e73706172656e745570677261646561626c6550726f78793a2061646d60448201527f696e2063616e6e6f742066616c6c6261636b20746f2070726f7879207461726760648201527f6574000000000000000000000000000000000000000000000000000000000000608482015260a4016101e3565b600061032661071c565b3660008037600080366000845af43d6000803e808015610519573d6000f35b3d6000fd5b6000610326610589565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f610551610589565b6040805173ffffffffffffffffffffffffffffffffffffffff928316815291841660208301520160405180910390a16101f581610744565b60007fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b5473ffffffffffffffffffffffffffffffffffffffff16919050565b6105d283610850565b6000825111806105df5750805b156102d4576105ee83836103bf565b50505050565b606073ffffffffffffffffffffffffffffffffffffffff84163b61069a576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f416464726573733a2064656c65676174652063616c6c20746f206e6f6e2d636f60448201527f6e7472616374000000000000000000000000000000000000000000000000000060648201526084016101e3565b6000808573ffffffffffffffffffffffffffffffffffffffff16856040516106c29190610aae565b600060405180830381855af49150503d80600081146106fd576040519150601f19603f3d011682016040523d82523d6000602084013e610702565b606091505b509150915061071282828661089d565b9695505050505050565b60007f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc6105ad565b73ffffffffffffffffffffffffffffffffffffffff81166107e7576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f455243313936373a206e65772061646d696e20697320746865207a65726f206160448201527f646472657373000000000000000000000000000000000000000000000000000060648201526084016101e3565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b80547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff9290921691909117905550565b610859816108f0565b60405173ffffffffffffffffffffffffffffffffffffffff8216907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a250565b606083156108ac5750816103e4565b8251156108bc5782518084602001fd5b816040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016101e39190610aca565b73ffffffffffffffffffffffffffffffffffffffff81163b610994576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602d60248201527f455243313936373a206e657720696d706c656d656e746174696f6e206973206e60448201527f6f74206120636f6e74726163740000000000000000000000000000000000000060648201526084016101e3565b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61080a565b803573ffffffffffffffffffffffffffffffffffffffff811681146109df57600080fd5b919050565b6000602082840312156109f657600080fd5b6103e4826109bb565b600080600060408486031215610a1457600080fd5b610a1d846109bb565b9250602084013567ffffffffffffffff80821115610a3a57600080fd5b818601915086601f830112610a4e57600080fd5b813581811115610a5d57600080fd5b876020828501011115610a6f57600080fd5b6020830194508093505050509250925092565b60005b83811015610a9d578181015183820152602001610a85565b838111156105ee5750506000910152565b60008251610ac0818460208701610a82565b9190910192915050565b6020815260008251806020840152610ae9816040850160208701610a82565b601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016919091016040019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a26469706673582212208f0bc66b8ee51a4c109376b6c4d7e171cd933a56f45ed508f31a9c3b7aa9d4eb64736f6c63430008090033" + }, + "0x03000000000000000000000000000000000000b5": { + "balance": "0x0", + "code": "0x6080604052600436106100695760003560e01c80635c60da1b116100435780635c60da1b146100d35780638f28397014610111578063f851a4401461013157610078565b80632c6eefd5146100805780633659cfe6146100a05780634f1ef286146100c057610078565b3661007857610076610146565b005b610076610146565b34801561008c57600080fd5b5061007661009b3660046109e4565b610160565b3480156100ac57600080fd5b506100766100bb3660046109e4565b6101f8565b6100766100ce3660046109ff565b610256565b3480156100df57600080fd5b506100e86102e1565b60405173ffffffffffffffffffffffffffffffffffffffff909116815260200160405180910390f35b34801561011d57600080fd5b5061007661012c3660046109e4565b610336565b34801561013d57600080fd5b506100e861037a565b61014e610407565b61015e6101596104f0565b6104fa565b565b600061016a61051e565b73ffffffffffffffffffffffffffffffffffffffff16146101ec576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601360248201527f616c726561647920696e697469616c697a65640000000000000000000000000060448201526064015b60405180910390fd5b6101f581610528565b50565b610200610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561024e576101f5816040518060200160405280600081525060006105c9565b6101f5610146565b61025e610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102d9576102d48383838080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250600192506105c9915050565b505050565b6102d4610146565b60006102eb610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561032b576103266104f0565b905090565b610333610146565b90565b61033e610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561024e576101f581610528565b6000610384610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561032b57610326610589565b60606103e48383604051806060016040528060278152602001610b1c602791396105f4565b9392505050565b73ffffffffffffffffffffffffffffffffffffffff163b151590565b61040f610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561015e576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152604260248201527f5472616e73706172656e745570677261646561626c6550726f78793a2061646d60448201527f696e2063616e6e6f742066616c6c6261636b20746f2070726f7879207461726760648201527f6574000000000000000000000000000000000000000000000000000000000000608482015260a4016101e3565b600061032661071c565b3660008037600080366000845af43d6000803e808015610519573d6000f35b3d6000fd5b6000610326610589565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f610551610589565b6040805173ffffffffffffffffffffffffffffffffffffffff928316815291841660208301520160405180910390a16101f581610744565b60007fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b5473ffffffffffffffffffffffffffffffffffffffff16919050565b6105d283610850565b6000825111806105df5750805b156102d4576105ee83836103bf565b50505050565b606073ffffffffffffffffffffffffffffffffffffffff84163b61069a576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f416464726573733a2064656c65676174652063616c6c20746f206e6f6e2d636f60448201527f6e7472616374000000000000000000000000000000000000000000000000000060648201526084016101e3565b6000808573ffffffffffffffffffffffffffffffffffffffff16856040516106c29190610aae565b600060405180830381855af49150503d80600081146106fd576040519150601f19603f3d011682016040523d82523d6000602084013e610702565b606091505b509150915061071282828661089d565b9695505050505050565b60007f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc6105ad565b73ffffffffffffffffffffffffffffffffffffffff81166107e7576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f455243313936373a206e65772061646d696e20697320746865207a65726f206160448201527f646472657373000000000000000000000000000000000000000000000000000060648201526084016101e3565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b80547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff9290921691909117905550565b610859816108f0565b60405173ffffffffffffffffffffffffffffffffffffffff8216907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a250565b606083156108ac5750816103e4565b8251156108bc5782518084602001fd5b816040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016101e39190610aca565b73ffffffffffffffffffffffffffffffffffffffff81163b610994576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602d60248201527f455243313936373a206e657720696d706c656d656e746174696f6e206973206e60448201527f6f74206120636f6e74726163740000000000000000000000000000000000000060648201526084016101e3565b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61080a565b803573ffffffffffffffffffffffffffffffffffffffff811681146109df57600080fd5b919050565b6000602082840312156109f657600080fd5b6103e4826109bb565b600080600060408486031215610a1457600080fd5b610a1d846109bb565b9250602084013567ffffffffffffffff80821115610a3a57600080fd5b818601915086601f830112610a4e57600080fd5b813581811115610a5d57600080fd5b876020828501011115610a6f57600080fd5b6020830194508093505050509250925092565b60005b83811015610a9d578181015183820152602001610a85565b838111156105ee5750506000910152565b60008251610ac0818460208701610a82565b9190910192915050565b6020815260008251806020840152610ae9816040850160208701610a82565b601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016919091016040019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a26469706673582212208f0bc66b8ee51a4c109376b6c4d7e171cd933a56f45ed508f31a9c3b7aa9d4eb64736f6c63430008090033" + } + }, + "nonce": "0x0", + "timestamp": "0x0", + "extraData": "0x00", + "gasLimit": "500000000", + "difficulty": "0x0", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase": "0x0000000000000000000000000000000000000000", + "number": "0x0", + "gasUsed": "0x0", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000" +} diff --git a/go.mod b/go.mod index 4e84054be0..a8d9386afb 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/fsnotify/fsnotify v1.6.0 github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 github.com/go-cmd/cmd v1.4.1 + github.com/golang/mock v1.6.0 github.com/google/uuid v1.6.0 github.com/gorilla/rpc v1.2.0 github.com/gorilla/websocket v1.4.2 @@ -109,6 +110,7 @@ require ( github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/spf13/afero v1.8.2 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/stretchr/objx v0.5.0 // indirect github.com/subosito/gotenv v1.3.0 // indirect github.com/supranational/blst v0.3.11 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a // indirect diff --git a/go.sum b/go.sum index 40cbf87166..1ca969fe73 100644 --- a/go.sum +++ b/go.sum @@ -230,6 +230,8 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -509,6 +511,8 @@ github.com/status-im/keycard-go v0.2.0 h1:QDLFswOQu1r5jsycloeQh3bVU8n/NatHHaZobt github.com/status-im/keycard-go v0.2.0/go.mod h1:wlp8ZLbsmrF6g6WjugPAx+IzoLrkdf9+mHxBEeo3Hbg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -860,6 +864,7 @@ golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM= diff --git a/metrics/README.md b/metrics/README.md index cf153c8093..a21591efae 100644 --- a/metrics/README.md +++ b/metrics/README.md @@ -3,6 +3,8 @@ go-metrics ![travis build status](https://travis-ci.org/rcrowley/go-metrics.svg?branch=master) +Read more about what different metric types mean - https://metrics.dropwizard.io/4.2.0/getting-started.html + Go port of Coda Hale's Metrics library: . Documentation: . diff --git a/miner/miner.go b/miner/miner.go index 14e5ba8d75..f22b81b475 100644 --- a/miner/miner.go +++ b/miner/miner.go @@ -64,6 +64,10 @@ func (miner *Miner) SetEtherbase(addr common.Address) { miner.worker.setEtherbase(addr) } +func (miner *Miner) SetOrderbookChecker(orderBookChecker OrderbookChecker) { + miner.worker.setOrderbookChecker(orderBookChecker) +} + func (miner *Miner) GenerateBlock(predicateContext *precompileconfig.PredicateContext) (*types.Block, error) { return miner.worker.commitNewWork(predicateContext) } diff --git a/miner/orderbook_checker.go b/miner/orderbook_checker.go new file mode 100644 index 0000000000..a2cf56fb12 --- /dev/null +++ b/miner/orderbook_checker.go @@ -0,0 +1,14 @@ +package miner + +import ( + "math/big" + + "github.com/ava-labs/subnet-evm/core/state" + "github.com/ava-labs/subnet-evm/core/types" + "github.com/ethereum/go-ethereum/common" +) + +type OrderbookChecker interface { + GetMatchingTxs(tx *types.Transaction, stateDB *state.StateDB, blockNumber *big.Int) map[common.Address]types.Transactions + ResetMemoryDB() +} diff --git a/miner/worker.go b/miner/worker.go index a2cdaab174..ef008af38b 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -99,6 +99,8 @@ type worker struct { mu sync.RWMutex // The lock used to protect the coinbase and extra fields coinbase common.Address clock *mockable.Clock // Allows us mock the clock for testing + + orderbookChecker OrderbookChecker } func newWorker(config *Config, chainConfig *params.ChainConfig, engine consensus.Engine, eth Backend, mux *event.TypeMux, clock *mockable.Clock) *worker { @@ -123,6 +125,10 @@ func (w *worker) setEtherbase(addr common.Address) { w.coinbase = addr } +func (w *worker) setOrderbookChecker(orderBookChecker OrderbookChecker) { + w.orderbookChecker = orderBookChecker +} + // commitNewWork generates several new sealing tasks based on the parent block. func (w *worker) commitNewWork(predicateContext *precompileconfig.PredicateContext) (*types.Block, error) { w.mu.RLock() @@ -221,18 +227,46 @@ func (w *worker) commitNewWork(predicateContext *precompileconfig.PredicateConte localTxs[account] = txs } } + + orderBookTxs := w.eth.TxPool().GetOrderBookTxs() + if len(orderBookTxs) > 0 { + txs := types.NewTransactionsByPriceAndNonce(env.signer, orderBookTxs, header.BaseFee) + w.commitTransactions(env, txs, header.Coinbase) + } if len(localTxs) > 0 { txs := types.NewTransactionsByPriceAndNonce(env.signer, localTxs, header.BaseFee) + txsCopy := txs.Copy() w.commitTransactions(env, txs, header.Coinbase) + w.commitOrderbookTxs(env, txsCopy, header) } if len(remoteTxs) > 0 { txs := types.NewTransactionsByPriceAndNonce(env.signer, remoteTxs, header.BaseFee) + txsCopy := txs.Copy() w.commitTransactions(env, txs, header.Coinbase) + w.commitOrderbookTxs(env, txsCopy, header) } + w.orderbookChecker.ResetMemoryDB() + return w.commit(env) } +func (w *worker) commitOrderbookTxs(env *environment, transactions *types.TransactionsByPriceAndNonce, header *types.Header) { + for { + tx := transactions.Peek() + if tx == nil { + break + } + transactions.Pop() + + orderbookTxs := w.orderbookChecker.GetMatchingTxs(tx, env.state, header.Number) + if orderbookTxs != nil { + txsByPrice := types.NewTransactionsByPriceAndNonce(env.signer, orderbookTxs, header.BaseFee) + w.commitTransactions(env, txsByPrice, header.Coinbase) + } + } +} + func (w *worker) createCurrentEnvironment(predicateContext *precompileconfig.PredicateContext, parent *types.Header, header *types.Header, tstart time.Time) (*environment, error) { state, err := w.chain.StateAt(parent.Root) if err != nil { @@ -291,7 +325,7 @@ func (w *worker) commitTransactions(env *environment, txs *types.TransactionsByP for { // If we don't have enough gas for any further transactions then we're done. if env.gasPool.Gas() < params.TxGas { - log.Trace("Not enough gas for further transactions", "have", env.gasPool, "want", params.TxGas) + log.Info("commitTransactions - Not enough gas for further transactions", "have", env.gasPool, "want", params.TxGas) break } // Retrieve the next transaction and abort if all done. @@ -302,7 +336,7 @@ func (w *worker) commitTransactions(env *environment, txs *types.TransactionsByP // Abort transaction if it won't fit in the block and continue to search for a smaller // transction that will fit. if totalTxsSize := env.size + tx.Size(); totalTxsSize > targetTxsSize { - log.Trace("Skipping transaction that would exceed target size", "hash", tx.Hash(), "totalTxsSize", totalTxsSize, "txSize", tx.Size()) + log.Info("commitTransactions - Skipping transaction that would exceed target size", "hash", tx.Hash(), "totalTxsSize", totalTxsSize, "txSize", tx.Size()) txs.Pop() continue @@ -314,7 +348,7 @@ func (w *worker) commitTransactions(env *environment, txs *types.TransactionsByP // Check whether the tx is replay protected. If we're not in the EIP155 hf // phase, start ignoring the sender until we do. if tx.Protected() && !w.chainConfig.IsEIP155(env.header.Number) { - log.Trace("Ignoring reply protected transaction", "hash", tx.Hash(), "eip155", w.chainConfig.EIP155Block) + log.Info("commitTransactions - Ignoring reply protected transaction", "hash", tx.Hash(), "eip155", w.chainConfig.EIP155Block) txs.Pop() continue @@ -326,12 +360,13 @@ func (w *worker) commitTransactions(env *environment, txs *types.TransactionsByP switch { case errors.Is(err, core.ErrNonceTooLow): // New head notification data race between the transaction pool and miner, shift - log.Trace("Skipping transaction with low nonce", "sender", from, "nonce", tx.Nonce()) + log.Info("commitTransactions - Skipping transaction with low nonce", "sender", from, "nonce", tx.Nonce()) txs.Shift() case errors.Is(err, nil): env.tcount++ txs.Shift() + log.Info("Transaction committed", "hash", tx.Hash().String(), "nonce", tx.Nonce()) default: // Transaction is regarded as invalid, drop all consecutive transactions from diff --git a/network-configs/aylin/chain.json b/network-configs/aylin/chain.json new file mode 100644 index 0000000000..cea495e71d --- /dev/null +++ b/network-configs/aylin/chain.json @@ -0,0 +1,19 @@ +{ + "snowman-api-enabled": true, + "local-txs-enabled": true, + "priority-regossip-frequency": "1s", + "tx-regossip-max-size": 32, + "priority-regossip-max-txs": 32, + "priority-regossip-txs-per-address": 20, + "priority-regossip-addresses": [ + "0x06CCAD927e6B1d36E219Cb582Af3185D0705f78F" + ], + "validator-private-key-file": "/home/ubuntu/validator.pk", + "feeRecipient": "", + "is-validator": true, + "snapshot-file-path": "/tmp/snapshot", + "makerbook-database-path": "/tmp/makerbook", + "order-gossip-num-validators": 10, + "order-gossip-num-non-validators": 5, + "order-gossip-num-peers": 15 +} diff --git a/network-configs/aylin/chain_api_node.json b/network-configs/aylin/chain_api_node.json new file mode 100644 index 0000000000..5eb6dae9e4 --- /dev/null +++ b/network-configs/aylin/chain_api_node.json @@ -0,0 +1,32 @@ +{ + "snowman-api-enabled": true, + "local-txs-enabled": true, + "priority-regossip-frequency": "1s", + "tx-regossip-max-size": 32, + "priority-regossip-max-txs": 32, + "priority-regossip-txs-per-address": 20, + "priority-regossip-addresses": [ + "0x06CCAD927e6B1d36E219Cb582Af3185D0705f78F" + ], + "coreth-admin-api-enabled": true, + "eth-apis": [ + "public-eth", + "public-eth-filter", + "net", + "web3", + "internal-eth", + "internal-blockchain", + "internal-transaction", + "internal-debug", + "internal-tx-pool", + "internal-account", + "debug-tracer" + ], + "trading-api-enabled": true, + "testing-api-enabled": true, + "snapshot-file-path": "/tmp/snapshot", + "makerbook-database-path": "/tmp/makerbook", + "order-gossip-num-validators": 10, + "order-gossip-num-non-validators": 5, + "order-gossip-num-peers": 15 +} diff --git a/network-configs/aylin/chain_archival_node.json b/network-configs/aylin/chain_archival_node.json new file mode 100644 index 0000000000..8583aa3213 --- /dev/null +++ b/network-configs/aylin/chain_archival_node.json @@ -0,0 +1,33 @@ +{ + "pruning-enabled": false, + "snowman-api-enabled": true, + "local-txs-enabled": true, + "priority-regossip-frequency": "1s", + "tx-regossip-max-size": 32, + "priority-regossip-max-txs": 32, + "priority-regossip-txs-per-address": 20, + "priority-regossip-addresses": [ + "0x06CCAD927e6B1d36E219Cb582Af3185D0705f78F" + ], + "coreth-admin-api-enabled": true, + "eth-apis": [ + "public-eth", + "public-eth-filter", + "net", + "web3", + "internal-public-eth", + "internal-blockchain", + "internal-transaction", + "internal-debug", + "internal-tx-pool", + "internal-account", + "debug-tracer" + ], + "trading-api-enabled": true, + "testing-api-enabled": true, + "snapshot-file-path": "/tmp/snapshot", + "makerbook-database-path": "/tmp/makerbook", + "order-gossip-num-validators": 10, + "order-gossip-num-non-validators": 5, + "order-gossip-num-peers": 15 +} diff --git a/network-configs/aylin/genesis.json b/network-configs/aylin/genesis.json new file mode 100644 index 0000000000..71245e791a --- /dev/null +++ b/network-configs/aylin/genesis.json @@ -0,0 +1,84 @@ +{ + "config": { + "chainId": 486, + "homesteadBlock": 0, + "eip150Block": 0, + "eip150Hash": "0x2086799aeebeae135c246c65021c82b4e15a2c451340993aacfd2751886514f0", + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "petersburgBlock": 0, + "istanbulBlock": 0, + "muirGlacierBlock": 0, + "SubnetEVMTimestamp": 0, + "feeConfig": { + "gasLimit": 15000000, + "targetBlockRate": 1, + "minBaseFee": 10000000000, + "targetGas": 150000000, + "baseFeeChangeDenominator": 50, + "minBlockGasCost": 0, + "maxBlockGasCost": 1000000, + "blockGasCostStep": 0 + }, + "allowFeeRecipients": true, + "contractDeployerAllowListConfig": { + "blockTimestamp": 0, + "adminAddresses": ["0x06CCAD927e6B1d36E219Cb582Af3185D0705f78F"] + }, + "contractNativeMinterConfig": { + "blockTimestamp": 0, + "adminAddresses": ["0x06CCAD927e6B1d36E219Cb582Af3185D0705f78F"] + }, + "feeManagerConfig": { + "blockTimestamp": 0, + "adminAddresses": ["0x06CCAD927e6B1d36E219Cb582Af3185D0705f78F"] + }, + "rewardManagerConfig": { + "blockTimestamp": 0, + "adminAddresses": ["0x06CCAD927e6B1d36E219Cb582Af3185D0705f78F"] + }, + "jurorConfig": { + "blockTimestamp": 0 + }, + "ticksConfig": { + "blockTimestamp": 0 + } + }, + "alloc": { + "0x06CCAD927e6B1d36E219Cb582Af3185D0705f78F": { + "balance": "0x56BC75E2D63100000" + }, + "0x03000000000000000000000000000000000000b0": { + "balance": "0x0", + "code": "0x6080604052600436106100695760003560e01c80635c60da1b116100435780635c60da1b146100d35780638f28397014610111578063f851a4401461013157610078565b80632c6eefd5146100805780633659cfe6146100a05780634f1ef286146100c057610078565b3661007857610076610146565b005b610076610146565b34801561008c57600080fd5b5061007661009b3660046109e4565b610160565b3480156100ac57600080fd5b506100766100bb3660046109e4565b6101f8565b6100766100ce3660046109ff565b610256565b3480156100df57600080fd5b506100e86102e1565b60405173ffffffffffffffffffffffffffffffffffffffff909116815260200160405180910390f35b34801561011d57600080fd5b5061007661012c3660046109e4565b610336565b34801561013d57600080fd5b506100e861037a565b61014e610407565b61015e6101596104f0565b6104fa565b565b600061016a61051e565b73ffffffffffffffffffffffffffffffffffffffff16146101ec576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601360248201527f616c726561647920696e697469616c697a65640000000000000000000000000060448201526064015b60405180910390fd5b6101f581610528565b50565b610200610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561024e576101f5816040518060200160405280600081525060006105c9565b6101f5610146565b61025e610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102d9576102d48383838080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250600192506105c9915050565b505050565b6102d4610146565b60006102eb610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561032b576103266104f0565b905090565b610333610146565b90565b61033e610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561024e576101f581610528565b6000610384610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561032b57610326610589565b60606103e48383604051806060016040528060278152602001610b1c602791396105f4565b9392505050565b73ffffffffffffffffffffffffffffffffffffffff163b151590565b61040f610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561015e576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152604260248201527f5472616e73706172656e745570677261646561626c6550726f78793a2061646d60448201527f696e2063616e6e6f742066616c6c6261636b20746f2070726f7879207461726760648201527f6574000000000000000000000000000000000000000000000000000000000000608482015260a4016101e3565b600061032661071c565b3660008037600080366000845af43d6000803e808015610519573d6000f35b3d6000fd5b6000610326610589565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f610551610589565b6040805173ffffffffffffffffffffffffffffffffffffffff928316815291841660208301520160405180910390a16101f581610744565b60007fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b5473ffffffffffffffffffffffffffffffffffffffff16919050565b6105d283610850565b6000825111806105df5750805b156102d4576105ee83836103bf565b50505050565b606073ffffffffffffffffffffffffffffffffffffffff84163b61069a576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f416464726573733a2064656c65676174652063616c6c20746f206e6f6e2d636f60448201527f6e7472616374000000000000000000000000000000000000000000000000000060648201526084016101e3565b6000808573ffffffffffffffffffffffffffffffffffffffff16856040516106c29190610aae565b600060405180830381855af49150503d80600081146106fd576040519150601f19603f3d011682016040523d82523d6000602084013e610702565b606091505b509150915061071282828661089d565b9695505050505050565b60007f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc6105ad565b73ffffffffffffffffffffffffffffffffffffffff81166107e7576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f455243313936373a206e65772061646d696e20697320746865207a65726f206160448201527f646472657373000000000000000000000000000000000000000000000000000060648201526084016101e3565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b80547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff9290921691909117905550565b610859816108f0565b60405173ffffffffffffffffffffffffffffffffffffffff8216907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a250565b606083156108ac5750816103e4565b8251156108bc5782518084602001fd5b816040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016101e39190610aca565b73ffffffffffffffffffffffffffffffffffffffff81163b610994576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602d60248201527f455243313936373a206e657720696d706c656d656e746174696f6e206973206e60448201527f6f74206120636f6e74726163740000000000000000000000000000000000000060648201526084016101e3565b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61080a565b803573ffffffffffffffffffffffffffffffffffffffff811681146109df57600080fd5b919050565b6000602082840312156109f657600080fd5b6103e4826109bb565b600080600060408486031215610a1457600080fd5b610a1d846109bb565b9250602084013567ffffffffffffffff80821115610a3a57600080fd5b818601915086601f830112610a4e57600080fd5b813581811115610a5d57600080fd5b876020828501011115610a6f57600080fd5b6020830194508093505050509250925092565b60005b83811015610a9d578181015183820152602001610a85565b838111156105ee5750506000910152565b60008251610ac0818460208701610a82565b9190910192915050565b6020815260008251806020840152610ae9816040850160208701610a82565b601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016919091016040019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a26469706673582212208f0bc66b8ee51a4c109376b6c4d7e171cd933a56f45ed508f31a9c3b7aa9d4eb64736f6c63430008090033" + }, + "0x03000000000000000000000000000000000000b1": { + "balance": "0x0", + "code": "0x6080604052600436106100695760003560e01c80635c60da1b116100435780635c60da1b146100d35780638f28397014610111578063f851a4401461013157610078565b80632c6eefd5146100805780633659cfe6146100a05780634f1ef286146100c057610078565b3661007857610076610146565b005b610076610146565b34801561008c57600080fd5b5061007661009b3660046109e4565b610160565b3480156100ac57600080fd5b506100766100bb3660046109e4565b6101f8565b6100766100ce3660046109ff565b610256565b3480156100df57600080fd5b506100e86102e1565b60405173ffffffffffffffffffffffffffffffffffffffff909116815260200160405180910390f35b34801561011d57600080fd5b5061007661012c3660046109e4565b610336565b34801561013d57600080fd5b506100e861037a565b61014e610407565b61015e6101596104f0565b6104fa565b565b600061016a61051e565b73ffffffffffffffffffffffffffffffffffffffff16146101ec576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601360248201527f616c726561647920696e697469616c697a65640000000000000000000000000060448201526064015b60405180910390fd5b6101f581610528565b50565b610200610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561024e576101f5816040518060200160405280600081525060006105c9565b6101f5610146565b61025e610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102d9576102d48383838080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250600192506105c9915050565b505050565b6102d4610146565b60006102eb610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561032b576103266104f0565b905090565b610333610146565b90565b61033e610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561024e576101f581610528565b6000610384610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561032b57610326610589565b60606103e48383604051806060016040528060278152602001610b1c602791396105f4565b9392505050565b73ffffffffffffffffffffffffffffffffffffffff163b151590565b61040f610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561015e576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152604260248201527f5472616e73706172656e745570677261646561626c6550726f78793a2061646d60448201527f696e2063616e6e6f742066616c6c6261636b20746f2070726f7879207461726760648201527f6574000000000000000000000000000000000000000000000000000000000000608482015260a4016101e3565b600061032661071c565b3660008037600080366000845af43d6000803e808015610519573d6000f35b3d6000fd5b6000610326610589565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f610551610589565b6040805173ffffffffffffffffffffffffffffffffffffffff928316815291841660208301520160405180910390a16101f581610744565b60007fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b5473ffffffffffffffffffffffffffffffffffffffff16919050565b6105d283610850565b6000825111806105df5750805b156102d4576105ee83836103bf565b50505050565b606073ffffffffffffffffffffffffffffffffffffffff84163b61069a576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f416464726573733a2064656c65676174652063616c6c20746f206e6f6e2d636f60448201527f6e7472616374000000000000000000000000000000000000000000000000000060648201526084016101e3565b6000808573ffffffffffffffffffffffffffffffffffffffff16856040516106c29190610aae565b600060405180830381855af49150503d80600081146106fd576040519150601f19603f3d011682016040523d82523d6000602084013e610702565b606091505b509150915061071282828661089d565b9695505050505050565b60007f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc6105ad565b73ffffffffffffffffffffffffffffffffffffffff81166107e7576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f455243313936373a206e65772061646d696e20697320746865207a65726f206160448201527f646472657373000000000000000000000000000000000000000000000000000060648201526084016101e3565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b80547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff9290921691909117905550565b610859816108f0565b60405173ffffffffffffffffffffffffffffffffffffffff8216907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a250565b606083156108ac5750816103e4565b8251156108bc5782518084602001fd5b816040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016101e39190610aca565b73ffffffffffffffffffffffffffffffffffffffff81163b610994576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602d60248201527f455243313936373a206e657720696d706c656d656e746174696f6e206973206e60448201527f6f74206120636f6e74726163740000000000000000000000000000000000000060648201526084016101e3565b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61080a565b803573ffffffffffffffffffffffffffffffffffffffff811681146109df57600080fd5b919050565b6000602082840312156109f657600080fd5b6103e4826109bb565b600080600060408486031215610a1457600080fd5b610a1d846109bb565b9250602084013567ffffffffffffffff80821115610a3a57600080fd5b818601915086601f830112610a4e57600080fd5b813581811115610a5d57600080fd5b876020828501011115610a6f57600080fd5b6020830194508093505050509250925092565b60005b83811015610a9d578181015183820152602001610a85565b838111156105ee5750506000910152565b60008251610ac0818460208701610a82565b9190910192915050565b6020815260008251806020840152610ae9816040850160208701610a82565b601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016919091016040019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a26469706673582212208f0bc66b8ee51a4c109376b6c4d7e171cd933a56f45ed508f31a9c3b7aa9d4eb64736f6c63430008090033" + }, + "0x03000000000000000000000000000000000000b2": { + "balance": "0x0", + "code": "0x6080604052600436106100695760003560e01c80635c60da1b116100435780635c60da1b146100d35780638f28397014610111578063f851a4401461013157610078565b80632c6eefd5146100805780633659cfe6146100a05780634f1ef286146100c057610078565b3661007857610076610146565b005b610076610146565b34801561008c57600080fd5b5061007661009b3660046109e4565b610160565b3480156100ac57600080fd5b506100766100bb3660046109e4565b6101f8565b6100766100ce3660046109ff565b610256565b3480156100df57600080fd5b506100e86102e1565b60405173ffffffffffffffffffffffffffffffffffffffff909116815260200160405180910390f35b34801561011d57600080fd5b5061007661012c3660046109e4565b610336565b34801561013d57600080fd5b506100e861037a565b61014e610407565b61015e6101596104f0565b6104fa565b565b600061016a61051e565b73ffffffffffffffffffffffffffffffffffffffff16146101ec576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601360248201527f616c726561647920696e697469616c697a65640000000000000000000000000060448201526064015b60405180910390fd5b6101f581610528565b50565b610200610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561024e576101f5816040518060200160405280600081525060006105c9565b6101f5610146565b61025e610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102d9576102d48383838080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250600192506105c9915050565b505050565b6102d4610146565b60006102eb610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561032b576103266104f0565b905090565b610333610146565b90565b61033e610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561024e576101f581610528565b6000610384610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561032b57610326610589565b60606103e48383604051806060016040528060278152602001610b1c602791396105f4565b9392505050565b73ffffffffffffffffffffffffffffffffffffffff163b151590565b61040f610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561015e576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152604260248201527f5472616e73706172656e745570677261646561626c6550726f78793a2061646d60448201527f696e2063616e6e6f742066616c6c6261636b20746f2070726f7879207461726760648201527f6574000000000000000000000000000000000000000000000000000000000000608482015260a4016101e3565b600061032661071c565b3660008037600080366000845af43d6000803e808015610519573d6000f35b3d6000fd5b6000610326610589565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f610551610589565b6040805173ffffffffffffffffffffffffffffffffffffffff928316815291841660208301520160405180910390a16101f581610744565b60007fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b5473ffffffffffffffffffffffffffffffffffffffff16919050565b6105d283610850565b6000825111806105df5750805b156102d4576105ee83836103bf565b50505050565b606073ffffffffffffffffffffffffffffffffffffffff84163b61069a576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f416464726573733a2064656c65676174652063616c6c20746f206e6f6e2d636f60448201527f6e7472616374000000000000000000000000000000000000000000000000000060648201526084016101e3565b6000808573ffffffffffffffffffffffffffffffffffffffff16856040516106c29190610aae565b600060405180830381855af49150503d80600081146106fd576040519150601f19603f3d011682016040523d82523d6000602084013e610702565b606091505b509150915061071282828661089d565b9695505050505050565b60007f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc6105ad565b73ffffffffffffffffffffffffffffffffffffffff81166107e7576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f455243313936373a206e65772061646d696e20697320746865207a65726f206160448201527f646472657373000000000000000000000000000000000000000000000000000060648201526084016101e3565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b80547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff9290921691909117905550565b610859816108f0565b60405173ffffffffffffffffffffffffffffffffffffffff8216907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a250565b606083156108ac5750816103e4565b8251156108bc5782518084602001fd5b816040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016101e39190610aca565b73ffffffffffffffffffffffffffffffffffffffff81163b610994576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602d60248201527f455243313936373a206e657720696d706c656d656e746174696f6e206973206e60448201527f6f74206120636f6e74726163740000000000000000000000000000000000000060648201526084016101e3565b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61080a565b803573ffffffffffffffffffffffffffffffffffffffff811681146109df57600080fd5b919050565b6000602082840312156109f657600080fd5b6103e4826109bb565b600080600060408486031215610a1457600080fd5b610a1d846109bb565b9250602084013567ffffffffffffffff80821115610a3a57600080fd5b818601915086601f830112610a4e57600080fd5b813581811115610a5d57600080fd5b876020828501011115610a6f57600080fd5b6020830194508093505050509250925092565b60005b83811015610a9d578181015183820152602001610a85565b838111156105ee5750506000910152565b60008251610ac0818460208701610a82565b9190910192915050565b6020815260008251806020840152610ae9816040850160208701610a82565b601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016919091016040019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a26469706673582212208f0bc66b8ee51a4c109376b6c4d7e171cd933a56f45ed508f31a9c3b7aa9d4eb64736f6c63430008090033" + }, + "0x03000000000000000000000000000000000000b3": { + "balance": "0x0", + "code": "0x6080604052600436106100695760003560e01c80635c60da1b116100435780635c60da1b146100d35780638f28397014610111578063f851a4401461013157610078565b80632c6eefd5146100805780633659cfe6146100a05780634f1ef286146100c057610078565b3661007857610076610146565b005b610076610146565b34801561008c57600080fd5b5061007661009b3660046109e4565b610160565b3480156100ac57600080fd5b506100766100bb3660046109e4565b6101f8565b6100766100ce3660046109ff565b610256565b3480156100df57600080fd5b506100e86102e1565b60405173ffffffffffffffffffffffffffffffffffffffff909116815260200160405180910390f35b34801561011d57600080fd5b5061007661012c3660046109e4565b610336565b34801561013d57600080fd5b506100e861037a565b61014e610407565b61015e6101596104f0565b6104fa565b565b600061016a61051e565b73ffffffffffffffffffffffffffffffffffffffff16146101ec576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601360248201527f616c726561647920696e697469616c697a65640000000000000000000000000060448201526064015b60405180910390fd5b6101f581610528565b50565b610200610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561024e576101f5816040518060200160405280600081525060006105c9565b6101f5610146565b61025e610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102d9576102d48383838080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250600192506105c9915050565b505050565b6102d4610146565b60006102eb610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561032b576103266104f0565b905090565b610333610146565b90565b61033e610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561024e576101f581610528565b6000610384610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561032b57610326610589565b60606103e48383604051806060016040528060278152602001610b1c602791396105f4565b9392505050565b73ffffffffffffffffffffffffffffffffffffffff163b151590565b61040f610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561015e576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152604260248201527f5472616e73706172656e745570677261646561626c6550726f78793a2061646d60448201527f696e2063616e6e6f742066616c6c6261636b20746f2070726f7879207461726760648201527f6574000000000000000000000000000000000000000000000000000000000000608482015260a4016101e3565b600061032661071c565b3660008037600080366000845af43d6000803e808015610519573d6000f35b3d6000fd5b6000610326610589565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f610551610589565b6040805173ffffffffffffffffffffffffffffffffffffffff928316815291841660208301520160405180910390a16101f581610744565b60007fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b5473ffffffffffffffffffffffffffffffffffffffff16919050565b6105d283610850565b6000825111806105df5750805b156102d4576105ee83836103bf565b50505050565b606073ffffffffffffffffffffffffffffffffffffffff84163b61069a576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f416464726573733a2064656c65676174652063616c6c20746f206e6f6e2d636f60448201527f6e7472616374000000000000000000000000000000000000000000000000000060648201526084016101e3565b6000808573ffffffffffffffffffffffffffffffffffffffff16856040516106c29190610aae565b600060405180830381855af49150503d80600081146106fd576040519150601f19603f3d011682016040523d82523d6000602084013e610702565b606091505b509150915061071282828661089d565b9695505050505050565b60007f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc6105ad565b73ffffffffffffffffffffffffffffffffffffffff81166107e7576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f455243313936373a206e65772061646d696e20697320746865207a65726f206160448201527f646472657373000000000000000000000000000000000000000000000000000060648201526084016101e3565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b80547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff9290921691909117905550565b610859816108f0565b60405173ffffffffffffffffffffffffffffffffffffffff8216907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a250565b606083156108ac5750816103e4565b8251156108bc5782518084602001fd5b816040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016101e39190610aca565b73ffffffffffffffffffffffffffffffffffffffff81163b610994576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602d60248201527f455243313936373a206e657720696d706c656d656e746174696f6e206973206e60448201527f6f74206120636f6e74726163740000000000000000000000000000000000000060648201526084016101e3565b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61080a565b803573ffffffffffffffffffffffffffffffffffffffff811681146109df57600080fd5b919050565b6000602082840312156109f657600080fd5b6103e4826109bb565b600080600060408486031215610a1457600080fd5b610a1d846109bb565b9250602084013567ffffffffffffffff80821115610a3a57600080fd5b818601915086601f830112610a4e57600080fd5b813581811115610a5d57600080fd5b876020828501011115610a6f57600080fd5b6020830194508093505050509250925092565b60005b83811015610a9d578181015183820152602001610a85565b838111156105ee5750506000910152565b60008251610ac0818460208701610a82565b9190910192915050565b6020815260008251806020840152610ae9816040850160208701610a82565b601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016919091016040019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a26469706673582212208f0bc66b8ee51a4c109376b6c4d7e171cd933a56f45ed508f31a9c3b7aa9d4eb64736f6c63430008090033" + }, + "0x03000000000000000000000000000000000000b4": { + "balance": "0x0", + "code": "0x6080604052600436106100695760003560e01c80635c60da1b116100435780635c60da1b146100d35780638f28397014610111578063f851a4401461013157610078565b80632c6eefd5146100805780633659cfe6146100a05780634f1ef286146100c057610078565b3661007857610076610146565b005b610076610146565b34801561008c57600080fd5b5061007661009b3660046109e4565b610160565b3480156100ac57600080fd5b506100766100bb3660046109e4565b6101f8565b6100766100ce3660046109ff565b610256565b3480156100df57600080fd5b506100e86102e1565b60405173ffffffffffffffffffffffffffffffffffffffff909116815260200160405180910390f35b34801561011d57600080fd5b5061007661012c3660046109e4565b610336565b34801561013d57600080fd5b506100e861037a565b61014e610407565b61015e6101596104f0565b6104fa565b565b600061016a61051e565b73ffffffffffffffffffffffffffffffffffffffff16146101ec576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601360248201527f616c726561647920696e697469616c697a65640000000000000000000000000060448201526064015b60405180910390fd5b6101f581610528565b50565b610200610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561024e576101f5816040518060200160405280600081525060006105c9565b6101f5610146565b61025e610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102d9576102d48383838080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250600192506105c9915050565b505050565b6102d4610146565b60006102eb610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561032b576103266104f0565b905090565b610333610146565b90565b61033e610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561024e576101f581610528565b6000610384610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561032b57610326610589565b60606103e48383604051806060016040528060278152602001610b1c602791396105f4565b9392505050565b73ffffffffffffffffffffffffffffffffffffffff163b151590565b61040f610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561015e576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152604260248201527f5472616e73706172656e745570677261646561626c6550726f78793a2061646d60448201527f696e2063616e6e6f742066616c6c6261636b20746f2070726f7879207461726760648201527f6574000000000000000000000000000000000000000000000000000000000000608482015260a4016101e3565b600061032661071c565b3660008037600080366000845af43d6000803e808015610519573d6000f35b3d6000fd5b6000610326610589565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f610551610589565b6040805173ffffffffffffffffffffffffffffffffffffffff928316815291841660208301520160405180910390a16101f581610744565b60007fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b5473ffffffffffffffffffffffffffffffffffffffff16919050565b6105d283610850565b6000825111806105df5750805b156102d4576105ee83836103bf565b50505050565b606073ffffffffffffffffffffffffffffffffffffffff84163b61069a576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f416464726573733a2064656c65676174652063616c6c20746f206e6f6e2d636f60448201527f6e7472616374000000000000000000000000000000000000000000000000000060648201526084016101e3565b6000808573ffffffffffffffffffffffffffffffffffffffff16856040516106c29190610aae565b600060405180830381855af49150503d80600081146106fd576040519150601f19603f3d011682016040523d82523d6000602084013e610702565b606091505b509150915061071282828661089d565b9695505050505050565b60007f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc6105ad565b73ffffffffffffffffffffffffffffffffffffffff81166107e7576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f455243313936373a206e65772061646d696e20697320746865207a65726f206160448201527f646472657373000000000000000000000000000000000000000000000000000060648201526084016101e3565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b80547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff9290921691909117905550565b610859816108f0565b60405173ffffffffffffffffffffffffffffffffffffffff8216907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a250565b606083156108ac5750816103e4565b8251156108bc5782518084602001fd5b816040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016101e39190610aca565b73ffffffffffffffffffffffffffffffffffffffff81163b610994576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602d60248201527f455243313936373a206e657720696d706c656d656e746174696f6e206973206e60448201527f6f74206120636f6e74726163740000000000000000000000000000000000000060648201526084016101e3565b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61080a565b803573ffffffffffffffffffffffffffffffffffffffff811681146109df57600080fd5b919050565b6000602082840312156109f657600080fd5b6103e4826109bb565b600080600060408486031215610a1457600080fd5b610a1d846109bb565b9250602084013567ffffffffffffffff80821115610a3a57600080fd5b818601915086601f830112610a4e57600080fd5b813581811115610a5d57600080fd5b876020828501011115610a6f57600080fd5b6020830194508093505050509250925092565b60005b83811015610a9d578181015183820152602001610a85565b838111156105ee5750506000910152565b60008251610ac0818460208701610a82565b9190910192915050565b6020815260008251806020840152610ae9816040850160208701610a82565b601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016919091016040019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a26469706673582212208f0bc66b8ee51a4c109376b6c4d7e171cd933a56f45ed508f31a9c3b7aa9d4eb64736f6c63430008090033" + } + }, + "nonce": "0x0", + "timestamp": "0x0", + "extraData": "0x00", + "gasLimit": "15000000", + "difficulty": "0x0", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase": "0x0000000000000000000000000000000000000000", + "number": "0x0", + "gasUsed": "0x0", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000" +} diff --git a/network-configs/aylin/t2WSjSsoE3geV9ARu5r7gzTc5UayePy3NxDrSTx7hadLYvqbg.json b/network-configs/aylin/t2WSjSsoE3geV9ARu5r7gzTc5UayePy3NxDrSTx7hadLYvqbg.json new file mode 100644 index 0000000000..22901ef07b --- /dev/null +++ b/network-configs/aylin/t2WSjSsoE3geV9ARu5r7gzTc5UayePy3NxDrSTx7hadLYvqbg.json @@ -0,0 +1,3 @@ +{ + "proposerMinBlockDelay": 0 +} diff --git a/network-configs/aylin/upgrade.json b/network-configs/aylin/upgrade.json new file mode 100644 index 0000000000..2c3f5444d9 --- /dev/null +++ b/network-configs/aylin/upgrade.json @@ -0,0 +1,9 @@ +{ + "precompileUpgrades": [ + { + "jurorV2Config": { + "blockTimestamp": 1705583100 + } + } + ] +} diff --git a/network-configs/hubblenet/2mxZY7A2t1tuRMALW4BcBUPVGNR3LH1DXhftdbLAHm1QtDkFp8.json b/network-configs/hubblenet/2mxZY7A2t1tuRMALW4BcBUPVGNR3LH1DXhftdbLAHm1QtDkFp8.json new file mode 100644 index 0000000000..1e0ba71454 --- /dev/null +++ b/network-configs/hubblenet/2mxZY7A2t1tuRMALW4BcBUPVGNR3LH1DXhftdbLAHm1QtDkFp8.json @@ -0,0 +1,3 @@ +{ + "proposerMinBlockDelay": 0 +} diff --git a/network-configs/hubblenet/chain_api_node.json b/network-configs/hubblenet/chain_api_node.json new file mode 100644 index 0000000000..01793057e3 --- /dev/null +++ b/network-configs/hubblenet/chain_api_node.json @@ -0,0 +1,35 @@ +{ + "snowman-api-enabled": true, + "local-txs-enabled": true, + "priority-regossip-frequency": "1s", + "tx-regossip-max-size": 32, + "priority-regossip-max-txs": 32, + "priority-regossip-txs-per-address": 20, + "priority-regossip-addresses": [ + "0x8747adFCE380492ec7e9b78761Ec7C87F5Cd3d4F" + ], + "continuous-profiler-dir": "/var/avalanche/profiles/hubblenet/continuous/", + "continuous-profiler-max-files": 200, + "continuous-profiler-frequency": "10m", + "admin-api-enabled": true, + "eth-apis": [ + "public-eth", + "public-eth-filter", + "net", + "web3", + "internal-public-eth", + "internal-blockchain", + "internal-transaction", + "internal-debug", + "internal-tx-pool", + "internal-account", + "debug-tracer" + ], + "trading-api-enabled": true, + "testing-api-enabled": true, + "snapshot-file-path": "/tmp/snapshot", + "makerbook-database-path": "/tmp/makerbook", + "order-gossip-num-validators": 10, + "order-gossip-num-non-validators": 5, + "order-gossip-num-peers": 15 +} diff --git a/network-configs/hubblenet/chain_archival_node.json b/network-configs/hubblenet/chain_archival_node.json new file mode 100644 index 0000000000..2a0f853b85 --- /dev/null +++ b/network-configs/hubblenet/chain_archival_node.json @@ -0,0 +1,36 @@ +{ + "pruning-enabled": false, + "snowman-api-enabled": true, + "local-txs-enabled": true, + "priority-regossip-frequency": "1s", + "tx-regossip-max-size": 32, + "priority-regossip-max-txs": 32, + "priority-regossip-txs-per-address": 20, + "priority-regossip-addresses": [ + "0x8747adFCE380492ec7e9b78761Ec7C87F5Cd3d4F" + ], + "continuous-profiler-dir": "/var/avalanche/profiles/hubblenet/continuous/", + "continuous-profiler-max-files": 200, + "continuous-profiler-frequency": "10m", + "admin-api-enabled": true, + "eth-apis": [ + "public-eth", + "public-eth-filter", + "net", + "web3", + "internal-public-eth", + "internal-blockchain", + "internal-transaction", + "internal-debug", + "internal-tx-pool", + "internal-account", + "debug-tracer" + ], + "trading-api-enabled": true, + "testing-api-enabled": true, + "snapshot-file-path": "/tmp/snapshot", + "makerbook-database-path": "/tmp/makerbook", + "order-gossip-num-validators": 10, + "order-gossip-num-non-validators": 5, + "order-gossip-num-peers": 15 +} diff --git a/network-configs/hubblenet/chain_validator_1.json b/network-configs/hubblenet/chain_validator_1.json new file mode 100644 index 0000000000..8c159486b0 --- /dev/null +++ b/network-configs/hubblenet/chain_validator_1.json @@ -0,0 +1,21 @@ +{ + "snowman-api-enabled": true, + "local-txs-enabled": true, + "priority-regossip-frequency": "1s", + "tx-regossip-max-size": 32, + "priority-regossip-max-txs": 32, + "priority-regossip-txs-per-address": 20, + "priority-regossip-addresses": [ + "0x8747adFCE380492ec7e9b78761Ec7C87F5Cd3d4F" + ], + "continuous-profiler-dir": "/var/avalanche/profiles/hubblenet/continuous/", + "continuous-profiler-max-files": 200, + "continuous-profiler-frequency": "10m", + "validator-private-key-file": "/var/avalanche/validator.pk", + "feeRecipient": "0xa5e31FbE901362Cc93b6fdab99DB9741c673a942", + "is-validator": true, + "snapshot-file-path": "/tmp/snapshot", + "order-gossip-num-validators": 10, + "order-gossip-num-non-validators": 5, + "order-gossip-num-peers": 15 +} diff --git a/network-configs/hubblenet/genesis.json b/network-configs/hubblenet/genesis.json new file mode 100644 index 0000000000..c3a36e3084 --- /dev/null +++ b/network-configs/hubblenet/genesis.json @@ -0,0 +1,78 @@ +{ + "config": { + "chainId": 1992, + "homesteadBlock": 0, + "eip150Block": 0, + "eip150Hash": "0x2086799aeebeae135c246c65021c82b4e15a2c451340993aacfd2751886514f0", + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "petersburgBlock": 0, + "istanbulBlock": 0, + "muirGlacierBlock": 0, + "SubnetEVMTimestamp": 0, + "feeConfig": { + "gasLimit": 15000000, + "targetBlockRate": 1, + "minBaseFee": 10000000000, + "targetGas": 150000000, + "baseFeeChangeDenominator": 50, + "minBlockGasCost": 0, + "maxBlockGasCost": 1000000, + "blockGasCostStep": 0 + }, + "allowFeeRecipients": true, + "contractDeployerAllowListConfig": { + "blockTimestamp": 0, + "adminAddresses": ["0x8747adFCE380492ec7e9b78761Ec7C87F5Cd3d4F"] + }, + "contractNativeMinterConfig": { + "blockTimestamp": 0, + "adminAddresses": ["0x8747adFCE380492ec7e9b78761Ec7C87F5Cd3d4F"] + }, + "feeManagerConfig": { + "blockTimestamp": 0, + "adminAddresses": ["0x8747adFCE380492ec7e9b78761Ec7C87F5Cd3d4F"] + }, + "rewardManagerConfig": { + "blockTimestamp": 0, + "adminAddresses": ["0x8747adFCE380492ec7e9b78761Ec7C87F5Cd3d4F"] + } + }, + "alloc": { + "0x8747adFCE380492ec7e9b78761Ec7C87F5Cd3d4F": { + "balance": "0x8AC7230489E80000" + }, + "0x03000000000000000000000000000000000000b0": { + "balance": "0x0", + "code": "0x6080604052600436106100695760003560e01c80635c60da1b116100435780635c60da1b146100d35780638f28397014610111578063f851a4401461013157610078565b80632c6eefd5146100805780633659cfe6146100a05780634f1ef286146100c057610078565b3661007857610076610146565b005b610076610146565b34801561008c57600080fd5b5061007661009b3660046109e4565b610160565b3480156100ac57600080fd5b506100766100bb3660046109e4565b6101f8565b6100766100ce3660046109ff565b610256565b3480156100df57600080fd5b506100e86102e1565b60405173ffffffffffffffffffffffffffffffffffffffff909116815260200160405180910390f35b34801561011d57600080fd5b5061007661012c3660046109e4565b610336565b34801561013d57600080fd5b506100e861037a565b61014e610407565b61015e6101596104f0565b6104fa565b565b600061016a61051e565b73ffffffffffffffffffffffffffffffffffffffff16146101ec576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601360248201527f616c726561647920696e697469616c697a65640000000000000000000000000060448201526064015b60405180910390fd5b6101f581610528565b50565b610200610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561024e576101f5816040518060200160405280600081525060006105c9565b6101f5610146565b61025e610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102d9576102d48383838080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250600192506105c9915050565b505050565b6102d4610146565b60006102eb610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561032b576103266104f0565b905090565b610333610146565b90565b61033e610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561024e576101f581610528565b6000610384610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561032b57610326610589565b60606103e48383604051806060016040528060278152602001610b1c602791396105f4565b9392505050565b73ffffffffffffffffffffffffffffffffffffffff163b151590565b61040f610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561015e576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152604260248201527f5472616e73706172656e745570677261646561626c6550726f78793a2061646d60448201527f696e2063616e6e6f742066616c6c6261636b20746f2070726f7879207461726760648201527f6574000000000000000000000000000000000000000000000000000000000000608482015260a4016101e3565b600061032661071c565b3660008037600080366000845af43d6000803e808015610519573d6000f35b3d6000fd5b6000610326610589565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f610551610589565b6040805173ffffffffffffffffffffffffffffffffffffffff928316815291841660208301520160405180910390a16101f581610744565b60007fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b5473ffffffffffffffffffffffffffffffffffffffff16919050565b6105d283610850565b6000825111806105df5750805b156102d4576105ee83836103bf565b50505050565b606073ffffffffffffffffffffffffffffffffffffffff84163b61069a576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f416464726573733a2064656c65676174652063616c6c20746f206e6f6e2d636f60448201527f6e7472616374000000000000000000000000000000000000000000000000000060648201526084016101e3565b6000808573ffffffffffffffffffffffffffffffffffffffff16856040516106c29190610aae565b600060405180830381855af49150503d80600081146106fd576040519150601f19603f3d011682016040523d82523d6000602084013e610702565b606091505b509150915061071282828661089d565b9695505050505050565b60007f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc6105ad565b73ffffffffffffffffffffffffffffffffffffffff81166107e7576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f455243313936373a206e65772061646d696e20697320746865207a65726f206160448201527f646472657373000000000000000000000000000000000000000000000000000060648201526084016101e3565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b80547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff9290921691909117905550565b610859816108f0565b60405173ffffffffffffffffffffffffffffffffffffffff8216907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a250565b606083156108ac5750816103e4565b8251156108bc5782518084602001fd5b816040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016101e39190610aca565b73ffffffffffffffffffffffffffffffffffffffff81163b610994576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602d60248201527f455243313936373a206e657720696d706c656d656e746174696f6e206973206e60448201527f6f74206120636f6e74726163740000000000000000000000000000000000000060648201526084016101e3565b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61080a565b803573ffffffffffffffffffffffffffffffffffffffff811681146109df57600080fd5b919050565b6000602082840312156109f657600080fd5b6103e4826109bb565b600080600060408486031215610a1457600080fd5b610a1d846109bb565b9250602084013567ffffffffffffffff80821115610a3a57600080fd5b818601915086601f830112610a4e57600080fd5b813581811115610a5d57600080fd5b876020828501011115610a6f57600080fd5b6020830194508093505050509250925092565b60005b83811015610a9d578181015183820152602001610a85565b838111156105ee5750506000910152565b60008251610ac0818460208701610a82565b9190910192915050565b6020815260008251806020840152610ae9816040850160208701610a82565b601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016919091016040019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a26469706673582212208f0bc66b8ee51a4c109376b6c4d7e171cd933a56f45ed508f31a9c3b7aa9d4eb64736f6c63430008090033" + }, + "0x03000000000000000000000000000000000000b1": { + "balance": "0x0", + "code": "0x6080604052600436106100695760003560e01c80635c60da1b116100435780635c60da1b146100d35780638f28397014610111578063f851a4401461013157610078565b80632c6eefd5146100805780633659cfe6146100a05780634f1ef286146100c057610078565b3661007857610076610146565b005b610076610146565b34801561008c57600080fd5b5061007661009b3660046109e4565b610160565b3480156100ac57600080fd5b506100766100bb3660046109e4565b6101f8565b6100766100ce3660046109ff565b610256565b3480156100df57600080fd5b506100e86102e1565b60405173ffffffffffffffffffffffffffffffffffffffff909116815260200160405180910390f35b34801561011d57600080fd5b5061007661012c3660046109e4565b610336565b34801561013d57600080fd5b506100e861037a565b61014e610407565b61015e6101596104f0565b6104fa565b565b600061016a61051e565b73ffffffffffffffffffffffffffffffffffffffff16146101ec576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601360248201527f616c726561647920696e697469616c697a65640000000000000000000000000060448201526064015b60405180910390fd5b6101f581610528565b50565b610200610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561024e576101f5816040518060200160405280600081525060006105c9565b6101f5610146565b61025e610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102d9576102d48383838080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250600192506105c9915050565b505050565b6102d4610146565b60006102eb610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561032b576103266104f0565b905090565b610333610146565b90565b61033e610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561024e576101f581610528565b6000610384610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561032b57610326610589565b60606103e48383604051806060016040528060278152602001610b1c602791396105f4565b9392505050565b73ffffffffffffffffffffffffffffffffffffffff163b151590565b61040f610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561015e576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152604260248201527f5472616e73706172656e745570677261646561626c6550726f78793a2061646d60448201527f696e2063616e6e6f742066616c6c6261636b20746f2070726f7879207461726760648201527f6574000000000000000000000000000000000000000000000000000000000000608482015260a4016101e3565b600061032661071c565b3660008037600080366000845af43d6000803e808015610519573d6000f35b3d6000fd5b6000610326610589565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f610551610589565b6040805173ffffffffffffffffffffffffffffffffffffffff928316815291841660208301520160405180910390a16101f581610744565b60007fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b5473ffffffffffffffffffffffffffffffffffffffff16919050565b6105d283610850565b6000825111806105df5750805b156102d4576105ee83836103bf565b50505050565b606073ffffffffffffffffffffffffffffffffffffffff84163b61069a576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f416464726573733a2064656c65676174652063616c6c20746f206e6f6e2d636f60448201527f6e7472616374000000000000000000000000000000000000000000000000000060648201526084016101e3565b6000808573ffffffffffffffffffffffffffffffffffffffff16856040516106c29190610aae565b600060405180830381855af49150503d80600081146106fd576040519150601f19603f3d011682016040523d82523d6000602084013e610702565b606091505b509150915061071282828661089d565b9695505050505050565b60007f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc6105ad565b73ffffffffffffffffffffffffffffffffffffffff81166107e7576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f455243313936373a206e65772061646d696e20697320746865207a65726f206160448201527f646472657373000000000000000000000000000000000000000000000000000060648201526084016101e3565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b80547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff9290921691909117905550565b610859816108f0565b60405173ffffffffffffffffffffffffffffffffffffffff8216907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a250565b606083156108ac5750816103e4565b8251156108bc5782518084602001fd5b816040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016101e39190610aca565b73ffffffffffffffffffffffffffffffffffffffff81163b610994576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602d60248201527f455243313936373a206e657720696d706c656d656e746174696f6e206973206e60448201527f6f74206120636f6e74726163740000000000000000000000000000000000000060648201526084016101e3565b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61080a565b803573ffffffffffffffffffffffffffffffffffffffff811681146109df57600080fd5b919050565b6000602082840312156109f657600080fd5b6103e4826109bb565b600080600060408486031215610a1457600080fd5b610a1d846109bb565b9250602084013567ffffffffffffffff80821115610a3a57600080fd5b818601915086601f830112610a4e57600080fd5b813581811115610a5d57600080fd5b876020828501011115610a6f57600080fd5b6020830194508093505050509250925092565b60005b83811015610a9d578181015183820152602001610a85565b838111156105ee5750506000910152565b60008251610ac0818460208701610a82565b9190910192915050565b6020815260008251806020840152610ae9816040850160208701610a82565b601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016919091016040019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a26469706673582212208f0bc66b8ee51a4c109376b6c4d7e171cd933a56f45ed508f31a9c3b7aa9d4eb64736f6c63430008090033" + }, + "0x03000000000000000000000000000000000000b2": { + "balance": "0x0", + "code": "0x6080604052600436106100695760003560e01c80635c60da1b116100435780635c60da1b146100d35780638f28397014610111578063f851a4401461013157610078565b80632c6eefd5146100805780633659cfe6146100a05780634f1ef286146100c057610078565b3661007857610076610146565b005b610076610146565b34801561008c57600080fd5b5061007661009b3660046109e4565b610160565b3480156100ac57600080fd5b506100766100bb3660046109e4565b6101f8565b6100766100ce3660046109ff565b610256565b3480156100df57600080fd5b506100e86102e1565b60405173ffffffffffffffffffffffffffffffffffffffff909116815260200160405180910390f35b34801561011d57600080fd5b5061007661012c3660046109e4565b610336565b34801561013d57600080fd5b506100e861037a565b61014e610407565b61015e6101596104f0565b6104fa565b565b600061016a61051e565b73ffffffffffffffffffffffffffffffffffffffff16146101ec576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601360248201527f616c726561647920696e697469616c697a65640000000000000000000000000060448201526064015b60405180910390fd5b6101f581610528565b50565b610200610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561024e576101f5816040518060200160405280600081525060006105c9565b6101f5610146565b61025e610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102d9576102d48383838080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250600192506105c9915050565b505050565b6102d4610146565b60006102eb610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561032b576103266104f0565b905090565b610333610146565b90565b61033e610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561024e576101f581610528565b6000610384610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561032b57610326610589565b60606103e48383604051806060016040528060278152602001610b1c602791396105f4565b9392505050565b73ffffffffffffffffffffffffffffffffffffffff163b151590565b61040f610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561015e576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152604260248201527f5472616e73706172656e745570677261646561626c6550726f78793a2061646d60448201527f696e2063616e6e6f742066616c6c6261636b20746f2070726f7879207461726760648201527f6574000000000000000000000000000000000000000000000000000000000000608482015260a4016101e3565b600061032661071c565b3660008037600080366000845af43d6000803e808015610519573d6000f35b3d6000fd5b6000610326610589565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f610551610589565b6040805173ffffffffffffffffffffffffffffffffffffffff928316815291841660208301520160405180910390a16101f581610744565b60007fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b5473ffffffffffffffffffffffffffffffffffffffff16919050565b6105d283610850565b6000825111806105df5750805b156102d4576105ee83836103bf565b50505050565b606073ffffffffffffffffffffffffffffffffffffffff84163b61069a576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f416464726573733a2064656c65676174652063616c6c20746f206e6f6e2d636f60448201527f6e7472616374000000000000000000000000000000000000000000000000000060648201526084016101e3565b6000808573ffffffffffffffffffffffffffffffffffffffff16856040516106c29190610aae565b600060405180830381855af49150503d80600081146106fd576040519150601f19603f3d011682016040523d82523d6000602084013e610702565b606091505b509150915061071282828661089d565b9695505050505050565b60007f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc6105ad565b73ffffffffffffffffffffffffffffffffffffffff81166107e7576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f455243313936373a206e65772061646d696e20697320746865207a65726f206160448201527f646472657373000000000000000000000000000000000000000000000000000060648201526084016101e3565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b80547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff9290921691909117905550565b610859816108f0565b60405173ffffffffffffffffffffffffffffffffffffffff8216907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a250565b606083156108ac5750816103e4565b8251156108bc5782518084602001fd5b816040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016101e39190610aca565b73ffffffffffffffffffffffffffffffffffffffff81163b610994576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602d60248201527f455243313936373a206e657720696d706c656d656e746174696f6e206973206e60448201527f6f74206120636f6e74726163740000000000000000000000000000000000000060648201526084016101e3565b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61080a565b803573ffffffffffffffffffffffffffffffffffffffff811681146109df57600080fd5b919050565b6000602082840312156109f657600080fd5b6103e4826109bb565b600080600060408486031215610a1457600080fd5b610a1d846109bb565b9250602084013567ffffffffffffffff80821115610a3a57600080fd5b818601915086601f830112610a4e57600080fd5b813581811115610a5d57600080fd5b876020828501011115610a6f57600080fd5b6020830194508093505050509250925092565b60005b83811015610a9d578181015183820152602001610a85565b838111156105ee5750506000910152565b60008251610ac0818460208701610a82565b9190910192915050565b6020815260008251806020840152610ae9816040850160208701610a82565b601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016919091016040019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a26469706673582212208f0bc66b8ee51a4c109376b6c4d7e171cd933a56f45ed508f31a9c3b7aa9d4eb64736f6c63430008090033" + }, + "0x03000000000000000000000000000000000000b3": { + "balance": "0x0", + "code": "0x6080604052600436106100695760003560e01c80635c60da1b116100435780635c60da1b146100d35780638f28397014610111578063f851a4401461013157610078565b80632c6eefd5146100805780633659cfe6146100a05780634f1ef286146100c057610078565b3661007857610076610146565b005b610076610146565b34801561008c57600080fd5b5061007661009b3660046109e4565b610160565b3480156100ac57600080fd5b506100766100bb3660046109e4565b6101f8565b6100766100ce3660046109ff565b610256565b3480156100df57600080fd5b506100e86102e1565b60405173ffffffffffffffffffffffffffffffffffffffff909116815260200160405180910390f35b34801561011d57600080fd5b5061007661012c3660046109e4565b610336565b34801561013d57600080fd5b506100e861037a565b61014e610407565b61015e6101596104f0565b6104fa565b565b600061016a61051e565b73ffffffffffffffffffffffffffffffffffffffff16146101ec576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601360248201527f616c726561647920696e697469616c697a65640000000000000000000000000060448201526064015b60405180910390fd5b6101f581610528565b50565b610200610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561024e576101f5816040518060200160405280600081525060006105c9565b6101f5610146565b61025e610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102d9576102d48383838080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250600192506105c9915050565b505050565b6102d4610146565b60006102eb610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561032b576103266104f0565b905090565b610333610146565b90565b61033e610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561024e576101f581610528565b6000610384610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561032b57610326610589565b60606103e48383604051806060016040528060278152602001610b1c602791396105f4565b9392505050565b73ffffffffffffffffffffffffffffffffffffffff163b151590565b61040f610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561015e576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152604260248201527f5472616e73706172656e745570677261646561626c6550726f78793a2061646d60448201527f696e2063616e6e6f742066616c6c6261636b20746f2070726f7879207461726760648201527f6574000000000000000000000000000000000000000000000000000000000000608482015260a4016101e3565b600061032661071c565b3660008037600080366000845af43d6000803e808015610519573d6000f35b3d6000fd5b6000610326610589565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f610551610589565b6040805173ffffffffffffffffffffffffffffffffffffffff928316815291841660208301520160405180910390a16101f581610744565b60007fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b5473ffffffffffffffffffffffffffffffffffffffff16919050565b6105d283610850565b6000825111806105df5750805b156102d4576105ee83836103bf565b50505050565b606073ffffffffffffffffffffffffffffffffffffffff84163b61069a576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f416464726573733a2064656c65676174652063616c6c20746f206e6f6e2d636f60448201527f6e7472616374000000000000000000000000000000000000000000000000000060648201526084016101e3565b6000808573ffffffffffffffffffffffffffffffffffffffff16856040516106c29190610aae565b600060405180830381855af49150503d80600081146106fd576040519150601f19603f3d011682016040523d82523d6000602084013e610702565b606091505b509150915061071282828661089d565b9695505050505050565b60007f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc6105ad565b73ffffffffffffffffffffffffffffffffffffffff81166107e7576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f455243313936373a206e65772061646d696e20697320746865207a65726f206160448201527f646472657373000000000000000000000000000000000000000000000000000060648201526084016101e3565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b80547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff9290921691909117905550565b610859816108f0565b60405173ffffffffffffffffffffffffffffffffffffffff8216907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a250565b606083156108ac5750816103e4565b8251156108bc5782518084602001fd5b816040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016101e39190610aca565b73ffffffffffffffffffffffffffffffffffffffff81163b610994576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602d60248201527f455243313936373a206e657720696d706c656d656e746174696f6e206973206e60448201527f6f74206120636f6e74726163740000000000000000000000000000000000000060648201526084016101e3565b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61080a565b803573ffffffffffffffffffffffffffffffffffffffff811681146109df57600080fd5b919050565b6000602082840312156109f657600080fd5b6103e4826109bb565b600080600060408486031215610a1457600080fd5b610a1d846109bb565b9250602084013567ffffffffffffffff80821115610a3a57600080fd5b818601915086601f830112610a4e57600080fd5b813581811115610a5d57600080fd5b876020828501011115610a6f57600080fd5b6020830194508093505050509250925092565b60005b83811015610a9d578181015183820152602001610a85565b838111156105ee5750506000910152565b60008251610ac0818460208701610a82565b9190910192915050565b6020815260008251806020840152610ae9816040850160208701610a82565b601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016919091016040019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a26469706673582212208f0bc66b8ee51a4c109376b6c4d7e171cd933a56f45ed508f31a9c3b7aa9d4eb64736f6c63430008090033" + }, + "0x03000000000000000000000000000000000000b4": { + "balance": "0x0", + "code": "0x6080604052600436106100695760003560e01c80635c60da1b116100435780635c60da1b146100d35780638f28397014610111578063f851a4401461013157610078565b80632c6eefd5146100805780633659cfe6146100a05780634f1ef286146100c057610078565b3661007857610076610146565b005b610076610146565b34801561008c57600080fd5b5061007661009b3660046109e4565b610160565b3480156100ac57600080fd5b506100766100bb3660046109e4565b6101f8565b6100766100ce3660046109ff565b610256565b3480156100df57600080fd5b506100e86102e1565b60405173ffffffffffffffffffffffffffffffffffffffff909116815260200160405180910390f35b34801561011d57600080fd5b5061007661012c3660046109e4565b610336565b34801561013d57600080fd5b506100e861037a565b61014e610407565b61015e6101596104f0565b6104fa565b565b600061016a61051e565b73ffffffffffffffffffffffffffffffffffffffff16146101ec576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601360248201527f616c726561647920696e697469616c697a65640000000000000000000000000060448201526064015b60405180910390fd5b6101f581610528565b50565b610200610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561024e576101f5816040518060200160405280600081525060006105c9565b6101f5610146565b61025e610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102d9576102d48383838080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250600192506105c9915050565b505050565b6102d4610146565b60006102eb610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561032b576103266104f0565b905090565b610333610146565b90565b61033e610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561024e576101f581610528565b6000610384610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561032b57610326610589565b60606103e48383604051806060016040528060278152602001610b1c602791396105f4565b9392505050565b73ffffffffffffffffffffffffffffffffffffffff163b151590565b61040f610589565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561015e576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152604260248201527f5472616e73706172656e745570677261646561626c6550726f78793a2061646d60448201527f696e2063616e6e6f742066616c6c6261636b20746f2070726f7879207461726760648201527f6574000000000000000000000000000000000000000000000000000000000000608482015260a4016101e3565b600061032661071c565b3660008037600080366000845af43d6000803e808015610519573d6000f35b3d6000fd5b6000610326610589565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f610551610589565b6040805173ffffffffffffffffffffffffffffffffffffffff928316815291841660208301520160405180910390a16101f581610744565b60007fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b5473ffffffffffffffffffffffffffffffffffffffff16919050565b6105d283610850565b6000825111806105df5750805b156102d4576105ee83836103bf565b50505050565b606073ffffffffffffffffffffffffffffffffffffffff84163b61069a576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f416464726573733a2064656c65676174652063616c6c20746f206e6f6e2d636f60448201527f6e7472616374000000000000000000000000000000000000000000000000000060648201526084016101e3565b6000808573ffffffffffffffffffffffffffffffffffffffff16856040516106c29190610aae565b600060405180830381855af49150503d80600081146106fd576040519150601f19603f3d011682016040523d82523d6000602084013e610702565b606091505b509150915061071282828661089d565b9695505050505050565b60007f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc6105ad565b73ffffffffffffffffffffffffffffffffffffffff81166107e7576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f455243313936373a206e65772061646d696e20697320746865207a65726f206160448201527f646472657373000000000000000000000000000000000000000000000000000060648201526084016101e3565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b80547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff9290921691909117905550565b610859816108f0565b60405173ffffffffffffffffffffffffffffffffffffffff8216907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a250565b606083156108ac5750816103e4565b8251156108bc5782518084602001fd5b816040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016101e39190610aca565b73ffffffffffffffffffffffffffffffffffffffff81163b610994576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602d60248201527f455243313936373a206e657720696d706c656d656e746174696f6e206973206e60448201527f6f74206120636f6e74726163740000000000000000000000000000000000000060648201526084016101e3565b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61080a565b803573ffffffffffffffffffffffffffffffffffffffff811681146109df57600080fd5b919050565b6000602082840312156109f657600080fd5b6103e4826109bb565b600080600060408486031215610a1457600080fd5b610a1d846109bb565b9250602084013567ffffffffffffffff80821115610a3a57600080fd5b818601915086601f830112610a4e57600080fd5b813581811115610a5d57600080fd5b876020828501011115610a6f57600080fd5b6020830194508093505050509250925092565b60005b83811015610a9d578181015183820152602001610a85565b838111156105ee5750506000910152565b60008251610ac0818460208701610a82565b9190910192915050565b6020815260008251806020840152610ae9816040850160208701610a82565b601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016919091016040019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a26469706673582212208f0bc66b8ee51a4c109376b6c4d7e171cd933a56f45ed508f31a9c3b7aa9d4eb64736f6c63430008090033" + } + }, + "nonce": "0x0", + "timestamp": "0x0", + "extraData": "0x00", + "gasLimit": "15000000", + "difficulty": "0x0", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase": "0x0000000000000000000000000000000000000000", + "number": "0x0", + "gasUsed": "0x0", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000" +} diff --git a/network-configs/hubblenet/node.json b/network-configs/hubblenet/node.json new file mode 100644 index 0000000000..54afb63c02 --- /dev/null +++ b/network-configs/hubblenet/node.json @@ -0,0 +1,5 @@ +{ + "profile-continuous-enabled": true, + "profile-continuous-max-files": 100, + "profile-dir": "/var/avalanche/profiles/continuous/" +} diff --git a/network-configs/hubblenet/upgrade.json b/network-configs/hubblenet/upgrade.json new file mode 100644 index 0000000000..c1a7d7202e --- /dev/null +++ b/network-configs/hubblenet/upgrade.json @@ -0,0 +1,19 @@ +{ + "precompileUpgrades": [ + { + "jurorConfig": { + "blockTimestamp": 1699950600 + } + }, + { + "ticksConfig": { + "blockTimestamp": 1699950600 + } + }, + { + "jurorV2Config": { + "blockTimestamp": 1708686000 + } + } + ] +} diff --git a/peer/network_test.go b/peer/network_test.go index d2aaf8d931..2bd20fd477 100644 --- a/peer/network_test.go +++ b/peer/network_test.go @@ -998,6 +998,12 @@ func (t *testGossipHandler) HandleEthTxs(nodeID ids.NodeID, msg message.EthTxsGo return nil } +func (t *testGossipHandler) HandleSignedOrders(nodeID ids.NodeID, msg message.SignedOrdersGossip) error { + t.received = true + t.nodeID = nodeID + return nil +} + type testRequestHandler struct { message.RequestHandler calls uint32 diff --git a/plugin/evm/block_builder.go b/plugin/evm/block_builder.go index 9cd749bcc1..00d1596318 100644 --- a/plugin/evm/block_builder.go +++ b/plugin/evm/block_builder.go @@ -20,7 +20,7 @@ import ( const ( // Minimum amount of time to wait after building a block before attempting to build a block // a second time without changing the contents of the mempool. - minBlockBuildingRetryDelay = 500 * time.Millisecond + minBlockBuildingRetryDelay = 50 * time.Millisecond ) type blockBuilder struct { diff --git a/plugin/evm/config.go b/plugin/evm/config.go index 4871415327..60e35e5933 100644 --- a/plugin/evm/config.go +++ b/plugin/evm/config.go @@ -50,6 +50,9 @@ const ( defaultPopulateMissingTriesParallelism = 1024 defaultStateSyncServerTrieCache = 64 // MB defaultAcceptedCacheSize = 32 // blocks + defaulOrderGossipNumValidators = 10 + defaultOrderGossipNumNonValidators = 5 + defaultOrderGossipNumPeers = 15 // defaultStateSyncMinBlocks is the minimum number of blocks the blockchain // should be ahead of local last accepted to perform state sync. @@ -60,10 +63,18 @@ const ( // - state sync time: ~6 hrs. defaultStateSyncMinBlocks = 300_000 defaultStateSyncRequestSize = 1024 // the number of key/values to ask peers for per request + + defaultIsValidator = false + defaultTradingAPIEnabled = false + defaultLoadFromSnapshotEnabled = true + defaultSnapshotFilePath = "/tmp/snapshot" + defaultMakerbookDatabasePath = "/tmp/makerbook" ) var ( - defaultEnabledAPIs = []string{ + defaultTestingApiEnabled = false + defaultValidatorPrivateKeyFile = "/home/ubuntu/.avalanche-cli/key/validator.pk" + defaultEnabledAPIs = []string{ "eth", "eth-filter", "net", @@ -165,6 +176,11 @@ type Config struct { RegossipFrequency Duration `json:"regossip-frequency"` PriorityRegossipAddresses []common.Address `json:"priority-regossip-addresses"` + // Order Gossip Settings + OrderGossipNumValidators int `json:"order-gossip-num-validators"` + OrderGossipNumNonValidators int `json:"order-gossip-num-non-validators"` + OrderGossipNumPeers int `json:"order-gossip-num-peers"` + // Log LogLevel string `json:"log-level"` LogJSONFormat bool `json:"log-json-format"` @@ -222,6 +238,26 @@ type Config struct { // Note: only supports AddressedCall payloads as defined here: // https://github.com/ava-labs/avalanchego/tree/7623ffd4be915a5185c9ed5e11fa9be15a6e1f00/vms/platformvm/warp/payload#addressedcall WarpOffChainMessages []hexutil.Bytes `json:"warp-off-chain-messages"` + + // Path to validator private key file + ValidatorPrivateKeyFile string `json:"validator-private-key-file"` + + // Testing apis enabled + TestingApiEnabled bool `json:"testing-api-enabled"` + // IsValidator is true if this node is a validator + IsValidator bool `json:"is-validator"` + + // TradingAPI is for the sdk + TradingAPIEnabled bool `json:"trading-api-enabled"` + + // LoadFromSnapshotEnabled = true if the node should load the memory db from a snapshot + LoadFromSnapshotEnabled bool `json:"load-from-snapshot-enabled"` + + // SnapshotFilePath is the path to the file which saves the latest snapshot bytes + SnapshotFilePath string `json:"snapshot-file-path"` + + // MakerbookDatabasePath is the path to the file which saves the makerbook orders + MakerbookDatabasePath string `json:"makerbook-database-path"` } // EthAPIs returns an array of strings representing the Eth APIs that should be enabled @@ -281,6 +317,16 @@ func (c *Config) SetDefaults() { c.StateSyncRequestSize = defaultStateSyncRequestSize c.AllowUnprotectedTxHashes = defaultAllowUnprotectedTxHashes c.AcceptedCacheSize = defaultAcceptedCacheSize + c.ValidatorPrivateKeyFile = defaultValidatorPrivateKeyFile + c.TestingApiEnabled = defaultTestingApiEnabled + c.IsValidator = defaultIsValidator + c.TradingAPIEnabled = defaultTradingAPIEnabled + c.LoadFromSnapshotEnabled = defaultLoadFromSnapshotEnabled + c.SnapshotFilePath = defaultSnapshotFilePath + c.MakerbookDatabasePath = defaultMakerbookDatabasePath + c.OrderGossipNumValidators = defaulOrderGossipNumValidators + c.OrderGossipNumNonValidators = defaultOrderGossipNumNonValidators + c.OrderGossipNumPeers = defaultOrderGossipNumPeers } func (d *Duration) UnmarshalJSON(data []byte) (err error) { diff --git a/plugin/evm/gossip_stats.go b/plugin/evm/gossip_stats.go index 3a6f552fcc..5840250015 100644 --- a/plugin/evm/gossip_stats.go +++ b/plugin/evm/gossip_stats.go @@ -10,22 +10,39 @@ var _ GossipStats = &gossipStats{} // GossipStats contains methods for updating incoming and outgoing gossip stats. type GossipStats interface { IncEthTxsGossipReceived() - - // new vs. known txs received IncEthTxsGossipReceivedError() IncEthTxsGossipReceivedKnown() IncEthTxsGossipReceivedNew() + + IncSignedOrdersGossipReceived(count int64) + IncSignedOrdersGossipBatchReceived() + IncSignedOrdersGossipReceivedKnown() + IncSignedOrdersGossipReceivedNew() + IncSignedOrdersGossipReceiveError() + + IncSignedOrdersGossipSent(count int64) + IncSignedOrdersGossipBatchSent() + IncSignedOrdersGossipSendError() + IncSignedOrdersGossipOrderExpired() } // gossipStats implements stats for incoming and outgoing gossip stats. type gossipStats struct { - // messages - ethTxsGossipReceived metrics.Counter - - // new vs. known txs received + ethTxsGossipReceived metrics.Counter ethTxsGossipReceivedError metrics.Counter ethTxsGossipReceivedKnown metrics.Counter ethTxsGossipReceivedNew metrics.Counter + + signedOrdersGossipReceived metrics.Counter + signedOrdersGossipBatchReceived metrics.Counter + signedOrdersGossipReceivedKnown metrics.Counter + signedOrdersGossipReceivedNew metrics.Counter + signedOrdersGossipReceiveError metrics.Counter + + signedOrdersGossipSent metrics.Counter + signedOrdersGossipBatchSent metrics.Counter + signedOrdersGossipSendError metrics.Counter + signedOrdersGossipOrderExpired metrics.Counter } func NewGossipStats() GossipStats { @@ -34,6 +51,17 @@ func NewGossipStats() GossipStats { ethTxsGossipReceivedError: metrics.GetOrRegisterCounter("gossip_eth_txs_received_error", nil), ethTxsGossipReceivedKnown: metrics.GetOrRegisterCounter("gossip_eth_txs_received_known", nil), ethTxsGossipReceivedNew: metrics.GetOrRegisterCounter("gossip_eth_txs_received_new", nil), + + signedOrdersGossipSent: metrics.GetOrRegisterCounter("gossip_signed_orders_sent", nil), + signedOrdersGossipBatchSent: metrics.GetOrRegisterCounter("gossip_signed_orders_batch_sent", nil), + signedOrdersGossipSendError: metrics.GetOrRegisterCounter("gossip_signed_orders_send_error", nil), + signedOrdersGossipOrderExpired: metrics.GetOrRegisterCounter("gossip_signed_orders_expired", nil), + signedOrdersGossipReceived: metrics.GetOrRegisterCounter("gossip_signed_orders_received", nil), + signedOrdersGossipBatchReceived: metrics.GetOrRegisterCounter("gossip_signed_orders_batch_received", nil), + signedOrdersGossipReceiveError: metrics.GetOrRegisterCounter("gossip_signed_orders_received", nil), + + signedOrdersGossipReceivedKnown: metrics.GetOrRegisterCounter("gossip_signed_orders_received_known", nil), + signedOrdersGossipReceivedNew: metrics.GetOrRegisterCounter("gossip_signed_orders_received_new", nil), } } @@ -44,3 +72,20 @@ func (g *gossipStats) IncEthTxsGossipReceived() { g.ethTxsGossipReceived.Inc(1) func (g *gossipStats) IncEthTxsGossipReceivedError() { g.ethTxsGossipReceivedError.Inc(1) } func (g *gossipStats) IncEthTxsGossipReceivedKnown() { g.ethTxsGossipReceivedKnown.Inc(1) } func (g *gossipStats) IncEthTxsGossipReceivedNew() { g.ethTxsGossipReceivedNew.Inc(1) } + +// incoming messages +func (g *gossipStats) IncSignedOrdersGossipReceived(count int64) { + g.signedOrdersGossipReceived.Inc(count) +} +func (g *gossipStats) IncSignedOrdersGossipBatchReceived() { g.signedOrdersGossipBatchReceived.Inc(1) } + +// new vs. known txs received +func (g *gossipStats) IncSignedOrdersGossipReceivedKnown() { g.signedOrdersGossipReceivedKnown.Inc(1) } +func (g *gossipStats) IncSignedOrdersGossipReceivedNew() { g.signedOrdersGossipReceivedNew.Inc(1) } +func (g *gossipStats) IncSignedOrdersGossipReceiveError() { g.signedOrdersGossipReceiveError.Inc(1) } + +// outgoing messages +func (g *gossipStats) IncSignedOrdersGossipSent(count int64) { g.signedOrdersGossipSent.Inc(count) } +func (g *gossipStats) IncSignedOrdersGossipBatchSent() { g.signedOrdersGossipBatchSent.Inc(1) } +func (g *gossipStats) IncSignedOrdersGossipSendError() { g.signedOrdersGossipSendError.Inc(1) } +func (g *gossipStats) IncSignedOrdersGossipOrderExpired() { g.signedOrdersGossipOrderExpired.Inc(1) } diff --git a/plugin/evm/gossiper_orders.go b/plugin/evm/gossiper_orders.go new file mode 100644 index 0000000000..7f8d2cd3d1 --- /dev/null +++ b/plugin/evm/gossiper_orders.go @@ -0,0 +1,193 @@ +package evm + +import ( + "bytes" + "context" + "encoding/gob" + "sync" + "time" + + "github.com/ava-labs/avalanchego/codec" + "github.com/ava-labs/avalanchego/snow" + commonEng "github.com/ava-labs/avalanchego/snow/engine/common" + "github.com/ava-labs/subnet-evm/plugin/evm/message" + "github.com/ava-labs/subnet-evm/plugin/evm/orderbook" + "github.com/ava-labs/subnet-evm/plugin/evm/orderbook/hubbleutils" + hu "github.com/ava-labs/subnet-evm/plugin/evm/orderbook/hubbleutils" + "github.com/ethereum/go-ethereum/log" +) + +const ( + // [ordersGossipInterval] is how often we attempt to gossip newly seen + // signed orders to other nodes. + ordersGossipInterval = 100 * time.Millisecond + + // [minGossipOrdersBatchInterval] is the minimum amount of time that must pass + // before our last gossip to peers. + minGossipOrdersBatchInterval = 50 * time.Millisecond + + // [maxSignedOrdersGossipBatchSize] is the maximum number of orders we will + // attempt to gossip at once. + maxSignedOrdersGossipBatchSize = 100 +) + +type OrderGossiper interface { + // GossipSignedOrders sends signed orders to the network + GossipSignedOrders(orders []*hubbleutils.SignedOrder) error +} + +type orderPushGossiper struct { + ctx *snow.Context + config Config + + shutdownChan chan struct{} + shutdownWg *sync.WaitGroup + + ordersToGossipChan chan []*hubbleutils.SignedOrder + ordersToGossip []*hubbleutils.SignedOrder + lastOrdersGossiped time.Time + + codec codec.Manager + stats GossipStats + + appSender commonEng.AppSender +} + +// createOrderGossiper constructs and returns a orderPushGossiper or noopGossiper +func (vm *VM) createOrderGossiper( + stats GossipStats, +) OrderGossiper { + net := &orderPushGossiper{ + ctx: vm.ctx, + config: vm.config, + shutdownChan: vm.shutdownChan, + shutdownWg: &vm.shutdownWg, + codec: vm.networkCodec, + stats: stats, + ordersToGossipChan: make(chan []*hubbleutils.SignedOrder), + ordersToGossip: []*hubbleutils.SignedOrder{}, + appSender: vm.p2pSender, + } + + net.awaitSignedOrderGossip() + return net +} + +func (n *orderPushGossiper) GossipSignedOrders(orders []*hu.SignedOrder) error { + select { + case n.ordersToGossipChan <- orders: + case <-n.shutdownChan: + } + return nil +} + +func (n *orderPushGossiper) awaitSignedOrderGossip() { + n.shutdownWg.Add(1) + go executeFuncAndRecoverPanic(func() { + var ( + gossipTicker = time.NewTicker(ordersGossipInterval) + ) + defer func() { + gossipTicker.Stop() + n.shutdownWg.Done() + }() + + for { + select { + case <-gossipTicker.C: + if attempted, err := n.gossipSignedOrders(); err != nil { + log.Warn( + "failed to send signed orders", + "len(orders)", attempted, + "err", err, + ) + } + case orders := <-n.ordersToGossipChan: + for _, order := range orders { + n.ordersToGossip = append(n.ordersToGossip, order) + } + if attempted, err := n.gossipSignedOrders(); err != nil { + log.Warn( + "failed to send signed orders", + "len(orders)", attempted, + "err", err, + ) + } + case <-n.shutdownChan: + return + } + } + }, "panic in awaitSignedOrderGossip", orderbook.AwaitSignedOrdersGossipPanicsCounter) +} + +func (n *orderPushGossiper) gossipSignedOrders() (int, error) { + if (time.Since(n.lastOrdersGossiped) < minGossipOrdersBatchInterval) || len(n.ordersToGossip) == 0 { + return 0, nil + } + n.lastOrdersGossiped = time.Now() + now := time.Now().Unix() + selectedOrders := []*hu.SignedOrder{} + numConsumed := 0 + for _, order := range n.ordersToGossip { + if len(selectedOrders) >= maxSignedOrdersGossipBatchSize { + break + } + numConsumed++ + if order.ExpireAt.Int64() < now { + n.stats.IncSignedOrdersGossipOrderExpired() + log.Warn("signed order expired before gossip", "order", order, "now", now) + continue + } + selectedOrders = append(selectedOrders, order) + } + // delete all selected orders from n.ordersToGossip + n.ordersToGossip = n.ordersToGossip[numConsumed:] + + if len(selectedOrders) == 0 { + return 0, nil + } + + err := n.sendSignedOrders(selectedOrders) + if err != nil { + n.stats.IncSignedOrdersGossipSendError() + } + return len(selectedOrders), err +} + +func (n *orderPushGossiper) sendSignedOrders(orders []*hu.SignedOrder) error { + if len(orders) == 0 { + return nil + } + + var buf bytes.Buffer + err := gob.NewEncoder(&buf).Encode(&orders) + if err != nil { + return err + } + ordersBytes := buf.Bytes() + msg := message.SignedOrdersGossip{ + Orders: ordersBytes, + } + msgBytes, err := message.BuildGossipMessage(n.codec, msg) + if err != nil { + return err + } + + log.Trace( + "gossiping signed orders", + "len(orders)", len(orders), + "size(orders)", len(msg.Orders), + ) + + validators := n.config.OrderGossipNumValidators + nonValidators := n.config.OrderGossipNumNonValidators + peers := n.config.OrderGossipNumPeers + err = n.appSender.SendAppGossip(context.TODO(), msgBytes, validators, nonValidators, peers) + if err != nil { + log.Error("failed to gossip orders") + return err + } + n.stats.IncSignedOrdersGossipSent(int64(len(orders))) + n.stats.IncSignedOrdersGossipBatchSent() + return nil +} diff --git a/plugin/evm/handler.go b/plugin/evm/handler.go index 2915d422a2..c69b149523 100644 --- a/plugin/evm/handler.go +++ b/plugin/evm/handler.go @@ -4,6 +4,10 @@ package evm import ( + "bytes" + "encoding/gob" + "sync" + "github.com/ava-labs/avalanchego/ids" "github.com/ethereum/go-ethereum/log" @@ -12,10 +16,12 @@ import ( "github.com/ava-labs/subnet-evm/core/txpool" "github.com/ava-labs/subnet-evm/core/types" "github.com/ava-labs/subnet-evm/plugin/evm/message" + hu "github.com/ava-labs/subnet-evm/plugin/evm/orderbook/hubbleutils" ) // GossipHandler handles incoming gossip messages type GossipHandler struct { + mu sync.RWMutex vm *VM txPool *txpool.TxPool stats GossipStats @@ -74,3 +80,60 @@ func (h *GossipHandler) HandleEthTxs(nodeID ids.NodeID, msg message.EthTxsGossip } return nil } + +func (h *GossipHandler) HandleSignedOrders(nodeID ids.NodeID, msg message.SignedOrdersGossip) error { + h.mu.Lock() + defer h.mu.Unlock() + + log.Trace( + "AppGossip called with SignedOrdersGossip", + "peerID", nodeID, + "bytes(orders)", len(msg.Orders), + ) + + if len(msg.Orders) == 0 { + log.Warn( + "AppGossip received empty SignedOrdersGossip Message", + "peerID", nodeID, + ) + return nil + } + + orders := make([]*hu.SignedOrder, 0) + buf := bytes.NewBuffer(msg.Orders) + err := gob.NewDecoder(buf).Decode(&orders) + if err != nil { + log.Error("failed to decode signed orders", "err", err) + return err + } + + h.stats.IncSignedOrdersGossipReceived(int64(len(orders))) + h.stats.IncSignedOrdersGossipBatchReceived() + + tradingAPI := h.vm.limitOrderProcesser.GetTradingAPI() + + // re-gossip orders, but not when we already knew the orders + ordersToGossip := make([]*hu.SignedOrder, 0) + for _, order := range orders { + _, shouldTriggerMatching, err := tradingAPI.PlaceOrder(order) + if err == nil { + h.stats.IncSignedOrdersGossipReceivedNew() + ordersToGossip = append(ordersToGossip, order) + if shouldTriggerMatching { + log.Info("received new match-able signed order, triggering matching pipeline...") + h.vm.limitOrderProcesser.RunMatchingPipeline() + } + } else if err == hu.ErrOrderAlreadyExists { + h.stats.IncSignedOrdersGossipReceivedKnown() + } else { + h.stats.IncSignedOrdersGossipReceiveError() + log.Error("failed to place order", "err", err) + } + } + + if len(ordersToGossip) > 0 { + h.vm.orderGossiper.GossipSignedOrders(ordersToGossip) + } + + return nil +} diff --git a/plugin/evm/limit_order.go b/plugin/evm/limit_order.go new file mode 100644 index 0000000000..94d6628d4f --- /dev/null +++ b/plugin/evm/limit_order.go @@ -0,0 +1,533 @@ +package evm + +import ( + "bytes" + "context" + "encoding/gob" + "fmt" + "math/big" + "os" + "runtime" + "runtime/debug" + "sync" + "time" + + "github.com/ava-labs/subnet-evm/core" + "github.com/ava-labs/subnet-evm/core/txpool" + "github.com/ava-labs/subnet-evm/core/types" + "github.com/ava-labs/subnet-evm/eth" + "github.com/ava-labs/subnet-evm/eth/filters" + "github.com/ava-labs/subnet-evm/metrics" + "github.com/ava-labs/subnet-evm/plugin/evm/orderbook" + hu "github.com/ava-labs/subnet-evm/plugin/evm/orderbook/hubbleutils" + "github.com/ava-labs/subnet-evm/utils" + + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/snow" + "github.com/ethereum/go-ethereum/log" +) + +const ( + memoryDBSnapshotKey string = "memoryDBSnapshot" + snapshotInterval uint64 = 10 // save snapshot every 1000 blocks +) + +type LimitOrderProcesser interface { + ListenAndProcessTransactions(blockBuilder *blockBuilder) + GetOrderBookAPI() *orderbook.OrderBookAPI + GetTestingAPI() *orderbook.TestingAPI + GetTradingAPI() *orderbook.TradingAPI + RunMatchingPipeline() + GetMemoryDB() orderbook.LimitOrderDatabase + GetLimitOrderTxProcessor() orderbook.LimitOrderTxProcessor +} + +type limitOrderProcesser struct { + ctx *snow.Context + mu *sync.Mutex + txPool *txpool.TxPool + shutdownChan <-chan struct{} + shutdownWg *sync.WaitGroup + backend *eth.EthAPIBackend + blockChain *core.BlockChain + memoryDb orderbook.LimitOrderDatabase + limitOrderTxProcessor orderbook.LimitOrderTxProcessor + contractEventProcessor *orderbook.ContractEventsProcessor + matchingPipeline *orderbook.MatchingPipeline + filterAPI *filters.FilterAPI + hubbleDB database.Database + configService orderbook.IConfigService + blockBuilder *blockBuilder + isValidator bool + tradingAPIEnabled bool + loadFromSnapshotEnabled bool + snapshotSavedBlockNumber uint64 + snapshotFilePath string + tradingAPI *orderbook.TradingAPI +} + +func NewLimitOrderProcesser(ctx *snow.Context, txPool *txpool.TxPool, shutdownChan <-chan struct{}, shutdownWg *sync.WaitGroup, backend *eth.EthAPIBackend, blockChain *core.BlockChain, hubbleDB database.Database, validatorPrivateKey string, config Config) LimitOrderProcesser { + log.Info("**** NewLimitOrderProcesser") + configService := orderbook.NewConfigService(blockChain) + memoryDb := orderbook.NewInMemoryDatabase(configService) + lotp := orderbook.NewLimitOrderTxProcessor(txPool, memoryDb, backend, validatorPrivateKey) + signedObAddy := configService.GetSignedOrderbookContract() + contractEventProcessor := orderbook.NewContractEventsProcessor(memoryDb, signedObAddy) + + matchingPipeline := orderbook.NewMatchingPipeline(memoryDb, lotp, configService) + // if any of the following values are changed, the nodes will need to be restarted. + // This is also true for local testing. once contracts are deployed it's mandatory to restart the nodes + hu.SetChainIdAndVerifyingSignedOrdersContract(backend.ChainConfig().ChainID.Int64(), signedObAddy.String()) + + filterSystem := filters.NewFilterSystem(backend, filters.Config{}) + filterAPI := filters.NewFilterAPI(filterSystem) + + // need to register the types for gob encoding because memory DB has an interface field(ContractOrder) + // naming hu.LimitOrder as orderbook.LimitOrder instead of hubbleutils.LimitOrder because of backward compatibility. DO NOT CHANGE + // same for hu.IOCOrder + gob.RegisterName("*orderbook.LimitOrder", &hu.LimitOrder{}) + gob.RegisterName("*orderbook.IOCOrder", &hu.IOCOrder{}) + gob.Register(&hu.SignedOrder{}) + return &limitOrderProcesser{ + ctx: ctx, + mu: &sync.Mutex{}, + txPool: txPool, + shutdownChan: shutdownChan, + shutdownWg: shutdownWg, + backend: backend, + memoryDb: memoryDb, + hubbleDB: hubbleDB, + blockChain: blockChain, + limitOrderTxProcessor: lotp, + contractEventProcessor: contractEventProcessor, + matchingPipeline: matchingPipeline, + filterAPI: filterAPI, + configService: configService, + isValidator: config.IsValidator, + tradingAPIEnabled: config.TradingAPIEnabled, + loadFromSnapshotEnabled: config.LoadFromSnapshotEnabled, + snapshotFilePath: config.SnapshotFilePath, + } +} + +func (lop *limitOrderProcesser) ListenAndProcessTransactions(blockBuilder *blockBuilder) { + lop.mu.Lock() + + lastAccepted := lop.blockChain.LastAcceptedBlock() + lastAcceptedBlockNumber := lastAccepted.Number() + if lastAcceptedBlockNumber.Sign() > 0 { + fromBlock := big.NewInt(0) + + if lop.loadFromSnapshotEnabled { + // first load the last snapshot containing finalised data till block x and query the logs of [x+1, latest] + acceptedBlockNumber, err := lop.loadMemoryDBSnapshot() + if err != nil { + log.Error("ListenAndProcessTransactions - error in loading snapshot", "err", err) + } else { + if acceptedBlockNumber > 0 { + fromBlock = big.NewInt(int64(acceptedBlockNumber) + 1) + } else { + // not an error, but unlikely after the blockchain is running for some time + log.Warn("ListenAndProcessTransactions - no snapshot found") + } + } + } else { + log.Info("ListenAndProcessTransactions - loading from snapshot is disabled") + } + + logHandler := log.Root().GetHandler() + errorOnlyHandler := ErrorOnlyHandler(logHandler) + log.Info("ListenAndProcessTransactions - beginning sync", " till block number", lastAcceptedBlockNumber) + JUMP := big.NewInt(3999) + toBlock := utils.BigIntMin(lastAcceptedBlockNumber, big.NewInt(0).Add(fromBlock, JUMP)) + for toBlock.Cmp(fromBlock) > 0 { + logs := lop.getLogs(fromBlock, toBlock) + // set the log handler to discard logs so that the ProcessEvents doesn't spam the logs + log.Root().SetHandler(errorOnlyHandler) + lop.contractEventProcessor.ProcessEvents(logs) + lop.contractEventProcessor.ProcessAcceptedEvents(logs, true) + lop.memoryDb.Accept(toBlock.Uint64(), 0) // will delete stale orders from the memorydb + log.Root().SetHandler(logHandler) + log.Info("ListenAndProcessTransactions - processed log chunk", "fromBlock", fromBlock.String(), "toBlock", toBlock.String(), "number of logs", len(logs)) + + fromBlock = fromBlock.Add(toBlock, big.NewInt(1)) + toBlock = utils.BigIntMin(lastAcceptedBlockNumber, big.NewInt(0).Add(fromBlock, JUMP)) + } + lop.memoryDb.Accept(lastAcceptedBlockNumber.Uint64(), lastAccepted.Time()) // will delete stale orders from the memorydb + lop.snapshotSavedBlockNumber = lastAcceptedBlockNumber.Uint64() + log.Info("Set snapshotSavedBlockNumber", "snapshotSavedBlockNumber", lop.snapshotSavedBlockNumber) + log.Root().SetHandler(logHandler) + } + + lop.mu.Unlock() + + lop.blockBuilder = blockBuilder + lop.runMatchingTimer() + lop.listenAndStoreLimitOrderTransactions() +} + +func (lop *limitOrderProcesser) RunMatchingPipeline() { + if !lop.isValidator { + return + } + executeFuncAndRecoverPanic(func() { + matchesFound := lop.matchingPipeline.Run(new(big.Int).Add(lop.blockChain.CurrentBlock().Number, big.NewInt(1))) + if matchesFound { + lop.blockBuilder.signalTxsReady() + } + }, orderbook.RunMatchingPipelinePanicMessage, orderbook.RunMatchingPipelinePanicsCounter) +} + +func (lop *limitOrderProcesser) RunSanitaryPipeline() { + executeFuncAndRecoverPanic(func() { + lop.matchingPipeline.RunSanitization() + }, orderbook.RunSanitaryPipelinePanicMessage, orderbook.RunSanitaryPipelinePanicsCounter) +} + +func (lop *limitOrderProcesser) GetOrderBookAPI() *orderbook.OrderBookAPI { + return orderbook.NewOrderBookAPI(lop.memoryDb, lop.backend, lop.configService) +} + +func (lop *limitOrderProcesser) GetTradingAPI() *orderbook.TradingAPI { + if lop.tradingAPI == nil { + lop.tradingAPI = orderbook.NewTradingAPI(lop.memoryDb, lop.backend, lop.configService, lop.shutdownChan, lop.shutdownWg) + } + return lop.tradingAPI +} + +func (lop *limitOrderProcesser) GetTestingAPI() *orderbook.TestingAPI { + return orderbook.NewTestingAPI(lop.memoryDb, lop.backend, lop.configService, lop.hubbleDB) +} + +func (lop *limitOrderProcesser) GetMemoryDB() orderbook.LimitOrderDatabase { + return lop.memoryDb +} + +func (lop *limitOrderProcesser) GetLimitOrderTxProcessor() orderbook.LimitOrderTxProcessor { + return lop.limitOrderTxProcessor +} + +func (lop *limitOrderProcesser) listenAndStoreLimitOrderTransactions() { + logsCh := make(chan []*types.Log) + logsSubscription := lop.backend.SubscribeHubbleLogsEvent(logsCh) + lop.shutdownWg.Add(1) + go func() { + defer lop.shutdownWg.Done() + defer logsSubscription.Unsubscribe() + for { + select { + case logs := <-logsCh: + executeFuncAndRecoverPanic(func() { + lop.mu.Lock() + defer lop.mu.Unlock() + lop.contractEventProcessor.ProcessEvents(logs) + if lop.tradingAPIEnabled { + go lop.contractEventProcessor.PushToTraderFeed(logs, orderbook.ConfirmationLevelHead) + go lop.contractEventProcessor.PushToMarketFeed(logs, orderbook.ConfirmationLevelHead) + } + }, orderbook.HandleHubbleFeedLogsPanicMessage, orderbook.HandleHubbleFeedLogsPanicsCounter) + + lop.RunMatchingPipeline() + + case <-lop.shutdownChan: + return + } + } + }() + + acceptedLogsCh := make(chan []*types.Log) + acceptedLogsSubscription := lop.backend.SubscribeAcceptedLogsEvent(acceptedLogsCh) + lop.shutdownWg.Add(1) + go func() { + defer lop.shutdownWg.Done() + defer acceptedLogsSubscription.Unsubscribe() + + for { + select { + case logs := <-acceptedLogsCh: + executeFuncAndRecoverPanic(func() { + lop.mu.Lock() + defer lop.mu.Unlock() + + if len(logs) == 0 { + return + } + if lop.tradingAPIEnabled { + go lop.contractEventProcessor.PushToTraderFeed(logs, orderbook.ConfirmationLevelAccepted) + go lop.contractEventProcessor.PushToMarketFeed(logs, orderbook.ConfirmationLevelAccepted) + } + + blockNumber := logs[0].BlockNumber + block := lop.blockChain.GetBlockByHash(logs[0].BlockHash) + + // If n is the block at which snapshot should be saved(n is multiple of [snapshotInterval]), save the snapshot + // when logs of block number >= n + 1 are received before applying them in memory db + + // snapshot should be saved at block number = blockNumber - 1 because Accepted logs + // have been applied in memory DB at this point + snapshotBlockNumber := blockNumber - 1 + blockNumberFloor := ((snapshotBlockNumber) / snapshotInterval) * snapshotInterval + if blockNumberFloor > lop.snapshotSavedBlockNumber { + log.Info("Saving memory DB snapshot", "snapshotBlockNumber", snapshotBlockNumber, "current blockNumber", blockNumber, "blockNumberFloor", blockNumberFloor) + snapshotBlock := lop.blockChain.GetBlockByNumber(snapshotBlockNumber) + lop.memoryDb.Accept(snapshotBlockNumber, snapshotBlock.Timestamp()) + executeFuncAndRecoverPanic(func() { + err := lop.saveMemoryDBSnapshot(big.NewInt(int64(snapshotBlockNumber))) + if err != nil { + orderbook.SnapshotWriteFailuresCounter.Inc(1) + log.Error("Error in saving memory DB snapshot", "err", err, "snapshotBlockNumber", snapshotBlockNumber, "current blockNumber", blockNumber, "blockNumberFloor", blockNumberFloor) + } + }, orderbook.SaveSnapshotPanicMessage, orderbook.SaveSnapshotPanicsCounter) + } + + lop.contractEventProcessor.ProcessAcceptedEvents(logs, false) + lop.memoryDb.Accept(blockNumber, block.Timestamp()) + }, orderbook.HandleChainAcceptedLogsPanicMessage, orderbook.HandleChainAcceptedLogsPanicsCounter) + case <-lop.shutdownChan: + return + } + } + }() + + chainAcceptedEventCh := make(chan core.ChainEvent) + chainAcceptedEventSubscription := lop.backend.SubscribeChainAcceptedEvent(chainAcceptedEventCh) + lop.shutdownWg.Add(1) + go func() { + defer lop.shutdownWg.Done() + defer chainAcceptedEventSubscription.Unsubscribe() + + for { + select { + case chainAcceptedEvent := <-chainAcceptedEventCh: + executeFuncAndRecoverPanic(func() { + lop.mu.Lock() + defer lop.mu.Unlock() + block := chainAcceptedEvent.Block + log.Info("received ChainAcceptedEvent", "number", block.NumberU64(), "hash", block.Hash().String()) + + // update metrics asynchronously + go lop.limitOrderTxProcessor.UpdateMetrics(block) + + }, orderbook.HandleChainAcceptedEventPanicMessage, orderbook.HandleChainAcceptedEventPanicsCounter) + case <-lop.shutdownChan: + return + } + } + }() + + chainHeadEventCh := make(chan core.ChainHeadEvent) + chainHeadEventSubscription := lop.backend.SubscribeChainHeadEvent(chainHeadEventCh) + lop.shutdownWg.Add(1) + go func() { + defer lop.shutdownWg.Done() + defer chainHeadEventSubscription.Unsubscribe() + + for { + select { + case chainHeadEvent := <-chainHeadEventCh: + block := chainHeadEvent.Block + log.Info("received ChainHeadEvent", "number", block.NumberU64(), "hash", block.Hash().String()) + case <-lop.shutdownChan: + return + } + } + }() +} + +// executes the matching pipeline periodically +func (lop *limitOrderProcesser) runMatchingTimer() { + lop.shutdownWg.Add(1) + go executeFuncAndRecoverPanic(func() { + defer lop.shutdownWg.Done() + + for { + select { + case <-lop.matchingPipeline.MatchingTicker.C: + lop.RunMatchingPipeline() + + case <-lop.matchingPipeline.SanitaryTicker.C: + lop.RunSanitaryPipeline() + + case <-lop.shutdownChan: + lop.matchingPipeline.MatchingTicker.Stop() + lop.matchingPipeline.SanitaryTicker.Stop() + return + } + } + }, orderbook.RunMatchingPipelinePanicMessage, orderbook.RunMatchingPipelinePanicsCounter) +} + +func (lop *limitOrderProcesser) loadMemoryDBSnapshot() (acceptedBlockNumber uint64, err error) { + acceptedBlockNumber, err = lop.loadMemoryDBSnapshotFromFile() + if err != nil || acceptedBlockNumber == 0 { + acceptedBlockNumber, err = lop.loadMemoryDBSnapshotFromHubbleDB() + } + return acceptedBlockNumber, err +} + +func (lop *limitOrderProcesser) loadMemoryDBSnapshotFromHubbleDB() (uint64, error) { + snapshotFound, err := lop.hubbleDB.Has([]byte(memoryDBSnapshotKey)) + if err != nil { + return 0, fmt.Errorf("Error in checking snapshot in hubbleDB: err=%v", err) + } + + if !snapshotFound { + return 0, nil + } + + memorySnapshotBytes, err := lop.hubbleDB.Get([]byte(memoryDBSnapshotKey)) + if err != nil { + return 0, fmt.Errorf("Error in fetching snapshot from hubbleDB; err=%v", err) + } + + buf := bytes.NewBuffer(memorySnapshotBytes) + var snapshot orderbook.Snapshot + err = gob.NewDecoder(buf).Decode(&snapshot) + if err != nil { + return 0, fmt.Errorf("Error in snapshot parsing from hubbleDB; err=%v", err) + } + + if snapshot.AcceptedBlockNumber != nil && snapshot.AcceptedBlockNumber.Uint64() > 0 { + err = lop.memoryDb.LoadFromSnapshot(snapshot) + if err != nil { + return 0, fmt.Errorf("Error in loading snapshot from hubbleDB: err=%v", err) + } else { + log.Info("memory DB snapshot loaded from hubbleDB", "acceptedBlockNumber", snapshot.AcceptedBlockNumber) + return snapshot.AcceptedBlockNumber.Uint64(), nil + } + } else { + return 0, nil + } +} + +func (lop *limitOrderProcesser) loadMemoryDBSnapshotFromFile() (uint64, error) { + if lop.snapshotFilePath == "" { + return 0, fmt.Errorf("snapshot file path not set") + } + + memorySnapshotBytes, err := os.ReadFile(lop.snapshotFilePath) + if err != nil { + return 0, fmt.Errorf("Error in reading snapshot file: err=%v", err) + } + + buf := bytes.NewBuffer(memorySnapshotBytes) + var snapshot orderbook.Snapshot + err = gob.NewDecoder(buf).Decode(&snapshot) + if err != nil { + return 0, fmt.Errorf("Error in snapshot parsing from file; err=%v", err) + } + + if snapshot.AcceptedBlockNumber != nil && snapshot.AcceptedBlockNumber.Uint64() > 0 { + err = lop.memoryDb.LoadFromSnapshot(snapshot) + if err != nil { + return 0, fmt.Errorf("Error in loading snapshot from file: err=%v", err) + } else { + log.Info("memory DB snapshot loaded from file", "acceptedBlockNumber", snapshot.AcceptedBlockNumber) + } + + return snapshot.AcceptedBlockNumber.Uint64(), nil + } else { + return 0, nil + } +} + +// assumes that memory DB lock is held +func (lop *limitOrderProcesser) saveMemoryDBSnapshot(acceptedBlockNumber *big.Int) error { + start := time.Now() + currentHeadBlock := lop.blockChain.CurrentBlock() + + if lop.snapshotFilePath == "" { + return fmt.Errorf("snapshot file path not set") + } + + memoryDBCopy, err := lop.memoryDb.GetOrderBookDataCopy() + if err != nil { + return fmt.Errorf("Error in getting memory DB copy: err=%v", err) + } + if currentHeadBlock.Number.Cmp(acceptedBlockNumber) == 1 { + // if current head is ahead of the accepted block, then certain events(OrderBook) + // need to be removed from the saved state + logsToRemove := []*types.Log{} + for { + logs := lop.blockChain.GetLogs(currentHeadBlock.Hash(), currentHeadBlock.Number.Uint64()) + flattenedLogs := types.FlattenLogs(logs) + logsToRemove = append(logsToRemove, flattenedLogs...) + + currentHeadBlock = lop.blockChain.GetHeaderByHash(currentHeadBlock.ParentHash) + if currentHeadBlock.Number.Cmp(acceptedBlockNumber) == 0 { + break + } + } + + for i := 0; i < len(logsToRemove); i++ { + logsToRemove[i].Removed = true + } + + cev := orderbook.NewContractEventsProcessor(memoryDBCopy, lop.configService.GetSignedOrderbookContract()) + // @todo: find a way to not log events removal here(add a logger in ContractEventsProcessor struct and use that to log everything) + cev.ProcessEvents(logsToRemove) + } + + // these SHOULD be re-populated while loading from snapshot + memoryDBCopy.LongOrders = nil + memoryDBCopy.ShortOrders = nil + snapshot := orderbook.Snapshot{ + Data: memoryDBCopy, + AcceptedBlockNumber: acceptedBlockNumber, + } + + var buf bytes.Buffer + err = gob.NewEncoder(&buf).Encode(&snapshot) + if err != nil { + return fmt.Errorf("error in gob encoding: err=%v", err) + } + + snapshotBytes := buf.Bytes() + // write to snapshot file + err = os.WriteFile(lop.snapshotFilePath, snapshotBytes, 0644) + if err != nil { + return fmt.Errorf("Error in writing to snapshot file: err=%v", err) + } + + lop.snapshotSavedBlockNumber = acceptedBlockNumber.Uint64() + log.Info("Saved memory DB snapshot successfully", "accepted block", acceptedBlockNumber, "head block number", currentHeadBlock.Number, "head block hash", currentHeadBlock.Hash(), "duration", time.Since(start)) + + return nil +} + +func (lop *limitOrderProcesser) getLogs(fromBlock, toBlock *big.Int) []*types.Log { + ctx := context.Background() + logs, err := lop.filterAPI.GetLogs(ctx, filters.FilterCriteria{ + FromBlock: fromBlock, + ToBlock: toBlock, + }) + + if err != nil { + log.Error("ListenAndProcessTransactions - GetLogs failed", "err", err) + panic(err) + } + return logs +} + +func executeFuncAndRecoverPanic(fn func(), panicMessage string, panicCounter metrics.Counter) { + defer func() { + if panicInfo := recover(); panicInfo != nil { + var errorMessage string + switch panicInfo := panicInfo.(type) { + case string: + errorMessage = fmt.Sprintf("recovered (string) panic: %s", panicInfo) + case runtime.Error: + errorMessage = fmt.Sprintf("recovered (runtime.Error) panic: %s", panicInfo.Error()) + case error: + errorMessage = fmt.Sprintf("recovered (error) panic: %s", panicInfo.Error()) + default: + errorMessage = fmt.Sprintf("recovered (default) panic: %v", panicInfo) + } + + log.Error(panicMessage, "errorMessage", errorMessage, "stack_trace", string(debug.Stack())) + panicCounter.Inc(1) + orderbook.AllPanicsCounter.Inc(1) + } + }() + fn() +} diff --git a/plugin/evm/log.go b/plugin/evm/log.go index fcc70d6525..77f50abdf5 100644 --- a/plugin/evm/log.go +++ b/plugin/evm/log.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "reflect" + "strings" "time" "github.com/ethereum/go-ethereum/log" @@ -15,7 +16,7 @@ import ( const ( errorKey = "LOG15_ERROR" - timeFormat = "2006-01-02T15:04:05-0700" + timeFormat = "2006-01-02T15:04:05.000000-0700" ) type SubnetEVMLogger struct { @@ -25,13 +26,17 @@ type SubnetEVMLogger struct { // InitLogger initializes logger with alias and sets the log level and format with the original [os.StdErr] interface // along with the context logger. func InitLogger(alias string, level string, jsonFormat bool, writer io.Writer) (SubnetEVMLogger, error) { - logFormat := SubnetEVMTermFormat(alias) + // logFormat := SubnetEVMTermFormat(alias) + logFormat := LogfmtFormat() if jsonFormat { logFormat = SubnetEVMJSONFormat(alias) } // Create handler logHandler := log.StreamHandler(writer, logFormat) + logHandler = log.CallerFileHandler(logHandler) + logHandler = HubbleTypeHandler(logHandler) + logHandler = HubbleErrorHandler(logHandler) c := SubnetEVMLogger{Handler: logHandler} if err := c.SetLogLevel(level); err != nil { @@ -94,6 +99,44 @@ func SubnetEVMJSONFormat(alias string) log.Format { }) } +func HubbleTypeHandler(h log.Handler) log.Handler { + return log.FuncHandler(func(r *log.Record) error { + var logType string + // works for evm/limit_order.go, evm/orderbook/*.go, precompile/contracts/* + if containsAnySubstr(r.Call.Frame().File, []string{"orderbook", "limit_order", "contracts"}) { + logType = "hubble" + } else { + logType = "system" + } + // it's also possible to add type=hubble in logs originating from other files + // by setting logtype=hubble and checking for it in this function by iterating through r.Ctx + r.Ctx = append(r.Ctx, "type", logType) + return h.Log(r) + }) +} + +func HubbleErrorHandler(h log.Handler) log.Handler { + // sets lvl=error when key name is "err" and value is not nil + return log.FuncHandler(func(r *log.Record) error { + for i := 0; i < len(r.Ctx); i += 2 { + if r.Ctx[i] == "err" && r.Ctx[i+1] != nil { + r.Lvl = log.LvlError + } + } + return h.Log(r) + }) +} + +func ErrorOnlyHandler(h log.Handler) log.Handler { + // ignores all logs except lvl=error + return log.FuncHandler(func(r *log.Record) error { + if r.Lvl == log.LvlError { + return h.Log(r) + } + return nil + }) +} + func formatJSONValue(value interface{}) (result interface{}) { defer func() { if err := recover(); err != nil { @@ -119,3 +162,13 @@ func formatJSONValue(value interface{}) (result interface{}) { return v } } + +// containsAnySubstr checks if the string contains any of the specified substrings +func containsAnySubstr(s string, substrings []string) bool { + for _, substr := range substrings { + if strings.Contains(s, substr) { + return true + } + } + return false +} diff --git a/plugin/evm/logfmt.go b/plugin/evm/logfmt.go new file mode 100644 index 0000000000..56f5917aa0 --- /dev/null +++ b/plugin/evm/logfmt.go @@ -0,0 +1,179 @@ +package evm + +import ( + "bytes" + "fmt" + "math/big" + "reflect" + "strconv" + "sync" + "time" + "unicode/utf8" + + "github.com/ethereum/go-ethereum/log" +) + +const ( + termCtxMaxPadding = 40 + floatFormat = 'f' +) + +// fieldPadding is a global map with maximum field value lengths seen until now +// to allow padding log contexts in a bit smarter way. +var fieldPadding = make(map[string]int) + +// fieldPaddingLock is a global mutex protecting the field padding map. +var fieldPaddingLock sync.RWMutex + +func LogfmtFormat() log.Format { + return log.FormatFunc(func(r *log.Record) []byte { + common := []interface{}{r.KeyNames.Time, r.Time, r.KeyNames.Lvl, r.Lvl, r.KeyNames.Msg, r.Msg} + buf := &bytes.Buffer{} + logfmt(buf, append(common, r.Ctx...), 0, false) + return buf.Bytes() + }) +} + +func logfmt(buf *bytes.Buffer, ctx []interface{}, color int, term bool) { + for i := 0; i < len(ctx); i += 2 { + if i != 0 { + buf.WriteByte(' ') + } + + k, ok := ctx[i].(string) + v := formatLogfmtValue(ctx[i+1], term) + if !ok { + k, v = errorKey, formatLogfmtValue(k, term) + } + + // XXX: we should probably check that all of your key bytes aren't invalid + fieldPaddingLock.RLock() + padding := fieldPadding[k] + fieldPaddingLock.RUnlock() + + length := utf8.RuneCountInString(v) + if padding < length && length <= termCtxMaxPadding { + padding = length + + fieldPaddingLock.Lock() + fieldPadding[k] = padding + fieldPaddingLock.Unlock() + } + if color > 0 { + fmt.Fprintf(buf, "\x1b[%dm%s\x1b[0m=", color, k) + } else { + buf.WriteString(k) + buf.WriteByte('=') + } + buf.WriteString(v) + if i < len(ctx)-2 && padding > length { + buf.Write(bytes.Repeat([]byte{' '}, padding-length)) + } + } + buf.WriteByte('\n') +} + +// formatValue formats a value for serialization +func formatLogfmtValue(value interface{}, term bool) string { + if value == nil { + return "nil" + } + + switch v := value.(type) { + case time.Time: + // Performance optimization: No need for escaping since the provided + // timeFormat doesn't have any escape characters, and escaping is + // expensive. + return v.Format(timeFormat) + + case *big.Int: + // Big ints get consumed by the Stringer clause so we need to handle + // them earlier on. + if v == nil { + return "" + } + return v.String() + } + if term { + if s, ok := value.(log.TerminalStringer); ok { + // Custom terminal stringer provided, use that + return escapeString(s.TerminalString()) + } + } + value = formatShared(value) + switch v := value.(type) { + case bool: + return strconv.FormatBool(v) + case float32: + return strconv.FormatFloat(float64(v), floatFormat, 3, 64) + case float64: + return strconv.FormatFloat(v, floatFormat, 3, 64) + case int8: + return strconv.FormatInt(int64(v), 10) + case uint8: + return strconv.FormatInt(int64(v), 10) + case int16: + return strconv.FormatInt(int64(v), 10) + case uint16: + return strconv.FormatInt(int64(v), 10) + case int: + return strconv.FormatInt(int64(v), 10) + case int32: + return strconv.FormatInt(int64(v), 10) + case int64: + return strconv.FormatInt(v, 10) + case uint: + return strconv.FormatUint(uint64(v), 10) + case uint32: + return strconv.FormatUint(uint64(v), 10) + case uint64: + return strconv.FormatUint(v, 10) + case string: + return escapeString(v) + default: + return escapeString(fmt.Sprintf("%+v", value)) + } +} + +func formatShared(value interface{}) (result interface{}) { + defer func() { + if err := recover(); err != nil { + if v := reflect.ValueOf(value); v.Kind() == reflect.Ptr && v.IsNil() { + result = "nil" + } else { + panic(err) + } + } + }() + + switch v := value.(type) { + case time.Time: + return v.Format(timeFormat) + + case error: + return v.Error() + + case fmt.Stringer: + return v.String() + + default: + return v + } +} + +// escapeString checks if the provided string needs escaping/quoting, and +// calls strconv.Quote if needed +func escapeString(s string) string { + needsQuoting := false + for _, r := range s { + // We quote everything below " (0x34) and above~ (0x7E), plus equal-sign + if r <= '"' || r > '~' || r == '=' { + needsQuoting = true + break + } + } + if !needsQuoting { + return s + } + return strconv.Quote(s) +} diff --git a/plugin/evm/message/codec.go b/plugin/evm/message/codec.go index 48b1436f7d..4250518bdd 100644 --- a/plugin/evm/message/codec.go +++ b/plugin/evm/message/codec.go @@ -30,6 +30,8 @@ func init() { errs.Add( // Gossip types c.RegisterType(EthTxsGossip{}), + // ordering of gossip types is important + c.RegisterType(SignedOrdersGossip{}), // Types for state sync frontier consensus c.RegisterType(SyncSummary{}), diff --git a/plugin/evm/message/handler.go b/plugin/evm/message/handler.go index bb1fd3f05e..d2ff2c5afb 100644 --- a/plugin/evm/message/handler.go +++ b/plugin/evm/message/handler.go @@ -19,6 +19,7 @@ var ( // GossipHandler handles incoming gossip messages type GossipHandler interface { + HandleSignedOrders(nodeID ids.NodeID, msg SignedOrdersGossip) error HandleEthTxs(nodeID ids.NodeID, msg EthTxsGossip) error } @@ -29,6 +30,11 @@ func (NoopMempoolGossipHandler) HandleEthTxs(nodeID ids.NodeID, msg EthTxsGossip return nil } +func (NoopMempoolGossipHandler) HandleSignedOrders(nodeID ids.NodeID, _ SignedOrdersGossip) error { + log.Debug("dropping unexpected SignedOrdersGossip message", "peerID", nodeID) + return nil +} + // RequestHandler interface handles incoming requests from peers // Must have methods in format of handleType(context.Context, ids.NodeID, uint32, request Type) error // so that the Request object of relevant Type can invoke its respective handle method diff --git a/plugin/evm/message/handler_test.go b/plugin/evm/message/handler_test.go index 8b87135ff5..7819f3e7a8 100644 --- a/plugin/evm/message/handler_test.go +++ b/plugin/evm/message/handler_test.go @@ -12,6 +12,7 @@ import ( ) type CounterHandler struct { + Orders int EthTxs int } @@ -20,6 +21,11 @@ func (h *CounterHandler) HandleEthTxs(ids.NodeID, EthTxsGossip) error { return nil } +func (h *CounterHandler) HandleSignedOrders(ids.NodeID, SignedOrdersGossip) error { + h.Orders++ + return nil +} + func TestHandleEthTxs(t *testing.T) { assert := assert.New(t) diff --git a/plugin/evm/message/message.go b/plugin/evm/message/message.go index 35887911c9..f22fbd86ac 100644 --- a/plugin/evm/message/message.go +++ b/plugin/evm/message/message.go @@ -39,6 +39,10 @@ type EthTxsGossip struct { Txs []byte `serialize:"true"` } +type SignedOrdersGossip struct { + Orders []byte `serialize:"true"` +} + func (msg EthTxsGossip) Handle(handler GossipHandler, nodeID ids.NodeID) error { return handler.HandleEthTxs(nodeID, msg) } @@ -47,6 +51,14 @@ func (msg EthTxsGossip) String() string { return fmt.Sprintf("EthTxsGossip(Len=%d)", len(msg.Txs)) } +func (msg SignedOrdersGossip) Handle(handler GossipHandler, nodeID ids.NodeID) error { + return handler.HandleSignedOrders(nodeID, msg) +} + +func (msg SignedOrdersGossip) String() string { + return fmt.Sprintf("SignedOrdersGossip(BytesLen=%d)", len(msg.Orders)) +} + func ParseGossipMessage(codec codec.Manager, bytes []byte) (GossipMessage, error) { var msg GossipMessage version, err := codec.Unmarshal(bytes, &msg) diff --git a/plugin/evm/order_api.go b/plugin/evm/order_api.go new file mode 100644 index 0000000000..6b16027290 --- /dev/null +++ b/plugin/evm/order_api.go @@ -0,0 +1,78 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package evm + +import ( + "context" + "encoding/hex" + "encoding/json" + "strings" + + "github.com/ava-labs/subnet-evm/plugin/evm/orderbook" + hu "github.com/ava-labs/subnet-evm/plugin/evm/orderbook/hubbleutils" +) + +type OrderAPI struct { + tradingAPI *orderbook.TradingAPI + vm *VM +} + +func NewOrderAPI(tradingAPI *orderbook.TradingAPI, vm *VM) *OrderAPI { + return &OrderAPI{ + tradingAPI: tradingAPI, + vm: vm, + } +} + +type PlaceOrderResponse struct { + OrderId string `json:"orderId,omitempty"` + Success bool `json:"success"` + Error string `json:"error,omitempty"` +} + +type PlaceSignedOrdersResponse struct { + Orders []PlaceOrderResponse `json:"orders"` +} + +func (api *OrderAPI) PlaceSignedOrders(ctx context.Context, input string) (PlaceSignedOrdersResponse, error) { + // input is a json encoded array of signed orders + var rawOrders []string + err := json.Unmarshal([]byte(input), &rawOrders) + if err != nil { + return PlaceSignedOrdersResponse{}, err + } + + ordersToGossip := []*hu.SignedOrder{} + response := []PlaceOrderResponse{} + for _, rawOrder := range rawOrders { + orderResponse := PlaceOrderResponse{Success: false} + testData, err := hex.DecodeString(strings.TrimPrefix(rawOrder, "0x")) + if err != nil { + orderResponse.Error = err.Error() + response = append(response, orderResponse) + continue + } + order, err := hu.DecodeSignedOrder(testData) + if err != nil { + orderResponse.Error = err.Error() + response = append(response, orderResponse) + continue + } + + orderId, _, err := api.tradingAPI.PlaceOrder(order) + orderResponse.OrderId = orderId.String() + if err != nil { + orderResponse.Error = err.Error() + response = append(response, orderResponse) + continue + } + orderResponse.Success = true + response = append(response, orderResponse) + ordersToGossip = append(ordersToGossip, order) + } + + api.vm.orderGossiper.GossipSignedOrders(ordersToGossip) + + return PlaceSignedOrdersResponse{Orders: response}, nil +} diff --git a/plugin/evm/orderbook/abis/ClearingHouse.go b/plugin/evm/orderbook/abis/ClearingHouse.go new file mode 100644 index 0000000000..15436f3631 --- /dev/null +++ b/plugin/evm/orderbook/abis/ClearingHouse.go @@ -0,0 +1,1290 @@ +package abis + +var ClearingHouseAbi = []byte(`{"abi": [ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "idx", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "takerFundingPayment", + "type": "int256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "cumulativePremiumFraction", + "type": "int256" + } + ], + "name": "FundingPaid", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "idx", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "premiumFraction", + "type": "int256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "underlyingPrice", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "cumulativePremiumFraction", + "type": "int256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "nextFundingTime", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + } + ], + "name": "FundingRateUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "version", + "type": "uint8" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "idx", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "amm", + "type": "address" + } + ], + "name": "MarketAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "nextSampleTime", + "type": "uint256" + } + ], + "name": "NotifyNextPISample", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "idx", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + } + ], + "name": "PISampleSkipped", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "idx", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "premiumIndex", + "type": "int256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + } + ], + "name": "PISampledUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "idx", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "baseAsset", + "type": "int256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "realizedPnl", + "type": "int256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "size", + "type": "int256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "openNotional", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "fee", + "type": "int256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "name": "PositionLiquidated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "idx", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "baseAsset", + "type": "int256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "realizedPnl", + "type": "int256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "size", + "type": "int256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "openNotional", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "fee", + "type": "int256" + }, + { + "indexed": false, + "internalType": "enum IClearingHouse.OrderExecutionMode", + "name": "mode", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "name": "PositionModified", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "referrer", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "referralBonus", + "type": "uint256" + } + ], + "name": "ReferralBonusAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "inputs": [], + "name": "LIQUIDATION_FAILED", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "amms", + "outputs": [ + { + "internalType": "contract IAMM", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + } + ], + "name": "assertMarginRequirement", + "outputs": [], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "bool", + "name": "includeFundingPayments", + "type": "bool" + }, + { + "internalType": "enum IClearingHouse.Mode", + "name": "mode", + "type": "uint8" + } + ], + "name": "calcMarginFraction", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256[]", + "name": "impactBids", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "impactAsks", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "midPrice", + "type": "uint256[]" + } + ], + "name": "commitLiquiditySample", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "defaultOrderBook", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "feeSink", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getAMMs", + "outputs": [ + { + "internalType": "contract IAMM[]", + "name": "", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getAmmsLength", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "bool", + "name": "includeFundingPayments", + "type": "bool" + }, + { + "internalType": "enum IClearingHouse.Mode", + "name": "mode", + "type": "uint8" + } + ], + "name": "getNotionalPositionAndMargin", + "outputs": [ + { + "internalType": "uint256", + "name": "notionalPosition", + "type": "uint256" + }, + { + "internalType": "int256", + "name": "margin", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "bool", + "name": "includeFundingPayments", + "type": "bool" + }, + { + "internalType": "enum IClearingHouse.Mode", + "name": "mode", + "type": "uint8" + } + ], + "name": "getNotionalPositionAndMarginVanilla", + "outputs": [ + { + "internalType": "uint256", + "name": "notionalPosition", + "type": "uint256" + }, + { + "internalType": "int256", + "name": "margin", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + } + ], + "name": "getTotalFunding", + "outputs": [ + { + "internalType": "int256", + "name": "totalFunding", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "enum IClearingHouse.Mode", + "name": "mode", + "type": "uint8" + } + ], + "name": "getTotalNotionalPositionAndUnrealizedPnl", + "outputs": [ + { + "internalType": "uint256", + "name": "notionalPosition", + "type": "uint256" + }, + { + "internalType": "int256", + "name": "unrealizedPnl", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getUnderlyingPrice", + "outputs": [ + { + "internalType": "uint256[]", + "name": "prices", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "governance", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "hubbleReferral", + "outputs": [ + { + "internalType": "contract IHubbleReferral", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_governance", + "type": "address" + }, + { + "internalType": "address", + "name": "_feeSink", + "type": "address" + }, + { + "internalType": "address", + "name": "_marginAccount", + "type": "address" + }, + { + "internalType": "address", + "name": "_defaultOrderBook", + "type": "address" + }, + { + "internalType": "address", + "name": "_vusd", + "type": "address" + }, + { + "internalType": "address", + "name": "_hubbleReferral", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + } + ], + "name": "isAboveMaintenanceMargin", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "isWhitelistedOrderBook", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "juror", + "outputs": [ + { + "internalType": "contract IJuror", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "lastFundingPaid", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "lastFundingTime", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "internalType": "enum IClearingHouse.OrderExecutionMode", + "name": "mode", + "type": "uint8" + } + ], + "internalType": "struct IClearingHouse.Instruction", + "name": "instruction", + "type": "tuple" + }, + { + "internalType": "int256", + "name": "liquidationAmount", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + } + ], + "name": "liquidate", + "outputs": [ + { + "internalType": "uint256", + "name": "openInterest", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "int256", + "name": "toLiquidate", + "type": "int256" + } + ], + "name": "liquidateSingleAmm", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "liquidationPenalty", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "maintenanceMargin", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "makerFee", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "marginAccount", + "outputs": [ + { + "internalType": "contract IMarginAccount", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "minAllowableMargin", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "nextSampleTime", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "internalType": "enum IClearingHouse.OrderExecutionMode", + "name": "mode", + "type": "uint8" + } + ], + "internalType": "struct IClearingHouse.Instruction[2]", + "name": "orders", + "type": "tuple[2]" + }, + { + "internalType": "int256", + "name": "fillAmount", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "fulfillPrice", + "type": "uint256" + } + ], + "name": "openComplementaryPositions", + "outputs": [ + { + "internalType": "uint256", + "name": "openInterest", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "internalType": "enum IClearingHouse.OrderExecutionMode", + "name": "mode", + "type": "uint8" + } + ], + "internalType": "struct IClearingHouse.Instruction", + "name": "order", + "type": "tuple" + }, + { + "internalType": "int256", + "name": "fillAmount", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "fulfillPrice", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "is2ndTrade", + "type": "bool" + } + ], + "name": "openPosition", + "outputs": [ + { + "internalType": "uint256", + "name": "openInterest", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "orderBook", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "paused", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "referralShare", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_feeSink", + "type": "address" + } + ], + "name": "setFeeSink", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "__governance", + "type": "address" + } + ], + "name": "setGovernace", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_juror", + "type": "address" + } + ], + "name": "setJuror", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_orderBook", + "type": "address" + }, + { + "internalType": "bool", + "name": "_status", + "type": "bool" + } + ], + "name": "setOrderBookWhitelist", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "int256", + "name": "_maintenanceMargin", + "type": "int256" + }, + { + "internalType": "int256", + "name": "_minAllowableMargin", + "type": "int256" + }, + { + "internalType": "int256", + "name": "_takerFee", + "type": "int256" + }, + { + "internalType": "int256", + "name": "_makerFee", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "_referralShare", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_tradingFeeDiscount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_liquidationPenalty", + "type": "uint256" + } + ], + "name": "setParams", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_referral", + "type": "address" + } + ], + "name": "setReferral", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "settleFunding", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "takerFee", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "tradingFeeDiscount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + } + ], + "name": "updatePositions", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "vusd", + "outputs": [ + { + "internalType": "contract VUSD", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_amm", + "type": "address" + } + ], + "name": "whitelistAmm", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +]}`) diff --git a/plugin/evm/orderbook/abis/IOCOrderBook.go b/plugin/evm/orderbook/abis/IOCOrderBook.go new file mode 100644 index 0000000000..178f895172 --- /dev/null +++ b/plugin/evm/orderbook/abis/IOCOrderBook.go @@ -0,0 +1,339 @@ +package abis + +var IOCOrderBookAbi = []byte(`{"abi": [ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "uint8", + "name": "orderType", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "expireAt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "int256", + "name": "baseAssetQuantity", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "salt", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "reduceOnly", + "type": "bool" + } + ], + "indexed": false, + "internalType": "struct IImmediateOrCancelOrders.Order", + "name": "order", + "type": "tuple" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "name": "OrderAccepted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "uint8", + "name": "orderType", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "expireAt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "int256", + "name": "baseAssetQuantity", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "salt", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "reduceOnly", + "type": "bool" + } + ], + "indexed": false, + "internalType": "struct IImmediateOrCancelOrders.Order", + "name": "order", + "type": "tuple" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "string", + "name": "err", + "type": "string" + } + ], + "name": "OrderRejected", + "type": "event" + }, + { + "inputs": [], + "name": "expirationCap", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint8", + "name": "orderType", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "expireAt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "int256", + "name": "baseAssetQuantity", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "salt", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "reduceOnly", + "type": "bool" + } + ], + "internalType": "struct IImmediateOrCancelOrders.Order", + "name": "order", + "type": "tuple" + } + ], + "name": "getOrderHash", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + } + ], + "name": "orderStatus", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "blockPlaced", + "type": "uint256" + }, + { + "internalType": "int256", + "name": "filledAmount", + "type": "int256" + }, + { + "internalType": "enum IOrderHandler.OrderStatus", + "name": "status", + "type": "uint8" + } + ], + "internalType": "struct IImmediateOrCancelOrders.OrderInfo", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint8", + "name": "orderType", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "expireAt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "int256", + "name": "baseAssetQuantity", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "salt", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "reduceOnly", + "type": "bool" + } + ], + "internalType": "struct IImmediateOrCancelOrders.Order[]", + "name": "orders", + "type": "tuple[]" + } + ], + "name": "placeOrders", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "metadata", + "type": "bytes" + } + ], + "name": "updateOrder", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +]}`) diff --git a/plugin/evm/orderbook/abis/LimitOrderBook.go b/plugin/evm/orderbook/abis/LimitOrderBook.go new file mode 100644 index 0000000000..5a6d1f2207 --- /dev/null +++ b/plugin/evm/orderbook/abis/LimitOrderBook.go @@ -0,0 +1,497 @@ +package abis + +var LimitOrderBookAbi = []byte(`{"abi": [ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "int256", + "name": "baseAssetQuantity", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "salt", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "reduceOnly", + "type": "bool" + }, + { + "internalType": "bool", + "name": "postOnly", + "type": "bool" + } + ], + "indexed": false, + "internalType": "struct ILimitOrderBook.Order", + "name": "order", + "type": "tuple" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "name": "OrderAccepted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bool", + "name": "isAutoCancelled", + "type": "bool" + } + ], + "name": "OrderCancelAccepted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "string", + "name": "err", + "type": "string" + } + ], + "name": "OrderCancelRejected", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "int256", + "name": "baseAssetQuantity", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "salt", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "reduceOnly", + "type": "bool" + }, + { + "internalType": "bool", + "name": "postOnly", + "type": "bool" + } + ], + "indexed": false, + "internalType": "struct ILimitOrderBook.Order", + "name": "order", + "type": "tuple" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "string", + "name": "err", + "type": "string" + } + ], + "name": "OrderRejected", + "type": "event" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "int256", + "name": "baseAssetQuantity", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "salt", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "reduceOnly", + "type": "bool" + }, + { + "internalType": "bool", + "name": "postOnly", + "type": "bool" + } + ], + "internalType": "struct ILimitOrderBook.Order[]", + "name": "orders", + "type": "tuple[]" + } + ], + "name": "cancelOrders", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "int256", + "name": "baseAssetQuantity", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "salt", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "reduceOnly", + "type": "bool" + }, + { + "internalType": "bool", + "name": "postOnly", + "type": "bool" + } + ], + "internalType": "struct ILimitOrderBook.Order[]", + "name": "orders", + "type": "tuple[]" + } + ], + "name": "cancelOrdersWithLowMargin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "int256", + "name": "baseAssetQuantity", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "salt", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "reduceOnly", + "type": "bool" + }, + { + "internalType": "bool", + "name": "postOnly", + "type": "bool" + } + ], + "internalType": "struct ILimitOrderBook.Order", + "name": "order", + "type": "tuple" + } + ], + "name": "getOrderHash", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + } + ], + "name": "orderStatus", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "blockPlaced", + "type": "uint256" + }, + { + "internalType": "int256", + "name": "filledAmount", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "reservedMargin", + "type": "uint256" + }, + { + "internalType": "enum IOrderHandler.OrderStatus", + "name": "status", + "type": "uint8" + } + ], + "internalType": "struct ILimitOrderBook.OrderInfo", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "int256", + "name": "baseAssetQuantity", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "salt", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "reduceOnly", + "type": "bool" + }, + { + "internalType": "bool", + "name": "postOnly", + "type": "bool" + } + ], + "internalType": "struct ILimitOrderBook.Order[]", + "name": "orders", + "type": "tuple[]" + } + ], + "name": "placeOrders", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + } + ], + "name": "reduceOnlyAmount", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "metadata", + "type": "bytes" + } + ], + "name": "updateOrder", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ]}`) diff --git a/plugin/evm/orderbook/abis/MarginAccount.go b/plugin/evm/orderbook/abis/MarginAccount.go new file mode 100644 index 0000000000..be623dc5db --- /dev/null +++ b/plugin/evm/orderbook/abis/MarginAccount.go @@ -0,0 +1,1095 @@ +package abis + +var MarginAccountAbi = []byte(`{"abi": [ + { + "inputs": [ + { + "internalType": "address", + "name": "_trustedForwarder", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "enum IMarginAccount.LiquidationStatus", + "name": "", + "type": "uint8" + } + ], + "name": "NOT_LIQUIDATABLE", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "idx", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "seizeAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "repayAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "name": "MarginAccountLiquidated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "idx", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "name": "MarginAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "MarginReleased", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "idx", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "name": "MarginRemoved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "MarginReserved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "indexed": false, + "internalType": "int256", + "name": "realizedPnl", + "type": "int256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "name": "PnLRealized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "seized", + "type": "uint256[]" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "repayAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "name": "SettledBadDebt", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "idx", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "addMargin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "idx", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "addMarginFor", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "idx", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_weight", + "type": "uint256" + } + ], + "name": "changeCollateralWeight", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "clearingHouse", + "outputs": [ + { + "internalType": "contract IClearingHouse", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "credit", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + } + ], + "name": "getAvailableMargin", + "outputs": [ + { + "internalType": "int256", + "name": "availableMargin", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "idx", + "type": "uint256" + } + ], + "name": "getCollateralToken", + "outputs": [ + { + "internalType": "contract IERC20", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + } + ], + "name": "getNormalizedMargin", + "outputs": [ + { + "internalType": "int256", + "name": "weighted", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + } + ], + "name": "getSpotCollateralValue", + "outputs": [ + { + "internalType": "int256", + "name": "spot", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "governance", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_governance", + "type": "address" + }, + { + "internalType": "address", + "name": "_vusd", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "insuranceFund", + "outputs": [ + { + "internalType": "contract IInsuranceFund", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "bool", + "name": "includeFunding", + "type": "bool" + } + ], + "name": "isLiquidatable", + "outputs": [ + { + "internalType": "enum IMarginAccount.LiquidationStatus", + "name": "_isLiquidatable", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "repayAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "incentivePerDollar", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "forwarder", + "type": "address" + } + ], + "name": "isTrustedForwarder", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "juror", + "outputs": [ + { + "internalType": "contract IJuror", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "uint256", + "name": "repay", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "idx", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "minSeizeAmount", + "type": "uint256" + } + ], + "name": "liquidateExactRepay", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "uint256", + "name": "maxRepay", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "idx", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "seize", + "type": "uint256" + } + ], + "name": "liquidateExactSeize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "uint256", + "name": "maxRepay", + "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "idxs", + "type": "uint256[]" + } + ], + "name": "liquidateFlexible", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "liquidationIncentive", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "margin", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "marginAccountHelper", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "minAllowableMargin", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "oracle", + "outputs": [ + { + "internalType": "contract IOracle", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "paused", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "int256", + "name": "realizedPnl", + "type": "int256" + } + ], + "name": "realizePnL", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "releaseMargin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "idx", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "removeMargin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "idx", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + } + ], + "name": "removeMarginFor", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "reserveMargin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "reservedMargin", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "__governance", + "type": "address" + } + ], + "name": "setGovernace", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_juror", + "type": "address" + } + ], + "name": "setJuror", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + } + ], + "name": "settleBadDebt", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "supportedAssets", + "outputs": [ + { + "components": [ + { + "internalType": "contract IERC20", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "weight", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "decimals", + "type": "uint8" + } + ], + "internalType": "struct IMarginAccount.Collateral[]", + "name": "", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "supportedAssetsLen", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "supportedCollateral", + "outputs": [ + { + "internalType": "contract IERC20", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "weight", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "decimals", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_registry", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_liquidationIncentive", + "type": "uint256" + } + ], + "name": "syncDeps", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_settler", + "type": "address" + } + ], + "name": "toggleTrustedSettler", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_orderBook", + "type": "address" + } + ], + "name": "toggleWhitelistedOrderBook", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferOutVusd", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "trustedSettlers", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_minAllowableMargin", + "type": "uint256" + } + ], + "name": "updateParams", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "vusd", + "outputs": [ + { + "internalType": "contract IERC20FlexibleSupply", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + } + ], + "name": "weightedAndSpotCollateral", + "outputs": [ + { + "internalType": "int256", + "name": "weighted", + "type": "int256" + }, + { + "internalType": "int256", + "name": "spot", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_coin", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_weight", + "type": "uint256" + } + ], + "name": "whitelistCollateral", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "whitelistedOrderBooks", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } +]}`) diff --git a/plugin/evm/orderbook/abis/OrderBook.go b/plugin/evm/orderbook/abis/OrderBook.go new file mode 100644 index 0000000000..411c2a9245 --- /dev/null +++ b/plugin/evm/orderbook/abis/OrderBook.go @@ -0,0 +1,589 @@ +package abis + +var OrderBookAbi = []byte(`{"abi": [ + { + "inputs": [ + { + "internalType": "address", + "name": "_clearingHouse", + "type": "address" + }, + { + "internalType": "address", + "name": "_trustedForwarder", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "version", + "type": "uint8" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "string", + "name": "err", + "type": "string" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "toLiquidate", + "type": "uint256" + } + ], + "name": "LiquidationError", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "string", + "name": "err", + "type": "string" + } + ], + "name": "MatchingValidationError", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "fillAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "openInterestNotional", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bool", + "name": "isLiquidation", + "type": "bool" + } + ], + "name": "OrderMatched", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "string", + "name": "err", + "type": "string" + } + ], + "name": "OrderMatchingError", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "authority", + "type": "address" + } + ], + "name": "TradingAuthorityRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "authority", + "type": "address" + } + ], + "name": "TradingAuthorityWhitelisted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "inputs": [], + "name": "clearingHouse", + "outputs": [ + { + "internalType": "contract IClearingHouse", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256[]", + "name": "impactBids", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "impactAsks", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "midPrice", + "type": "uint256[]" + } + ], + "name": "commitLiquiditySample", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes[2]", + "name": "data", + "type": "bytes[2]" + }, + { + "internalType": "int256", + "name": "fillAmount", + "type": "int256" + } + ], + "name": "executeMatchedOrders", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "governance", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_governance", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "isTradingAuthority", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "forwarder", + "type": "address" + } + ], + "name": "isTrustedForwarder", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "isValidator", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "juror", + "outputs": [ + { + "internalType": "contract IJuror", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "liquidationAmount", + "type": "uint256" + } + ], + "name": "liquidateAndExecuteOrder", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "name": "orderHandlers", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "err", + "type": "string" + } + ], + "name": "parseMatchingError", + "outputs": [ + { + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "internalType": "string", + "name": "reason", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "paused", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "referral", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "authority", + "type": "address" + } + ], + "name": "revokeTradingAuthority", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "__governance", + "type": "address" + } + ], + "name": "setGovernace", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_juror", + "type": "address" + } + ], + "name": "setJuror", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "orderType", + "type": "uint8" + }, + { + "internalType": "address", + "name": "handler", + "type": "address" + } + ], + "name": "setOrderHandler", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_referral", + "type": "address" + } + ], + "name": "setReferral", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "address", + "name": "authority", + "type": "address" + } + ], + "name": "setTradingAuthority", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "validator", + "type": "address" + }, + { + "internalType": "bool", + "name": "status", + "type": "bool" + } + ], + "name": "setValidatorStatus", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "authority", + "type": "address" + } + ], + "name": "whitelistTradingAuthority", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } +]}`) diff --git a/plugin/evm/orderbook/abis/SignedOrderBook.go b/plugin/evm/orderbook/abis/SignedOrderBook.go new file mode 100644 index 0000000000..977e50f9ee --- /dev/null +++ b/plugin/evm/orderbook/abis/SignedOrderBook.go @@ -0,0 +1,510 @@ +package abis + +var SignedOrderBookAbi = []byte(`{"abi": [ + { + "inputs": [ + { + "internalType": "address", + "name": "_defaultOrderBook", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "version", + "type": "uint8" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "name": "OrderCancelAccepted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "string", + "name": "err", + "type": "string" + } + ], + "name": "OrderCancelRejected", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "inputs": [], + "name": "ORDER_TYPEHASH", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint8", + "name": "orderType", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "expireAt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "int256", + "name": "baseAssetQuantity", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "salt", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "reduceOnly", + "type": "bool" + }, + { + "internalType": "bool", + "name": "postOnly", + "type": "bool" + } + ], + "internalType": "struct ISignedOrderBook.Order[]", + "name": "orders", + "type": "tuple[]" + }, + { + "internalType": "bytes[]", + "name": "signatures", + "type": "bytes[]" + } + ], + "name": "cancelOrders", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "defaultOrderBook", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint8", + "name": "orderType", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "expireAt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "int256", + "name": "baseAssetQuantity", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "salt", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "reduceOnly", + "type": "bool" + }, + { + "internalType": "bool", + "name": "postOnly", + "type": "bool" + } + ], + "internalType": "struct ISignedOrderBook.Order", + "name": "order", + "type": "tuple" + } + ], + "name": "getOrderHash", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "governance", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_governance", + "type": "address" + }, + { + "internalType": "address", + "name": "_juror", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "isTradingAuthority", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "juror", + "outputs": [ + { + "internalType": "contract IJuror", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "orderInfo", + "outputs": [ + { + "internalType": "int256", + "name": "filledAmount", + "type": "int256" + }, + { + "internalType": "enum IOrderHandler.OrderStatus", + "name": "status", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + } + ], + "name": "orderStatus", + "outputs": [ + { + "components": [ + { + "internalType": "int256", + "name": "filledAmount", + "type": "int256" + }, + { + "internalType": "enum IOrderHandler.OrderStatus", + "name": "status", + "type": "uint8" + } + ], + "internalType": "struct ISignedOrderBook.OrderInfo", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "paused", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "__governance", + "type": "address" + } + ], + "name": "setGovernace", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "encodedOrder", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "metadata", + "type": "bytes" + } + ], + "name": "updateOrder", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint8", + "name": "orderType", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "expireAt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "int256", + "name": "baseAssetQuantity", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "salt", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "reduceOnly", + "type": "bool" + }, + { + "internalType": "bool", + "name": "postOnly", + "type": "bool" + } + ], + "internalType": "struct ISignedOrderBook.Order", + "name": "order", + "type": "tuple" + }, + { + "internalType": "bytes", + "name": "signature", + "type": "bytes" + } + ], + "name": "verifySigner", + "outputs": [ + { + "internalType": "address", + "name": "signer", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + } + ]}`) diff --git a/plugin/evm/orderbook/config_service.go b/plugin/evm/orderbook/config_service.go new file mode 100644 index 0000000000..8af98a3bf9 --- /dev/null +++ b/plugin/evm/orderbook/config_service.go @@ -0,0 +1,170 @@ +package orderbook + +import ( + "math/big" + + "github.com/ava-labs/subnet-evm/core" + "github.com/ava-labs/subnet-evm/core/state" + hu "github.com/ava-labs/subnet-evm/plugin/evm/orderbook/hubbleutils" + "github.com/ava-labs/subnet-evm/precompile/contracts/bibliophile" + "github.com/ethereum/go-ethereum/common" +) + +type IConfigService interface { + getMaxLiquidationRatio(market Market) *big.Int + getLiquidationSpreadThreshold(market Market) *big.Int + GetMinAllowableMargin() *big.Int + GetMaintenanceMargin() *big.Int + getMinSizeRequirement(market Market) *big.Int + GetPriceMultiplier(market Market) *big.Int + GetActiveMarketsCount() int64 + GetMarketsIncludingSettled() []common.Address + GetUnderlyingPrices() []*big.Int + GetMidPrices() []*big.Int + GetSettlementPrices() []*big.Int + GetCollaterals() []hu.Collateral + GetLastPremiumFraction(market Market, trader *common.Address) *big.Int + GetCumulativePremiumFraction(market Market) *big.Int + GetAcceptableBounds(market Market) (*big.Int, *big.Int) + GetAcceptableBoundsForLiquidation(market Market) (*big.Int, *big.Int) + GetTakerFee() *big.Int + HasReferrer(trader common.Address) bool + + GetSignedOrderStatus(orderHash common.Hash) int64 + IsTradingAuthority(trader, signer common.Address) bool + GetSignedOrderbookContract() common.Address + + GetMarketAddressFromMarketID(marketId int64) common.Address + GetImpactMarginNotional(ammAddress common.Address) *big.Int + GetReduceOnlyAmounts(trader common.Address) []*big.Int + + IsSettledAll() bool +} + +type ConfigService struct { + blockChain *core.BlockChain + stateDB *state.StateDB +} + +func NewConfigService(blockChain *core.BlockChain) IConfigService { + return &ConfigService{ + blockChain: blockChain, + } +} + +func NewConfigServiceFromStateDB(stateDB *state.StateDB) IConfigService { + return &ConfigService{ + stateDB: stateDB, + } +} + +func (cs *ConfigService) getStateAtCurrentBlock() *state.StateDB { + if cs.stateDB != nil { + return cs.stateDB + } + stateDB, _ := cs.blockChain.StateAt(cs.blockChain.CurrentBlock().Root) + return stateDB +} + +func (cs *ConfigService) GetAcceptableBounds(market Market) (*big.Int, *big.Int) { + return bibliophile.GetAcceptableBounds(cs.getStateAtCurrentBlock(), int64(market)) +} + +func (cs *ConfigService) GetAcceptableBoundsForLiquidation(market Market) (*big.Int, *big.Int) { + return bibliophile.GetAcceptableBoundsForLiquidation(cs.getStateAtCurrentBlock(), int64(market)) +} + +func (cs *ConfigService) getLiquidationSpreadThreshold(market Market) *big.Int { + return bibliophile.GetMaxLiquidationPriceSpread(cs.getStateAtCurrentBlock(), int64(market)) +} + +func (cs *ConfigService) getMaxLiquidationRatio(market Market) *big.Int { + return bibliophile.GetMaxLiquidationRatio(cs.getStateAtCurrentBlock(), int64(market)) +} + +func (cs *ConfigService) GetMinAllowableMargin() *big.Int { + return bibliophile.GetMinAllowableMargin(cs.getStateAtCurrentBlock()) +} + +func (cs *ConfigService) GetMaintenanceMargin() *big.Int { + return bibliophile.GetMaintenanceMargin(cs.getStateAtCurrentBlock()) +} + +func (cs *ConfigService) getMinSizeRequirement(market Market) *big.Int { + return bibliophile.GetMinSizeRequirement(cs.getStateAtCurrentBlock(), int64(market)) +} + +func (cs *ConfigService) GetPriceMultiplier(market Market) *big.Int { + return bibliophile.GetMultiplier(cs.getStateAtCurrentBlock(), int64(market)) +} + +func (cs *ConfigService) GetActiveMarketsCount() int64 { + return bibliophile.GetActiveMarketsCount(cs.getStateAtCurrentBlock()) +} + +func (cs *ConfigService) GetMarketsIncludingSettled() []common.Address { + return bibliophile.GetMarketsIncludingSettled(cs.getStateAtCurrentBlock()) +} + +func (cs *ConfigService) GetUnderlyingPrices() []*big.Int { + return bibliophile.GetUnderlyingPrices(cs.getStateAtCurrentBlock()) +} + +func (cs *ConfigService) GetMidPrices() []*big.Int { + return bibliophile.GetMidPrices(cs.getStateAtCurrentBlock()) +} + +func (cs *ConfigService) GetSettlementPrices() []*big.Int { + return bibliophile.GetSettlementPrices(cs.getStateAtCurrentBlock()) +} + +func (cs *ConfigService) GetCollaterals() []hu.Collateral { + return bibliophile.GetCollaterals(cs.getStateAtCurrentBlock()) +} + +func (cs *ConfigService) GetLastPremiumFraction(market Market, trader *common.Address) *big.Int { + markets := bibliophile.GetMarketsIncludingSettled(cs.getStateAtCurrentBlock()) + return bibliophile.GetLastPremiumFraction(cs.getStateAtCurrentBlock(), markets[market], trader) +} + +func (cs *ConfigService) GetCumulativePremiumFraction(market Market) *big.Int { + markets := bibliophile.GetMarketsIncludingSettled(cs.getStateAtCurrentBlock()) + return bibliophile.GetCumulativePremiumFraction(cs.getStateAtCurrentBlock(), markets[market]) +} + +func (cs *ConfigService) GetTakerFee() *big.Int { + takerFee := bibliophile.GetTakerFee(cs.getStateAtCurrentBlock()) + return hu.Div(hu.Mul(takerFee, big.NewInt(8)), big.NewInt(10)) // 20% discount, which is applied to everyone currently +} + +func (cs *ConfigService) HasReferrer(trader common.Address) bool { + return bibliophile.HasReferrer(cs.getStateAtCurrentBlock(), trader) +} + +func (cs *ConfigService) GetSignedOrderStatus(orderHash common.Hash) int64 { + return bibliophile.GetSignedOrderStatus(cs.getStateAtCurrentBlock(), orderHash) +} + +func (cs *ConfigService) IsTradingAuthority(trader, signer common.Address) bool { + return bibliophile.IsTradingAuthority(cs.getStateAtCurrentBlock(), trader, signer) +} + +func (cs *ConfigService) GetSignedOrderbookContract() common.Address { + return bibliophile.GetSignedOrderBookAddress(cs.getStateAtCurrentBlock()) +} + +func (cs *ConfigService) GetMarketAddressFromMarketID(marketId int64) common.Address { + return bibliophile.GetMarketAddressFromMarketID(marketId, cs.getStateAtCurrentBlock()) +} + +func (cs *ConfigService) GetImpactMarginNotional(ammAddress common.Address) *big.Int { + return bibliophile.GetImpactMarginNotional(cs.getStateAtCurrentBlock(), ammAddress) +} + +func (cs *ConfigService) GetReduceOnlyAmounts(trader common.Address) []*big.Int { + return bibliophile.GetReduceOnlyAmounts(cs.getStateAtCurrentBlock(), trader) +} + +func (cs *ConfigService) IsSettledAll() bool { + return bibliophile.IsSettledAll(cs.getStateAtCurrentBlock()) +} diff --git a/plugin/evm/orderbook/contract_events_processor.go b/plugin/evm/orderbook/contract_events_processor.go new file mode 100644 index 0000000000..edc054feea --- /dev/null +++ b/plugin/evm/orderbook/contract_events_processor.go @@ -0,0 +1,810 @@ +package orderbook + +import ( + "fmt" + "math/big" + "sort" + + "github.com/ava-labs/subnet-evm/accounts/abi" + "github.com/ava-labs/subnet-evm/core/types" + "github.com/ava-labs/subnet-evm/metrics" + "github.com/ava-labs/subnet-evm/plugin/evm/orderbook/abis" + "github.com/ava-labs/subnet-evm/utils" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" +) + +type ContractEventsProcessor struct { + orderBookABI abi.ABI + limitOrderBookABI abi.ABI + iocOrderBookABI abi.ABI + signedOrderBookABI abi.ABI + marginAccountABI abi.ABI + clearingHouseABI abi.ABI + database LimitOrderDatabase + SignedOrderBookContractAddress common.Address +} + +func NewContractEventsProcessor(database LimitOrderDatabase, signedOrderbookContract common.Address) *ContractEventsProcessor { + orderBookABI, err := abi.FromSolidityJson(string(abis.OrderBookAbi)) + if err != nil { + panic(err) + } + + limitOrderBookABI, err := abi.FromSolidityJson(string(abis.LimitOrderBookAbi)) + if err != nil { + panic(err) + } + + marginAccountABI, err := abi.FromSolidityJson(string(abis.MarginAccountAbi)) + if err != nil { + panic(err) + } + + clearingHouseABI, err := abi.FromSolidityJson(string(abis.ClearingHouseAbi)) + if err != nil { + panic(err) + } + + iocOrderBookABI, err := abi.FromSolidityJson(string(abis.IOCOrderBookAbi)) + if err != nil { + panic(err) + } + + signedOrderBookABI, err := abi.FromSolidityJson(string(abis.SignedOrderBookAbi)) + if err != nil { + panic(err) + } + + log.Info("NewContractEventsProcessor", "signedOrderbookContract", signedOrderbookContract.String()) + return &ContractEventsProcessor{ + orderBookABI: orderBookABI, + limitOrderBookABI: limitOrderBookABI, + marginAccountABI: marginAccountABI, + clearingHouseABI: clearingHouseABI, + iocOrderBookABI: iocOrderBookABI, + signedOrderBookABI: signedOrderBookABI, + database: database, + SignedOrderBookContractAddress: signedOrderbookContract, + } +} + +func (cep *ContractEventsProcessor) ProcessEvents(logs []*types.Log) { + var ( + deletedLogs []*types.Log + rebirthLogs []*types.Log + ) + for i := 0; i < len(logs); i++ { + log := logs[i] + if log.Removed { + deletedLogs = append(deletedLogs, log) + } else { + rebirthLogs = append(rebirthLogs, log) + } + } + + // deletedLogs are in descending order by (blockNumber, LogIndex) + // rebirthLogs should be in ascending order by (blockNumber, LogIndex) + sort.Slice(deletedLogs, func(i, j int) bool { + if deletedLogs[i].BlockNumber == deletedLogs[j].BlockNumber { + return deletedLogs[i].Index > deletedLogs[j].Index + } + return deletedLogs[i].BlockNumber > deletedLogs[j].BlockNumber + }) + + sort.Slice(rebirthLogs, func(i, j int) bool { + if rebirthLogs[i].BlockNumber == rebirthLogs[j].BlockNumber { + return rebirthLogs[i].Index < rebirthLogs[j].Index + } + return rebirthLogs[i].BlockNumber < rebirthLogs[j].BlockNumber + }) + + logs = append(deletedLogs, rebirthLogs...) + for _, event := range logs { + switch event.Address { + case OrderBookContractAddress: + cep.handleOrderBookEvent(event) + case LimitOrderBookContractAddress: + cep.handleLimitOrderBookEvent(event) + case IOCOrderBookContractAddress: + cep.handleIOCOrderBookEvent(event) + case cep.SignedOrderBookContractAddress: + cep.handleSignedOrderBookEvent(event) + } + } +} + +func (cep *ContractEventsProcessor) ProcessAcceptedEvents(logs []*types.Log, inBootstrap bool) { + sort.Slice(logs, func(i, j int) bool { + if logs[i].BlockNumber == logs[j].BlockNumber { + return logs[i].Index < logs[j].Index + } + return logs[i].BlockNumber < logs[j].BlockNumber + }) + + for _, event := range logs { + switch event.Address { + case MarginAccountContractAddress: + cep.handleMarginAccountEvent(event) + case ClearingHouseContractAddress: + cep.handleClearingHouseEvent(event) + } + } + if !inBootstrap { + // events are applied in sequence during bootstrap also, those shouldn't be updated in metrics as they are already counted + go cep.updateMetrics(logs) + } +} + +func (cep *ContractEventsProcessor) handleOrderBookEvent(event *types.Log) { + removed := event.Removed + args := map[string]interface{}{} + switch event.Topics[0] { + // event OrderMatched(address indexed trader, bytes32 indexed orderHash, uint256 fillAmount, uint price, uint openInterestNotional, uint timestamp, bool isLiquidation); + case cep.orderBookABI.Events["OrderMatched"].ID: + err := cep.orderBookABI.UnpackIntoMap(args, "OrderMatched", event.Data) + if err != nil { + log.Error("error in orderBookAbi.UnpackIntoMap", "method", "OrderMatched", "err", err) + return + } + + trader := getAddressFromTopicHash(event.Topics[1]) + orderId := event.Topics[2] + fillAmount := args["fillAmount"].(*big.Int) + if !removed { + log.Info("OrderMatched", "orderId", orderId.String(), "trader", trader.String(), "args", args, "number", event.BlockNumber) + cep.database.UpdateFilledBaseAssetQuantity(fillAmount, orderId, event.BlockNumber) + } else { + fillAmount.Neg(fillAmount) + log.Info("OrderMatched removed", "orderId", orderId.String(), "trader", trader.String(), "args", args, "number", event.BlockNumber) + cep.database.UpdateFilledBaseAssetQuantity(fillAmount, orderId, event.BlockNumber) + } + // OrderMatchingError(bytes32 indexed orderHash, string err); + case cep.orderBookABI.Events["OrderMatchingError"].ID: + err := cep.orderBookABI.UnpackIntoMap(args, "OrderMatchingError", event.Data) + if err != nil { + log.Error("error in orderBookAbi.UnpackIntoMap", "method", "OrderMatchingError", "err", err) + return + } + orderId := event.Topics[1] + errorString := args["err"].(string) + if !removed { + log.Info("OrderMatchingError", "args", args, "orderId", orderId.String(), "TxHash", event.TxHash, "number", event.BlockNumber) + if err := cep.database.SetOrderStatus(orderId, Execution_Failed, errorString, event.BlockNumber); err != nil { + log.Error("error in SetOrderStatus", "method", "OrderMatchingError", "err", err) + return + } + } else { + log.Info("OrderMatchingError removed", "args", args, "orderId", orderId.String(), "number", event.BlockNumber) + if err := cep.database.RevertLastStatus(orderId); err != nil { + log.Error("error in SetOrderStatus", "method", "OrderMatchingError", "removed", true, "err", err) + return + } + } + + // event MatchingValidationError(string err); + case cep.orderBookABI.Events["MatchingValidationError"].ID: + err := cep.orderBookABI.UnpackIntoMap(args, "MatchingValidationError", event.Data) + if err != nil { + log.Error("error in orderBookAbi.UnpackIntoMap", "method", "MatchingValidationError", "err", err) + return + } + if !removed { + log.Info("MatchingValidationError", "args", args, "number", event.BlockNumber) + } else { + log.Info("MatchingValidationError removed", "args", args, "number", event.BlockNumber) + } + } +} + +func (cep *ContractEventsProcessor) handleLimitOrderBookEvent(event *types.Log) { + removed := event.Removed + args := map[string]interface{}{} + switch event.Topics[0] { + // event OrderAccepted(address indexed trader, bytes32 indexed orderHash, Order order, uint timestamp); + case cep.limitOrderBookABI.Events["OrderAccepted"].ID: + err := cep.limitOrderBookABI.UnpackIntoMap(args, "OrderAccepted", event.Data) + if err != nil { + log.Error("error in limitOrderBookABI.UnpackIntoMap", "method", "OrderAccepted", "err", err) + return + } + + orderId := event.Topics[2] + if !removed { + timestamp := args["timestamp"].(*big.Int) + order := LimitOrder{} + order.DecodeFromRawOrder(args["order"]) + + limitOrder := Order{ + Id: orderId, + Market: Market(order.AmmIndex.Int64()), + PositionType: getPositionTypeBasedOnBaseAssetQuantity(order.BaseAssetQuantity), + Trader: getAddressFromTopicHash(event.Topics[1]), + BaseAssetQuantity: order.BaseAssetQuantity, + FilledBaseAssetQuantity: big.NewInt(0), + Price: order.Price, + RawOrder: &order, + Salt: order.Salt, + ReduceOnly: order.ReduceOnly, + BlockNumber: big.NewInt(int64(event.BlockNumber)), + OrderType: Limit, + } + log.Info("LimitOrder/OrderAccepted", "order", limitOrder, "timestamp", timestamp) + cep.database.Add(&limitOrder) + } else { + log.Info("LimitOrder/OrderAccepted removed", "args", args, "orderId", orderId.String(), "number", event.BlockNumber) + cep.database.Delete(orderId) + } + + // event OrderRejected(address indexed trader, bytes32 indexed orderHash, Order order, uint timestamp, string err); + case cep.limitOrderBookABI.Events["OrderRejected"].ID: + err := cep.limitOrderBookABI.UnpackIntoMap(args, "OrderRejected", event.Data) + if err != nil { + log.Error("error in limitOrderBookABI.UnpackIntoMap", "method", "OrderRejected", "err", err) + return + } + + orderId := event.Topics[2] + order := args["order"] + if !removed { + log.Info("LimitOrder/OrderRejected", "args", args, "orderId", orderId.String(), "number", event.BlockNumber, "order", order) + } else { + log.Info("LimitOrder/OrderRejected removed", "args", args, "orderId", orderId.String(), "number", event.BlockNumber, "order", order) + } + + // event OrderCancelAccepted(address indexed trader, bytes32 indexed orderHash, uint timestamp, bool isAutoCancelled); + case cep.limitOrderBookABI.Events["OrderCancelAccepted"].ID: + err := cep.limitOrderBookABI.UnpackIntoMap(args, "OrderCancelAccepted", event.Data) + if err != nil { + log.Error("error in limitOrderBookABI.UnpackIntoMap", "method", "OrderCancelAccepted", "err", err) + return + } + + orderId := event.Topics[2] + if !removed { + timestamp := args["timestamp"].(*big.Int) + log.Info("LimitOrder/OrderCancelAccepted", "args", args, "orderId", orderId.String(), "number", event.BlockNumber, "timestamp", timestamp) + if err := cep.database.SetOrderStatus(orderId, Cancelled, "", event.BlockNumber); err != nil { + log.Error("error in SetOrderStatus", "method", "OrderCancelAccepted", "err", err) + return + } + } else { + log.Info("LimitOrder/OrderCancelAccepted removed", "args", args, "orderId", orderId.String(), "number", event.BlockNumber) + if err := cep.database.RevertLastStatus(orderId); err != nil { + log.Error("error in SetOrderStatus", "method", "OrderCancelAccepted", "removed", true, "err", err) + return + } + } + + // event OrderCancelRejected(address indexed trader, bytes32 indexed orderHash, uint timestamp, string err); + case cep.limitOrderBookABI.Events["OrderCancelRejected"].ID: + err := cep.limitOrderBookABI.UnpackIntoMap(args, "OrderCancelRejected", event.Data) + if err != nil { + log.Error("error in limitOrderBookABI.UnpackIntoMap", "method", "OrderCancelRejected", "err", err) + return + } + + orderId := event.Topics[2] + if !removed { + log.Info("LimitOrder/OrderCancelRejected", "args", args, "orderId", orderId.String(), "number", event.BlockNumber) + } else { + log.Info("LimitOrder/OrderCancelRejected removed", "args", args, "orderId", orderId.String(), "number", event.BlockNumber) + } + } +} + +func (cep *ContractEventsProcessor) handleIOCOrderBookEvent(event *types.Log) { + removed := event.Removed + args := map[string]interface{}{} + switch event.Topics[0] { + // event OrderAccepted(address indexed trader, bytes32 indexed orderHash, IImmediateOrCancelOrders.Order order, uint timestamp); + case cep.iocOrderBookABI.Events["OrderAccepted"].ID: + err := cep.iocOrderBookABI.UnpackIntoMap(args, "OrderAccepted", event.Data) + if err != nil { + log.Error("error in iocOrderBookABI.UnpackIntoMap", "method", "OrderAccepted", "err", err) + return + } + orderId := event.Topics[2] + if !removed { + iocOrder := IOCOrder{} + iocOrder.DecodeFromRawOrder(args["order"]) + order := Order{ + Id: orderId, + Market: Market(iocOrder.AmmIndex.Int64()), + PositionType: getPositionTypeBasedOnBaseAssetQuantity(iocOrder.BaseAssetQuantity), + Trader: getAddressFromTopicHash(event.Topics[1]), + BaseAssetQuantity: iocOrder.BaseAssetQuantity, + FilledBaseAssetQuantity: big.NewInt(0), + Price: iocOrder.Price, + RawOrder: &iocOrder, + Salt: iocOrder.Salt, + ReduceOnly: iocOrder.ReduceOnly, + BlockNumber: big.NewInt(int64(event.BlockNumber)), + OrderType: IOC, + } + log.Info("IOCOrder/OrderAccepted", "order", order, "number", event.BlockNumber) + cep.database.Add(&order) + } else { + log.Info("IOCOrder/OrderAccepted removed", "orderId", orderId.String(), "block", event.BlockHash.String(), "number", event.BlockNumber) + cep.database.Delete(orderId) + } + + // event OrderRejected(address indexed trader, bytes32 indexed orderHash, IImmediateOrCancelOrders.Order order, uint timestamp, string err); + case cep.iocOrderBookABI.Events["OrderRejected"].ID: + err := cep.iocOrderBookABI.UnpackIntoMap(args, "OrderRejected", event.Data) + if err != nil { + log.Error("error in iocOrderBookABI.UnpackIntoMap", "method", "OrderRejected", "err", err) + return + } + + orderId := event.Topics[2] + order := args["order"] + if !removed { + log.Info("IOCOrder/OrderRejected", "orderId", orderId.String(), "number", event.BlockNumber, "order", order, "err", args["err"]) + } else { + log.Info("IOCOrder/OrderRejected removed", "orderId", orderId.String(), "number", event.BlockNumber, "order", order, args["err"]) + } + } +} + +func (cep *ContractEventsProcessor) handleSignedOrderBookEvent(event *types.Log) { + removed := event.Removed + args := map[string]interface{}{} + switch event.Topics[0] { + // event OrderCancelAccepted(address indexed trader, bytes32 indexed orderHash, uint timestamp); + case cep.signedOrderBookABI.Events["OrderCancelAccepted"].ID: + err := cep.signedOrderBookABI.UnpackIntoMap(args, "OrderCancelAccepted", event.Data) + if err != nil { + log.Error("error in signedOrderBookABI.UnpackIntoMap", "method", "OrderCancelAccepted", "err", err) + return + } + orderId := event.Topics[2] + if !removed { + timestamp := args["timestamp"].(*big.Int) + log.Info("SignedOrder/OrderCancelAccepted", "args", args, "orderId", orderId.String(), "number", event.BlockNumber, "timestamp", timestamp) + if err := cep.database.SetOrderStatus(orderId, Cancelled, "", event.BlockNumber); err != nil { + log.Error("error in SetOrderStatus", "method", "OrderCancelAccepted", "err", err) + return + } + } else { + log.Info("SignedOrder/OrderCancelAccepted removed", "args", args, "orderId", orderId.String(), "number", event.BlockNumber) + if err := cep.database.RevertLastStatus(orderId); err != nil { + log.Error("error in SetOrderStatus", "method", "OrderCancelAccepted", "removed", true, "err", err) + return + } + } + } +} + +func (cep *ContractEventsProcessor) handleMarginAccountEvent(event *types.Log) { + args := map[string]interface{}{} + switch event.Topics[0] { + case cep.marginAccountABI.Events["MarginAdded"].ID: + err := cep.marginAccountABI.UnpackIntoMap(args, "MarginAdded", event.Data) + if err != nil { + log.Error("error in marginAccountABI.UnpackIntoMap", "method", "MarginAdded", "err", err) + return + } + trader := getAddressFromTopicHash(event.Topics[1]) + collateral := event.Topics[2].Big().Int64() + amount := args["amount"].(*big.Int) + log.Info("MarginAdded", "trader", trader, "collateral", collateral, "amount", amount.Uint64(), "number", event.BlockNumber) + cep.database.UpdateMargin(trader, Collateral(collateral), amount) + case cep.marginAccountABI.Events["MarginRemoved"].ID: + err := cep.marginAccountABI.UnpackIntoMap(args, "MarginRemoved", event.Data) + if err != nil { + log.Error("error in marginAccountABI.UnpackIntoMap", "method", "MarginRemoved", "err", err) + return + } + trader := getAddressFromTopicHash(event.Topics[1]) + collateral := event.Topics[2].Big().Int64() + amount := args["amount"].(*big.Int) + log.Info("MarginRemoved", "trader", trader, "collateral", collateral, "amount", amount.Uint64(), "number", event.BlockNumber) + cep.database.UpdateMargin(trader, Collateral(collateral), big.NewInt(0).Neg(amount)) + case cep.marginAccountABI.Events["MarginReserved"].ID: + err := cep.marginAccountABI.UnpackIntoMap(args, "MarginReserved", event.Data) + if err != nil { + log.Error("error in marginAccountABI.UnpackIntoMap", "method", "MarginReserved", "err", err) + return + } + trader := getAddressFromTopicHash(event.Topics[1]) + amount := args["amount"].(*big.Int) + log.Info("MarginReserved", "trader", trader, "amount", amount.Uint64(), "number", event.BlockNumber) + cep.database.UpdateReservedMargin(trader, amount) + case cep.marginAccountABI.Events["MarginReleased"].ID: + err := cep.marginAccountABI.UnpackIntoMap(args, "MarginReleased", event.Data) + if err != nil { + log.Error("error in marginAccountABI.UnpackIntoMap", "method", "MarginReleased", "err", err) + return + } + trader := getAddressFromTopicHash(event.Topics[1]) + amount := args["amount"].(*big.Int) + log.Info("MarginReleased", "trader", trader, "amount", amount.Uint64(), "number", event.BlockNumber) + cep.database.UpdateReservedMargin(trader, big.NewInt(0).Neg(amount)) + case cep.marginAccountABI.Events["PnLRealized"].ID: + err := cep.marginAccountABI.UnpackIntoMap(args, "PnLRealized", event.Data) + if err != nil { + log.Error("error in marginAccountABI.UnpackIntoMap", "method", "PnLRealized", "err", err) + return + } + trader := getAddressFromTopicHash(event.Topics[1]) + realisedPnL := args["realizedPnl"].(*big.Int) + log.Info("PnLRealized", "trader", trader, "amount", realisedPnL.Uint64(), "number", event.BlockNumber) + cep.database.UpdateMargin(trader, HUSD, realisedPnL) + } +} + +func (cep *ContractEventsProcessor) handleClearingHouseEvent(event *types.Log) { + args := map[string]interface{}{} + switch event.Topics[0] { + case cep.clearingHouseABI.Events["FundingRateUpdated"].ID: + err := cep.clearingHouseABI.UnpackIntoMap(args, "FundingRateUpdated", event.Data) + if err != nil { + log.Error("error in clearingHouseABI.UnpackIntoMap", "method", "FundingRateUpdated", "err", err) + return + } + cumulativePremiumFraction := args["cumulativePremiumFraction"].(*big.Int) + nextFundingTime := args["nextFundingTime"].(*big.Int) + market := Market(int(event.Topics[1].Big().Int64())) + log.Info("FundingRateUpdated", "args", args, "cumulativePremiumFraction", cumulativePremiumFraction, "market", market) + cep.database.UpdateUnrealisedFunding(market, cumulativePremiumFraction) + cep.database.UpdateNextFundingTime(nextFundingTime.Uint64()) + + case cep.clearingHouseABI.Events["FundingPaid"].ID: + err := cep.clearingHouseABI.UnpackIntoMap(args, "FundingPaid", event.Data) + if err != nil { + log.Error("error in clearingHouseABI.UnpackIntoMap", "method", "FundingPaid", "err", err) + return + } + trader := getAddressFromTopicHash(event.Topics[1]) + market := Market(int(event.Topics[2].Big().Int64())) + cumulativePremiumFraction := args["cumulativePremiumFraction"].(*big.Int) + log.Info("FundingPaid", "trader", trader, "market", market, "cumulativePremiumFraction", cumulativePremiumFraction) + cep.database.ResetUnrealisedFunding(market, trader, cumulativePremiumFraction) + + case cep.clearingHouseABI.Events["PositionModified"].ID: + err := cep.clearingHouseABI.UnpackIntoMap(args, "PositionModified", event.Data) + if err != nil { + log.Error("error in clearingHouseABI.UnpackIntoMap", "method", "PositionModified", "err", err) + return + } + + trader := getAddressFromTopicHash(event.Topics[1]) + market := Market(int(event.Topics[2].Big().Int64())) + lastPrice := args["price"].(*big.Int) + cep.database.UpdateLastPrice(market, lastPrice) + + openNotional := args["openNotional"].(*big.Int) + size := args["size"].(*big.Int) + log.Info("PositionModified", "trader", trader, "market", market, "args", args) + cep.database.UpdatePosition(trader, market, size, openNotional, false, event.BlockNumber) + case cep.clearingHouseABI.Events["PositionLiquidated"].ID: + err := cep.clearingHouseABI.UnpackIntoMap(args, "PositionLiquidated", event.Data) + if err != nil { + log.Error("error in clearingHouseABI.UnpackIntoMap", "method", "PositionLiquidated", "err", err) + return + } + trader := getAddressFromTopicHash(event.Topics[1]) + + market := Market(int(event.Topics[2].Big().Int64())) + lastPrice := args["price"].(*big.Int) + cep.database.UpdateLastPrice(market, lastPrice) + + openNotional := args["openNotional"].(*big.Int) + size := args["size"].(*big.Int) + log.Info("PositionLiquidated", "market", market, "trader", trader, "args", args) + cep.database.UpdatePosition(trader, market, size, openNotional, true, event.BlockNumber) + + // event NotifyNextPISample(uint nextSampleTime); + case cep.clearingHouseABI.Events["NotifyNextPISample"].ID: + err := cep.clearingHouseABI.UnpackIntoMap(args, "NotifyNextPISample", event.Data) + if err != nil { + log.Error("error in clearingHouseABI.UnpackIntoMap", "method", "NotifyNextPISample", "err", err) + return + } + nextSampleTime := args["nextSampleTime"].(*big.Int) + log.Info("NotifyNextPISample", "nextSampleTime", nextSampleTime) + cep.database.UpdateNextSamplePITime(nextSampleTime.Uint64()) + + case cep.clearingHouseABI.Events["PISampledUpdated"].ID: + err := cep.clearingHouseABI.UnpackIntoMap(args, "PISampledUpdated", event.Data) + if err != nil { + log.Error("error in clearingHouseABI.UnpackIntoMap", "method", "PISampledUpdated", "err", err) + return + } + log.Info("PISampledUpdated", "args", args) + + case cep.clearingHouseABI.Events["PISampleSkipped"].ID: + err := cep.clearingHouseABI.UnpackIntoMap(args, "PISampleSkipped", event.Data) + if err != nil { + log.Error("error in clearingHouseABI.UnpackIntoMap", "method", "PISampleSkipped", "err", err) + return + } + log.Info("PISampleSkipped", "args", args) + } +} + +type TraderEvent struct { + Trader common.Address + OrderId common.Hash + OrderType string + Removed bool + EventName string + Args map[string]interface{} + BlockNumber *big.Int + BlockStatus BlockConfirmationLevel + Timestamp *big.Int + TransactionHash common.Hash +} + +type MarketFeedEvent struct { + Trader common.Address + Market Market + Size float64 + Price float64 + Removed bool + EventName string + BlockNumber *big.Int + BlockStatus BlockConfirmationLevel + Timestamp *big.Int + TransactionHash common.Hash +} + +type BlockConfirmationLevel string + +const ( + ConfirmationLevelHead BlockConfirmationLevel = "head" + ConfirmationLevelAccepted BlockConfirmationLevel = "accepted" +) + +func (cep *ContractEventsProcessor) PushToTraderFeed(events []*types.Log, blockStatus BlockConfirmationLevel) { + for _, event := range events { + removed := event.Removed + args := map[string]interface{}{} + eventName := "" + var orderId common.Hash + var orderType string + var trader common.Address + txHash := event.TxHash + switch event.Address { + case OrderBookContractAddress: + switch event.Topics[0] { + case cep.orderBookABI.Events["OrderMatched"].ID: + err := cep.orderBookABI.UnpackIntoMap(args, "OrderMatched", event.Data) + if err != nil { + log.Error("error in orderBookABI.UnpackIntoMap", "method", "OrderMatched", "err", err) + continue + } + eventName = "OrderMatched" + fillAmount := args["fillAmount"].(*big.Int) + openInterestNotional := args["openInterestNotional"].(*big.Int) + price := args["price"].(*big.Int) + args["fillAmount"] = utils.BigIntToFloat(fillAmount, 18) + args["openInterestNotional"] = utils.BigIntToFloat(openInterestNotional, 18) + args["price"] = utils.BigIntToFloat(price, 6) + orderId = event.Topics[2] + trader = getAddressFromTopicHash(event.Topics[1]) + } + + case LimitOrderBookContractAddress: + orderType = Limit.String() + switch event.Topics[0] { + case cep.limitOrderBookABI.Events["OrderAccepted"].ID: + err := cep.limitOrderBookABI.UnpackIntoMap(args, "OrderAccepted", event.Data) + if err != nil { + log.Error("error in limitOrderBookABI.UnpackIntoMap", "method", "OrderAccepted", "err", err) + continue + } + eventName = "OrderAccepted" + order := LimitOrder{} + order.DecodeFromRawOrder(args["order"]) + args["order"] = order.Map() + orderId = event.Topics[2] + trader = getAddressFromTopicHash(event.Topics[1]) + + case cep.limitOrderBookABI.Events["OrderRejected"].ID: + err := cep.limitOrderBookABI.UnpackIntoMap(args, "OrderRejected", event.Data) + if err != nil { + log.Error("error in limitOrderBookABI.UnpackIntoMap", "method", "OrderRejected", "err", err) + continue + } + eventName = "OrderRejected" + order := LimitOrder{} + order.DecodeFromRawOrder(args["order"]) + args["order"] = order.Map() + orderId = event.Topics[2] + trader = getAddressFromTopicHash(event.Topics[1]) + + case cep.limitOrderBookABI.Events["OrderCancelAccepted"].ID: + err := cep.limitOrderBookABI.UnpackIntoMap(args, "OrderCancelAccepted", event.Data) + if err != nil { + log.Error("error in limitOrderBookABI.UnpackIntoMap", "method", "OrderCancelAccepted", "err", err) + continue + } + eventName = "OrderCancelAccepted" + orderId = event.Topics[2] + trader = getAddressFromTopicHash(event.Topics[1]) + + case cep.limitOrderBookABI.Events["OrderCancelRejected"].ID: + err := cep.limitOrderBookABI.UnpackIntoMap(args, "OrderCancelRejected", event.Data) + if err != nil { + log.Error("error in limitOrderBookABI.UnpackIntoMap", "method", "OrderCancelRejected", "err", err) + continue + } + eventName = "OrderCancelRejected" + orderId = event.Topics[2] + trader = getAddressFromTopicHash(event.Topics[1]) + } + + case IOCOrderBookContractAddress: + orderType = IOC.String() + switch event.Topics[0] { + case cep.iocOrderBookABI.Events["OrderAccepted"].ID: + err := cep.iocOrderBookABI.UnpackIntoMap(args, "OrderAccepted", event.Data) + if err != nil { + log.Error("error in iocOrderBookABI.UnpackIntoMap", "method", "OrderAccepted", "err", err) + continue + } + eventName = "OrderAccepted" + order := IOCOrder{} + order.DecodeFromRawOrder(args["order"]) + args["order"] = order.Map() + orderId = event.Topics[2] + trader = getAddressFromTopicHash(event.Topics[1]) + case cep.iocOrderBookABI.Events["OrderRejected"].ID: + err := cep.iocOrderBookABI.UnpackIntoMap(args, "OrderRejected", event.Data) + if err != nil { + log.Error("error in iocOrderBookABI.UnpackIntoMap", "method", "OrderRejected", "err", err) + continue + } + eventName = "OrderRejected" + order := IOCOrder{} + order.DecodeFromRawOrder(args["order"]) + args["order"] = order.Map() + orderId = event.Topics[2] + trader = getAddressFromTopicHash(event.Topics[1]) + } + + case cep.SignedOrderBookContractAddress: + orderType = Signed.String() + switch event.Topics[0] { + case cep.signedOrderBookABI.Events["OrderCancelAccepted"].ID: + err := cep.signedOrderBookABI.UnpackIntoMap(args, "OrderCancelAccepted", event.Data) + if err != nil { + log.Error("error in signedOrderBookABI.UnpackIntoMap", "method", "OrderCancelAccepted", "err", err) + continue + } + eventName = "OrderCancelAccepted" + orderId = event.Topics[2] + trader = getAddressFromTopicHash(event.Topics[1]) + + case cep.signedOrderBookABI.Events["OrderCancelRejected"].ID: + err := cep.signedOrderBookABI.UnpackIntoMap(args, "OrderCancelRejected", event.Data) + if err != nil { + log.Error("error in signedOrderBookABI.UnpackIntoMap", "method", "OrderCancelRejected", "err", err) + continue + } + eventName = "OrderCancelRejected" + orderId = event.Topics[2] + trader = getAddressFromTopicHash(event.Topics[1]) + } + } + + timestamp := args["timestamp"] + timestampInt, _ := timestamp.(*big.Int) + traderEvent := TraderEvent{ + Trader: trader, + Removed: removed, + EventName: eventName, + Args: args, + BlockNumber: big.NewInt(int64(event.BlockNumber)), + BlockStatus: blockStatus, + OrderId: orderId, + OrderType: orderType, + Timestamp: timestampInt, + TransactionHash: txHash, + } + + traderFeed.Send(traderEvent) + } +} + +func (cep *ContractEventsProcessor) PushToMarketFeed(events []*types.Log, blockStatus BlockConfirmationLevel) { + for _, event := range events { + args := map[string]interface{}{} + switch event.Topics[0] { + case cep.clearingHouseABI.Events["PositionModified"].ID: + err := cep.clearingHouseABI.UnpackIntoMap(args, "PositionModified", event.Data) + if err != nil { + log.Error("error in clearingHouseABI.UnpackIntoMap", "method", "PositionModified", "err", err) + return + } + + trader := getAddressFromTopicHash(event.Topics[1]) + market := Market(int(event.Topics[2].Big().Int64())) + price := args["price"].(*big.Int) + + size := args["baseAsset"].(*big.Int) + + timestamp := args["timestamp"] + timestampInt, _ := timestamp.(*big.Int) + marketFeedEvent := MarketFeedEvent{ + Trader: trader, + Market: market, + Size: utils.BigIntToFloat(size, 18), + Price: utils.BigIntToFloat(price, 6), + Removed: event.Removed, + EventName: "PositionModified", + BlockNumber: big.NewInt(int64(event.BlockNumber)), + BlockStatus: blockStatus, + Timestamp: timestampInt, + TransactionHash: event.TxHash, + } + marketFeed.Send(marketFeedEvent) + } + } +} + +func (cep *ContractEventsProcessor) updateMetrics(logs []*types.Log) { + var orderAcceptedCount int64 = 0 + var orderCancelledCount int64 = 0 + for _, event := range logs { + var contractABI abi.ABI + switch event.Address { + case OrderBookContractAddress: + contractABI = cep.orderBookABI + case MarginAccountContractAddress: + contractABI = cep.marginAccountABI + case ClearingHouseContractAddress: + contractABI = cep.clearingHouseABI + case LimitOrderBookContractAddress: + contractABI = cep.limitOrderBookABI + case IOCOrderBookContractAddress: + contractABI = cep.iocOrderBookABI + case cep.SignedOrderBookContractAddress: + contractABI = cep.signedOrderBookABI + } + + event_, err := contractABI.EventByID(event.Topics[0]) + if err != nil { + continue + } + + metricName := fmt.Sprintf("%s/%s", "events", event_.Name) + + metrics.GetOrRegisterCounter(metricName, nil).Inc(1) + + switch event_.Name { + case "OrderAccepted": + orderAcceptedCount += 1 + case "OrderCancelAccepted": + orderCancelledCount += 1 + case "OrderMatchingError": + // separate metrics for combination of order type and error string - for more granular analysis + args := map[string]interface{}{} + err := cep.orderBookABI.UnpackIntoMap(args, "OrderMatchingError", event.Data) + if err != nil { + log.Error("error in orderBookAbi.UnpackIntoMap", "method", "OrderMatchingError", "err", err) + return + } + orderId := event.Topics[1] + errorString := args["err"].(string) + + order := cep.database.GetOrderById(orderId) + if order != nil { + ordertype := order.OrderType + metricName := fmt.Sprintf("%s/%s/%s/%s", "events", "OrderMatchingError", ordertype, utils.RemoveSpacesAndSpecialChars(errorString)) + metrics.GetOrRegisterCounter(metricName, nil).Inc(1) + } else { + log.Error("updateMetrics - error in getting order", "event", "OrderMatchingError") + } + } + } + + ordersPlacedPerBlock.Update(orderAcceptedCount) + ordersCancelledPerBlock.Update(orderCancelledCount) +} + +func getAddressFromTopicHash(topicHash common.Hash) common.Address { + return common.BytesToAddress(topicHash.Bytes()) +} diff --git a/plugin/evm/orderbook/contract_events_processor_test.go b/plugin/evm/orderbook/contract_events_processor_test.go new file mode 100644 index 0000000000..87dc23017b --- /dev/null +++ b/plugin/evm/orderbook/contract_events_processor_test.go @@ -0,0 +1,783 @@ +package orderbook + +import ( + "math/big" + "testing" + "time" + + "github.com/ava-labs/subnet-evm/accounts/abi" + "github.com/ava-labs/subnet-evm/core/types" + "github.com/ava-labs/subnet-evm/plugin/evm/orderbook/abis" + hu "github.com/ava-labs/subnet-evm/plugin/evm/orderbook/hubbleutils" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/assert" +) + +var timestamp = big.NewInt(time.Now().Unix()) + +func TestProcessEvents(t *testing.T) { + // this test is obsolete because we expect the events to automatically come in sorted order + t.Run("it sorts events by blockNumber and executes in order", func(t *testing.T) { + db := getDatabase() + cep := newcep(t, db) + orderBookABI := getABIfromJson(abis.OrderBookAbi) + limitOrderBookABI := getABIfromJson(abis.LimitOrderBookAbi) + + traderAddress := common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8") + ammIndex := big.NewInt(0) + baseAssetQuantity := big.NewInt(5000000000000000000) + price := big.NewInt(1000000000) + salt1 := big.NewInt(1675239557437) + longOrder := getLimitOrder(ammIndex, traderAddress, baseAssetQuantity, price, salt1) + longOrderId := getIdFromLimitOrder(longOrder) + + salt2 := big.NewInt(0).Add(salt1, big.NewInt(1)) + shortOrder := getLimitOrder(ammIndex, traderAddress, big.NewInt(0).Neg(baseAssetQuantity), price, salt2) + shortOrderId := getIdFromLimitOrder(shortOrder) + + ordersPlacedBlockNumber := uint64(12) + orderAcceptedEvent := getEventFromABI(limitOrderBookABI, "OrderAccepted") + longOrderAcceptedEventTopics := []common.Hash{orderAcceptedEvent.ID, traderAddress.Hash(), longOrderId} + longOrderAcceptedEventData, err := orderAcceptedEvent.Inputs.NonIndexed().Pack(longOrder, timestamp) + if err != nil { + t.Fatalf("%s", err) + } + longOrderAcceptedEventLog := getEventLog(LimitOrderBookContractAddress, longOrderAcceptedEventTopics, longOrderAcceptedEventData, ordersPlacedBlockNumber) + + shortOrderAcceptedEventTopics := []common.Hash{orderAcceptedEvent.ID, traderAddress.Hash(), shortOrderId} + shortOrderAcceptedEventData, err := orderAcceptedEvent.Inputs.NonIndexed().Pack(shortOrder, timestamp) + if err != nil { + t.Fatalf("%s", err) + } + shortOrderAcceptedEventLog := getEventLog(LimitOrderBookContractAddress, shortOrderAcceptedEventTopics, shortOrderAcceptedEventData, ordersPlacedBlockNumber) + + orderMatchedBlockNumber := uint64(14) + orderMatchedEvent0 := getEventFromABI(orderBookABI, "OrderMatched") + orderMatchedEvent1 := getEventFromABI(orderBookABI, "OrderMatched") + orderMatchedEventTopics0 := []common.Hash{orderMatchedEvent0.ID, traderAddress.Hash(), longOrderId} + orderMatchedEventTopics1 := []common.Hash{orderMatchedEvent1.ID, traderAddress.Hash(), shortOrderId} + fillAmount := big.NewInt(3000000000000000000) + orderMatchedEventData0, _ := orderMatchedEvent0.Inputs.NonIndexed().Pack(fillAmount, price, big.NewInt(0), timestamp, false) + orderMatchedEventData1, _ := orderMatchedEvent0.Inputs.NonIndexed().Pack(fillAmount, price, big.NewInt(0), timestamp, false) + orderMatchedEventLog0 := getEventLog(OrderBookContractAddress, orderMatchedEventTopics0, orderMatchedEventData0, orderMatchedBlockNumber) + orderMatchedEventLog1 := getEventLog(OrderBookContractAddress, orderMatchedEventTopics1, orderMatchedEventData1, orderMatchedBlockNumber) + cep.ProcessEvents([]*types.Log{longOrderAcceptedEventLog, shortOrderAcceptedEventLog, orderMatchedEventLog0, orderMatchedEventLog1}) + + actualLongOrder := db.Orders[longOrderId] + assert.Equal(t, fillAmount, actualLongOrder.FilledBaseAssetQuantity) + + actualShortOrder := db.Orders[shortOrderId] + assert.Equal(t, big.NewInt(0).Neg(fillAmount), actualShortOrder.FilledBaseAssetQuantity) + }) + + // t.Run("when event is removed it is not processed", func(t *testing.T) { + // traderAddress := common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8") + // db := getDatabase() + // collateral := HUSD + // originalMargin := hu.Mul1e6(big.NewInt(100)) + // trader := &Trader{ + // Margins: map[Collateral]*big.Int{collateral: big.NewInt(0).Set(originalMargin)}, + // } + // db.TraderMap[traderAddress] = trader + // blockNumber := uint64(12) + + // //MarginAccount Contract log + // marginAccountABI := getABIfromJson(marginAccountAbi) + // marginAccountEvent := getEventFromABI(marginAccountABI, "MarginAdded") + // marginAccountEventTopics := []common.Hash{marginAccountEvent.ID, traderAddress.Hash(), common.BigToHash(big.NewInt(int64(collateral)))} + // marginAdded := hu.Mul1e6(big.NewInt(100)) + // timestamp := big.NewInt(time.Now().Unix()) + // marginAddedEventData, _ := marginAccountEvent.Inputs.NonIndexed().Pack(marginAdded, timestamp) + // marginAddedLog := getEventLog(MarginAccountContractAddress, marginAccountEventTopics, marginAddedEventData, blockNumber) + // marginAddedLog.Removed = true + // cep := newcep(t, db) + + // cep.ProcessEvents([]*types.Log{marginAddedLog}) + // assert.Equal(t, originalMargin, db.TraderMap[traderAddress].Margins[collateral]) + // }) +} + +func TestOrderBookMarginAccountClearingHouseEventInLog(t *testing.T) { + traderAddress := common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8") + blockNumber := uint64(12) + db := getDatabase() + cep := newcep(t, db) + collateral := HUSD + openNotional := hu.Mul1e6(big.NewInt(100)) + size := hu.Mul1e18(big.NewInt(10)) + lastPremiumFraction := hu.Mul1e6(big.NewInt(1)) + liquidationThreshold := hu.Mul1e6(big.NewInt(1)) + unrealisedFunding := hu.Mul1e6(big.NewInt(1)) + market := Market(0) + position := &Position{ + Position: hu.Position{OpenNotional: openNotional, Size: size}, + UnrealisedFunding: unrealisedFunding, + LastPremiumFraction: lastPremiumFraction, + LiquidationThreshold: liquidationThreshold, + } + originalMargin := hu.Mul1e6(big.NewInt(100)) + trader := &Trader{ + Margin: Margin{Deposited: map[Collateral]*big.Int{collateral: big.NewInt(0).Set(originalMargin)}}, + Positions: map[Market]*Position{market: position}, + } + db.TraderMap[traderAddress] = trader + + //OrderBook Contract log + ammIndex := big.NewInt(0) + baseAssetQuantity := big.NewInt(5000000000000000000) + price := big.NewInt(1000000000) + salt := big.NewInt(1675239557437) + order := getLimitOrder(ammIndex, traderAddress, baseAssetQuantity, price, salt) + limitOrderBookABI := getABIfromJson(abis.LimitOrderBookAbi) + orderBookEvent := getEventFromABI(limitOrderBookABI, "OrderAccepted") + orderAcceptedEventData, _ := orderBookEvent.Inputs.NonIndexed().Pack(order, timestamp) + orderBookEventTopics := []common.Hash{orderBookEvent.ID, traderAddress.Hash(), getIdFromLimitOrder(order)} + orderBookLog := getEventLog(LimitOrderBookContractAddress, orderBookEventTopics, orderAcceptedEventData, blockNumber) + + //MarginAccount Contract log + marginAccountABI := getABIfromJson(abis.MarginAccountAbi) + marginAccountEvent := getEventFromABI(marginAccountABI, "MarginAdded") + marginAccountEventTopics := []common.Hash{marginAccountEvent.ID, traderAddress.Hash(), common.BigToHash(big.NewInt(int64(collateral)))} + marginAdded := hu.Mul1e6(big.NewInt(100)) + marginAddedEventData, _ := marginAccountEvent.Inputs.NonIndexed().Pack(marginAdded, timestamp) + marginAccountLog := getEventLog(MarginAccountContractAddress, marginAccountEventTopics, marginAddedEventData, blockNumber) + + //ClearingHouse Contract log + clearingHouseABI := getABIfromJson(abis.ClearingHouseAbi) + clearingHouseEvent := getEventFromABI(clearingHouseABI, "FundingRateUpdated") + clearingHouseEventTopics := []common.Hash{clearingHouseEvent.ID, common.BigToHash(big.NewInt(int64(market)))} + + nextFundingTime := big.NewInt(time.Now().Unix()) + premiumFraction := hu.Mul1e6(big.NewInt(10)) + underlyingPrice := hu.Mul1e6(big.NewInt(100)) + cumulativePremiumFraction := hu.Mul1e6(big.NewInt(10)) + fundingRateUpdated, _ := clearingHouseEvent.Inputs.NonIndexed().Pack(premiumFraction, underlyingPrice, cumulativePremiumFraction, nextFundingTime, timestamp, big.NewInt(int64(blockNumber))) + clearingHouseLog := getEventLog(ClearingHouseContractAddress, clearingHouseEventTopics, fundingRateUpdated, blockNumber) + + // logs := []*types.Log{orderBookLog, marginAccountLog, clearingHouseLog} + cep.ProcessEvents([]*types.Log{orderBookLog}) + cep.ProcessAcceptedEvents([]*types.Log{marginAccountLog, clearingHouseLog}, true) + + //OrderBook log - OrderAccepted + actualLimitOrder := *db.GetOrderBookData().Orders[getIdFromLimitOrder(order)] + args := map[string]interface{}{} + limitOrderBookABI.UnpackIntoMap(args, "OrderAccepted", orderAcceptedEventData) + assert.Equal(t, Market(ammIndex.Int64()), actualLimitOrder.Market) + assert.Equal(t, LONG, actualLimitOrder.PositionType) + assert.Equal(t, traderAddress.String(), actualLimitOrder.Trader.String()) + assert.Equal(t, *baseAssetQuantity, *actualLimitOrder.BaseAssetQuantity) + assert.Equal(t, *price, *actualLimitOrder.Price) + assert.Equal(t, Placed, actualLimitOrder.getOrderStatus().Status) + assert.Equal(t, big.NewInt(int64(blockNumber)), actualLimitOrder.BlockNumber) + rawOrder := &LimitOrder{} + rawOrder.DecodeFromRawOrder(args["order"]) + assert.Equal(t, rawOrder, actualLimitOrder.RawOrder.(*LimitOrder)) + + //ClearingHouse log - FundingRateUpdated + expectedUnrealisedFunding := hu.Div1e18(big.NewInt(0).Mul(big.NewInt(0).Sub(cumulativePremiumFraction, position.LastPremiumFraction), position.Size)) + assert.Equal(t, expectedUnrealisedFunding, db.TraderMap[traderAddress].Positions[market].UnrealisedFunding) + + //MarginAccount log - marginAdded + actualMargin := db.GetOrderBookData().TraderMap[traderAddress].Margin.Deposited[collateral] + assert.Equal(t, big.NewInt(0).Add(marginAdded, originalMargin), actualMargin) + +} + +func TestHandleOrderBookEvent(t *testing.T) { + traderAddress := common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8") + ammIndex := big.NewInt(0) + baseAssetQuantity := big.NewInt(5000000000000000000) + price := big.NewInt(1000000000) + salt := big.NewInt(1675239557437) + blockNumber := uint64(12) + orderBookABI := getABIfromJson(abis.OrderBookAbi) + limitOrderBookABI := getABIfromJson(abis.LimitOrderBookAbi) + + t.Run("When event is OrderAccepted", func(t *testing.T) { + db := getDatabase() + cep := newcep(t, db) + event := getEventFromABI(limitOrderBookABI, "OrderAccepted") + order := getLimitOrder(ammIndex, traderAddress, baseAssetQuantity, price, salt) + orderId := getIdFromLimitOrder(order) + topics := []common.Hash{event.ID, traderAddress.Hash(), orderId} + t.Run("When data in log unpack fails", func(t *testing.T) { + orderAcceptedEventData := []byte{} + log := getEventLog(LimitOrderBookContractAddress, topics, orderAcceptedEventData, blockNumber) + cep.ProcessEvents([]*types.Log{log}) + actualLimitOrder := db.GetOrderBookData().Orders[orderId] + assert.Nil(t, actualLimitOrder) + }) + t.Run("When data in log unpack succeeds", func(t *testing.T) { + orderAcceptedEventData, err := event.Inputs.NonIndexed().Pack(order, timestamp) + if err != nil { + t.Fatalf("%s", err) + } + log := getEventLog(LimitOrderBookContractAddress, topics, orderAcceptedEventData, blockNumber) + cep.ProcessEvents([]*types.Log{log}) + + actualLimitOrder := db.GetOrderBookData().Orders[orderId] + args := map[string]interface{}{} + limitOrderBookABI.UnpackIntoMap(args, "OrderAccepted", orderAcceptedEventData) + assert.Equal(t, Market(ammIndex.Int64()), actualLimitOrder.Market) + assert.Equal(t, LONG, actualLimitOrder.PositionType) + assert.Equal(t, traderAddress.String(), actualLimitOrder.Trader.String()) + assert.Equal(t, *baseAssetQuantity, *actualLimitOrder.BaseAssetQuantity) + assert.Equal(t, false, actualLimitOrder.ReduceOnly) + assert.Equal(t, false, actualLimitOrder.isPostOnly()) + assert.Equal(t, *price, *actualLimitOrder.Price) + assert.Equal(t, Placed, actualLimitOrder.getOrderStatus().Status) + assert.Equal(t, big.NewInt(int64(blockNumber)), actualLimitOrder.BlockNumber) + rawOrder := &LimitOrder{} + rawOrder.DecodeFromRawOrder(args["order"]) + assert.Equal(t, rawOrder, actualLimitOrder.RawOrder.(*LimitOrder)) + }) + }) + t.Run("When event is OrderCancelAccepted", func(t *testing.T) { + db := getDatabase() + cep := newcep(t, db) + order := getLimitOrder(ammIndex, traderAddress, baseAssetQuantity, price, salt) + event := getEventFromABI(limitOrderBookABI, "OrderCancelAccepted") + topics := []common.Hash{event.ID, traderAddress.Hash(), getIdFromLimitOrder(order)} + blockNumber := uint64(4) + limitOrder := &Order{ + Market: Market(ammIndex.Int64()), + PositionType: LONG, + Trader: traderAddress, + BaseAssetQuantity: baseAssetQuantity, + Price: price, + BlockNumber: big.NewInt(1), + Salt: salt, + RawOrder: &order, + } + limitOrder.Id = getIdFromOrder(*limitOrder) + db.Add(limitOrder) + t.Run("When data in log unpack fails", func(t *testing.T) { + orderCancelAcceptedEventData := []byte{} + log := getEventLog(LimitOrderBookContractAddress, topics, orderCancelAcceptedEventData, blockNumber) + cep.ProcessEvents([]*types.Log{log}) + orderId := getIdFromOrder(*limitOrder) + actualLimitOrder := db.GetOrderBookData().Orders[orderId] + assert.Equal(t, limitOrder, actualLimitOrder) + }) + t.Run("When data in log unpack succeeds", func(t *testing.T) { + orderCancelAcceptedEventData, err := event.Inputs.NonIndexed().Pack(timestamp, false) + if err != nil { + t.Fatalf("%s", err) + } + log := getEventLog(LimitOrderBookContractAddress, topics, orderCancelAcceptedEventData, blockNumber) + orderId := getIdFromOrder(*limitOrder) + cep.ProcessEvents([]*types.Log{log}) + actualLimitOrder := db.GetOrderBookData().Orders[orderId] + assert.Equal(t, Cancelled, actualLimitOrder.getOrderStatus().Status) + }) + }) + t.Run("When event is OrderMatched", func(t *testing.T) { + db := getDatabase() + cep := newcep(t, db) + event := getEventFromABI(orderBookABI, "OrderMatched") + longOrder := &Order{ + Market: Market(ammIndex.Int64()), + PositionType: LONG, + Trader: traderAddress, + BaseAssetQuantity: baseAssetQuantity, + Price: price, + BlockNumber: big.NewInt(1), + FilledBaseAssetQuantity: big.NewInt(0), + Salt: salt, + } + shortOrder := &Order{ + Market: Market(ammIndex.Int64()), + PositionType: SHORT, + Trader: traderAddress, + BaseAssetQuantity: big.NewInt(0).Mul(baseAssetQuantity, big.NewInt(-1)), + Price: price, + BlockNumber: big.NewInt(1), + FilledBaseAssetQuantity: big.NewInt(0), + Salt: big.NewInt(0).Add(salt, big.NewInt(1000)), + } + + longOrder.Id = getIdFromOrder(*longOrder) + shortOrder.Id = getIdFromOrder(*shortOrder) + db.Add(longOrder) + db.Add(shortOrder) + // relayer := common.HexToAddress("0x710bf5F942331874dcBC7783319123679033b63b") + fillAmount := big.NewInt(10) + topics0 := []common.Hash{event.ID, traderAddress.Hash(), longOrder.Id} + topics1 := []common.Hash{event.ID, traderAddress.Hash(), shortOrder.Id} + t.Run("When data in log unpack fails", func(t *testing.T) { + orderMatchedEventData := []byte{} + log := getEventLog(OrderBookContractAddress, topics0, orderMatchedEventData, blockNumber) + cep.ProcessEvents([]*types.Log{log}) + assert.Equal(t, int64(0), longOrder.FilledBaseAssetQuantity.Int64()) + assert.Equal(t, int64(0), shortOrder.FilledBaseAssetQuantity.Int64()) + }) + t.Run("When data in log unpack succeeds", func(t *testing.T) { + orderMatchedEventData, err := event.Inputs.NonIndexed().Pack(fillAmount, price, big.NewInt(0).Mul(fillAmount, price), timestamp, false) + if err != nil { + t.Fatalf("%s", err) + } + log0 := getEventLog(OrderBookContractAddress, topics0, orderMatchedEventData, blockNumber) + log1 := getEventLog(OrderBookContractAddress, topics1, orderMatchedEventData, blockNumber) + cep.ProcessEvents([]*types.Log{log0, log1}) + assert.Equal(t, big.NewInt(fillAmount.Int64()), longOrder.FilledBaseAssetQuantity) + assert.Equal(t, big.NewInt(-fillAmount.Int64()), shortOrder.FilledBaseAssetQuantity) + }) + }) +} + +func TestHandleMarginAccountEvent(t *testing.T) { + traderAddress := common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8") + blockNumber := uint64(12) + collateral := HUSD + + marginAccountABI := getABIfromJson(abis.MarginAccountAbi) + + t.Run("when event is MarginAdded", func(t *testing.T) { + db := getDatabase() + cep := newcep(t, db) + event := getEventFromABI(marginAccountABI, "MarginAdded") + topics := []common.Hash{event.ID, traderAddress.Hash(), common.BigToHash(big.NewInt(int64(collateral)))} + t.Run("When event parsing fails", func(t *testing.T) { + marginAddedEventData := []byte{} + log := getEventLog(MarginAccountContractAddress, topics, marginAddedEventData, blockNumber) + cep.ProcessAcceptedEvents([]*types.Log{log}, true) + assert.Nil(t, db.GetOrderBookData().TraderMap[traderAddress]) + }) + t.Run("When event parsing succeeds", func(t *testing.T) { + marginAdded := big.NewInt(10000) + timestamp := big.NewInt(time.Now().Unix()) + marginAddedEventData, _ := event.Inputs.NonIndexed().Pack(marginAdded, timestamp) + log := getEventLog(MarginAccountContractAddress, topics, marginAddedEventData, blockNumber) + cep.ProcessAcceptedEvents([]*types.Log{log}, true) + actualMargin := db.GetOrderBookData().TraderMap[traderAddress].Margin.Deposited[collateral] + assert.Equal(t, marginAdded, actualMargin) + }) + }) + t.Run("when event is MarginRemoved", func(t *testing.T) { + db := getDatabase() + cep := newcep(t, db) + event := getEventFromABI(marginAccountABI, "MarginRemoved") + topics := []common.Hash{event.ID, traderAddress.Hash(), common.BigToHash(big.NewInt(int64(collateral)))} + t.Run("When event parsing fails", func(t *testing.T) { + marginRemovedEventData := []byte{} + log := getEventLog(MarginAccountContractAddress, topics, marginRemovedEventData, blockNumber) + cep.ProcessAcceptedEvents([]*types.Log{log}, true) + assert.Nil(t, db.GetOrderBookData().TraderMap[traderAddress]) + }) + t.Run("When event parsing succeeds", func(t *testing.T) { + marginRemoved := big.NewInt(10000) + marginRemovedEventData, _ := event.Inputs.NonIndexed().Pack(marginRemoved, timestamp) + log := getEventLog(MarginAccountContractAddress, topics, marginRemovedEventData, blockNumber) + cep.ProcessAcceptedEvents([]*types.Log{log}, true) + actualMargin := db.GetOrderBookData().TraderMap[traderAddress].Margin.Deposited[collateral] + assert.Equal(t, big.NewInt(0).Neg(marginRemoved), actualMargin) + }) + }) + t.Run("when event is PnLRealized", func(t *testing.T) { + event := getEventFromABI(marginAccountABI, "PnLRealized") + topics := []common.Hash{event.ID, traderAddress.Hash()} + db := getDatabase() + cep := newcep(t, db) + t.Run("When event parsing fails", func(t *testing.T) { + pnlRealizedEventData := []byte{} + log := getEventLog(MarginAccountContractAddress, topics, pnlRealizedEventData, blockNumber) + cep.ProcessAcceptedEvents([]*types.Log{log}, true) + assert.Nil(t, db.GetOrderBookData().TraderMap[traderAddress]) + }) + t.Run("When event parsing succeeds", func(t *testing.T) { + pnlRealized := big.NewInt(-10000) + pnlRealizedEventData, _ := event.Inputs.NonIndexed().Pack(pnlRealized, timestamp) + log := getEventLog(MarginAccountContractAddress, topics, pnlRealizedEventData, blockNumber) + cep.ProcessAcceptedEvents([]*types.Log{log}, true) + actualMargin := db.GetOrderBookData().TraderMap[traderAddress].Margin.Deposited[collateral] + assert.Equal(t, pnlRealized, actualMargin) + }) + }) + + t.Run("when event is MarginReserved", func(t *testing.T) { + event := getEventFromABI(marginAccountABI, "MarginReserved") + topics := []common.Hash{event.ID, traderAddress.Hash()} + db := getDatabase() + cep := newcep(t, db) + t.Run("When event parsing fails", func(t *testing.T) { + marginReservedEventData := []byte{} + log := getEventLog(MarginAccountContractAddress, topics, marginReservedEventData, blockNumber) + cep.ProcessAcceptedEvents([]*types.Log{log}, true) + assert.Nil(t, db.GetOrderBookData().TraderMap[traderAddress]) + }) + t.Run("When event parsing succeeds", func(t *testing.T) { + reservedMargin := big.NewInt(10000000) + marginReservedEventData, _ := event.Inputs.NonIndexed().Pack(reservedMargin) + log := getEventLog(MarginAccountContractAddress, topics, marginReservedEventData, blockNumber) + cep.ProcessAcceptedEvents([]*types.Log{log}, true) + reservedMarginInDb := db.GetOrderBookData().TraderMap[traderAddress].Margin.Reserved + assert.Equal(t, reservedMargin, reservedMarginInDb) + }) + }) + + t.Run("when event is MarginReleased", func(t *testing.T) { + event := getEventFromABI(marginAccountABI, "MarginReleased") + topics := []common.Hash{event.ID, traderAddress.Hash()} + db := getDatabase() + cep := newcep(t, db) + t.Run("When event parsing fails", func(t *testing.T) { + marginReleasedEventData := []byte{} + log := getEventLog(MarginAccountContractAddress, topics, marginReleasedEventData, blockNumber) + cep.ProcessAcceptedEvents([]*types.Log{log}, true) + assert.Nil(t, db.GetOrderBookData().TraderMap[traderAddress]) + }) + t.Run("When event parsing succeeds", func(t *testing.T) { + releasedMargin := big.NewInt(10000000) + marginReleasedEventData, _ := event.Inputs.NonIndexed().Pack(releasedMargin) + log := getEventLog(MarginAccountContractAddress, topics, marginReleasedEventData, blockNumber) + cep.ProcessAcceptedEvents([]*types.Log{log}, true) + releasedMarginInDb := db.GetOrderBookData().TraderMap[traderAddress].Margin.Reserved + assert.Equal(t, big.NewInt(0).Neg(releasedMargin), releasedMarginInDb) + }) + }) +} +func TestHandleClearingHouseEvent(t *testing.T) { + traderAddress := common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8") + blockNumber := uint64(12) + collateral := HUSD + market := Market(0) + clearingHouseABI := getABIfromJson(abis.ClearingHouseAbi) + openNotional := hu.Mul1e6(big.NewInt(100)) + size := hu.Mul1e18(big.NewInt(10)) + lastPremiumFraction := hu.Mul1e6(big.NewInt(1)) + liquidationThreshold := hu.Mul1e6(big.NewInt(1)) + unrealisedFunding := hu.Mul1e6(big.NewInt(1)) + t.Run("when event is FundingRateUpdated", func(t *testing.T) { + event := getEventFromABI(clearingHouseABI, "FundingRateUpdated") + topics := []common.Hash{event.ID, common.BigToHash(big.NewInt(int64(market)))} + db := getDatabase() + cep := newcep(t, db) + position := &Position{ + Position: hu.Position{OpenNotional: openNotional, Size: size}, + UnrealisedFunding: unrealisedFunding, + LastPremiumFraction: lastPremiumFraction, + LiquidationThreshold: liquidationThreshold, + } + trader := &Trader{ + Margin: Margin{Deposited: map[Collateral]*big.Int{collateral: big.NewInt(100)}}, + Positions: map[Market]*Position{market: position}, + } + db.TraderMap[traderAddress] = trader + + t.Run("When event parsing fails", func(t *testing.T) { + pnlRealizedEventData := []byte{} + log := getEventLog(ClearingHouseContractAddress, topics, pnlRealizedEventData, blockNumber) + cep.ProcessEvents([]*types.Log{log}) + + assert.Equal(t, uint64(0), db.NextFundingTime) + assert.Equal(t, unrealisedFunding, db.TraderMap[traderAddress].Positions[market].UnrealisedFunding) + }) + t.Run("When event parsing succeeds", func(t *testing.T) { + nextFundingTime := big.NewInt(time.Now().Unix()) + premiumFraction := hu.Mul1e6(big.NewInt(10)) + underlyingPrice := hu.Mul1e6(big.NewInt(100)) + cumulativePremiumFraction := hu.Mul1e6(big.NewInt(10)) + fundingRateUpdated, _ := event.Inputs.NonIndexed().Pack(premiumFraction, underlyingPrice, cumulativePremiumFraction, nextFundingTime, timestamp, big.NewInt(int64(blockNumber))) + log := getEventLog(ClearingHouseContractAddress, topics, fundingRateUpdated, blockNumber) + cep.ProcessAcceptedEvents([]*types.Log{log}, true) + expectedUnrealisedFunding := hu.Div1e18(big.NewInt(0).Mul(big.NewInt(0).Sub(cumulativePremiumFraction, position.LastPremiumFraction), position.Size)) + assert.Equal(t, expectedUnrealisedFunding, db.TraderMap[traderAddress].Positions[market].UnrealisedFunding) + }) + }) + t.Run("When event is FundingPaid", func(t *testing.T) { + event := getEventFromABI(clearingHouseABI, "FundingPaid") + topics := []common.Hash{event.ID, traderAddress.Hash(), common.BigToHash(big.NewInt(int64(market)))} + db := getDatabase() + cep := newcep(t, db) + position := &Position{ + Position: hu.Position{OpenNotional: openNotional, Size: size}, + UnrealisedFunding: unrealisedFunding, + LastPremiumFraction: lastPremiumFraction, + LiquidationThreshold: liquidationThreshold, + } + trader := &Trader{ + Margin: Margin{Deposited: map[Collateral]*big.Int{collateral: big.NewInt(100)}}, + Positions: map[Market]*Position{market: position}, + } + db.TraderMap[traderAddress] = trader + + t.Run("When event parsing fails", func(t *testing.T) { + pnlRealizedEventData := []byte{} + log := getEventLog(ClearingHouseContractAddress, topics, pnlRealizedEventData, blockNumber) + cep.ProcessAcceptedEvents([]*types.Log{log}, true) + + assert.Equal(t, unrealisedFunding, db.TraderMap[traderAddress].Positions[market].UnrealisedFunding) + assert.Equal(t, lastPremiumFraction, db.TraderMap[traderAddress].Positions[market].LastPremiumFraction) + }) + t.Run("When event parsing succeeds", func(t *testing.T) { + takerFundingPayment := hu.Mul1e6(big.NewInt(10)) + cumulativePremiumFraction := hu.Mul1e6(big.NewInt(10)) + fundingPaidEvent, _ := event.Inputs.NonIndexed().Pack(takerFundingPayment, cumulativePremiumFraction) + log := getEventLog(ClearingHouseContractAddress, topics, fundingPaidEvent, blockNumber) + cep.ProcessAcceptedEvents([]*types.Log{log}, true) + assert.Equal(t, big.NewInt(0), db.TraderMap[traderAddress].Positions[market].UnrealisedFunding) + assert.Equal(t, cumulativePremiumFraction, db.TraderMap[traderAddress].Positions[market].LastPremiumFraction) + }) + }) + t.Run("When event is PositionModified", func(t *testing.T) { + event := getEventFromABI(clearingHouseABI, "PositionModified") + topics := []common.Hash{event.ID, traderAddress.Hash(), common.BigToHash(big.NewInt(int64(market)))} + db := getDatabase() + cep := newcep(t, db) + position := &Position{ + Position: hu.Position{OpenNotional: openNotional, Size: size}, + UnrealisedFunding: unrealisedFunding, + LastPremiumFraction: lastPremiumFraction, + LiquidationThreshold: liquidationThreshold, + } + trader := &Trader{ + Margin: Margin{Deposited: map[Collateral]*big.Int{collateral: big.NewInt(100)}}, + Positions: map[Market]*Position{market: position}, + } + db.TraderMap[traderAddress] = trader + + t.Run("When event parsing fails", func(t *testing.T) { + positionModifiedEvent := []byte{} + log := getEventLog(ClearingHouseContractAddress, topics, positionModifiedEvent, blockNumber) + cep.ProcessAcceptedEvents([]*types.Log{log}, true) + assert.Nil(t, db.LastPrice[market]) + // assert.Equal(t, big.NewInt(0), db.LastPrice[market]) + }) + t.Run("When event parsing succeeds", func(t *testing.T) { + baseAsset := hu.Mul1e18(big.NewInt(10)) + // quoteAsset := hu.Mul1e6(big.NewInt(1000)) + realizedPnl := hu.Mul1e6(big.NewInt(20)) + openNotional := hu.Mul1e6(big.NewInt(4000)) + timestamp := hu.Mul1e6(big.NewInt(time.Now().Unix())) + size := hu.Mul1e18(big.NewInt(40)) + price := hu.Mul1e6(big.NewInt(100)) // baseAsset / quoteAsset + + positionModifiedEvent, err := event.Inputs.NonIndexed().Pack(baseAsset, price, realizedPnl, size, openNotional, big.NewInt(0), uint8(0), timestamp) + if err != nil { + t.Fatal(err) + } + log := getEventLog(ClearingHouseContractAddress, topics, positionModifiedEvent, blockNumber) + cep.ProcessAcceptedEvents([]*types.Log{log}, true) + + // quoteAsset/(baseAsset / 1e 18) + expectedLastPrice := big.NewInt(100000000) + assert.Equal(t, expectedLastPrice, db.LastPrice[market]) + assert.Equal(t, size, db.TraderMap[traderAddress].Positions[market].Size) + assert.Equal(t, openNotional, db.TraderMap[traderAddress].Positions[market].OpenNotional) + }) + }) + t.Run("When event is PositionLiquidated", func(t *testing.T) { + event := getEventFromABI(clearingHouseABI, "PositionLiquidated") + topics := []common.Hash{event.ID, traderAddress.Hash(), common.BigToHash(big.NewInt(int64(market)))} + db := getDatabase() + cep := newcep(t, db) + position := &Position{ + Position: hu.Position{OpenNotional: openNotional, Size: size}, + UnrealisedFunding: unrealisedFunding, + LastPremiumFraction: lastPremiumFraction, + LiquidationThreshold: liquidationThreshold, + } + trader := &Trader{ + Margin: Margin{Deposited: map[Collateral]*big.Int{collateral: big.NewInt(100)}}, + Positions: map[Market]*Position{market: position}, + } + db.TraderMap[traderAddress] = trader + + t.Run("When event parsing fails", func(t *testing.T) { + positionLiquidatedEvent := []byte{} + log := getEventLog(ClearingHouseContractAddress, topics, positionLiquidatedEvent, blockNumber) + cep.ProcessAcceptedEvents([]*types.Log{log}, true) + assert.Nil(t, db.LastPrice[market]) + }) + t.Run("When event parsing succeeds", func(t *testing.T) { + baseAsset := hu.Mul1e18(big.NewInt(10)) + // quoteAsset := hu.Mul1e6(big.NewInt(1000)) + realizedPnl := hu.Mul1e6(big.NewInt(20)) + openNotional := hu.Mul1e6(big.NewInt(4000)) + timestamp := hu.Mul1e6(big.NewInt(time.Now().Unix())) + size := hu.Mul1e18(big.NewInt(40)) + price := hu.Mul1e6(big.NewInt(100)) // baseAsset / quoteAsset + + positionLiquidatedEvent, _ := event.Inputs.NonIndexed().Pack(baseAsset, price, realizedPnl, size, openNotional, big.NewInt(0), timestamp) + log := getEventLog(ClearingHouseContractAddress, topics, positionLiquidatedEvent, blockNumber) + cep.ProcessAcceptedEvents([]*types.Log{log}, true) + + // quoteAsset/(baseAsset / 1e 18) + expectedLastPrice := big.NewInt(100000000) + assert.Equal(t, expectedLastPrice, db.LastPrice[market]) + assert.Equal(t, size, db.TraderMap[traderAddress].Positions[market].Size) + assert.Equal(t, openNotional, db.TraderMap[traderAddress].Positions[market].OpenNotional) + }) + }) +} + +func TestRemovedEvents(t *testing.T) { + traderAddress := common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8") + traderAddress2 := common.HexToAddress("0xC348509BD9dD348b963B4ae0CB876782387729a0") + blockNumber := big.NewInt(12) + ammIndex := big.NewInt(0) + baseAssetQuantity := big.NewInt(50) + salt1 := big.NewInt(1675239557437) + salt2 := big.NewInt(1675239557439) + orderBookABI := getABIfromJson(abis.OrderBookAbi) + limitOrderrderBookABI := getABIfromJson(abis.LimitOrderBookAbi) + + db := getDatabase() + cep := newcep(t, db) + + orderAcceptedEvent := getEventFromABI(limitOrderrderBookABI, "OrderAccepted") + longOrder := getLimitOrder(ammIndex, traderAddress, baseAssetQuantity, price, salt1) + longOrderId := getIdFromLimitOrder(longOrder) + longOrderAcceptedEventTopics := []common.Hash{orderAcceptedEvent.ID, traderAddress.Hash(), longOrderId} + longOrderAcceptedEventData, _ := orderAcceptedEvent.Inputs.NonIndexed().Pack(longOrder, timestamp) + + shortOrder := getLimitOrder(ammIndex, traderAddress2, big.NewInt(0).Neg(baseAssetQuantity), price, salt2) + shortOrderId := getIdFromLimitOrder(shortOrder) + shortOrderAcceptedEventTopics := []common.Hash{orderAcceptedEvent.ID, traderAddress2.Hash(), shortOrderId} + shortOrderAcceptedEventData, _ := orderAcceptedEvent.Inputs.NonIndexed().Pack(shortOrder, timestamp) + + t.Run("delete order when OrderAccepted is removed", func(t *testing.T) { + longOrderAcceptedEventLog := getEventLog(LimitOrderBookContractAddress, longOrderAcceptedEventTopics, longOrderAcceptedEventData, blockNumber.Uint64()) + cep.ProcessEvents([]*types.Log{longOrderAcceptedEventLog}) + + // order exists in memory now + assert.Equal(t, db.Orders[longOrderId].Salt, longOrder.Salt) + + // order should be deleted if OrderAccepted log is removed + longOrderAcceptedEventLog.Removed = true + cep.ProcessEvents([]*types.Log{longOrderAcceptedEventLog}) + assert.Nil(t, db.Orders[longOrderId]) + }) + + t.Run("un-cancel an order when OrderCancelAccepted is removed", func(t *testing.T) { + longOrderAcceptedEventLog := getEventLog(LimitOrderBookContractAddress, longOrderAcceptedEventTopics, longOrderAcceptedEventData, blockNumber.Uint64()) + cep.ProcessEvents([]*types.Log{longOrderAcceptedEventLog}) + + // order exists in memory now + assert.Equal(t, db.Orders[longOrderId].Salt, longOrder.Salt) + + // cancel it + orderCancelAcceptedEvent := getEventFromABI(limitOrderrderBookABI, "OrderCancelAccepted") + orderCancelAcceptedEventTopics := []common.Hash{orderCancelAcceptedEvent.ID, traderAddress.Hash(), longOrderId} + orderCancelAcceptedEventData, _ := orderCancelAcceptedEvent.Inputs.NonIndexed().Pack(timestamp, false) + orderCancelAcceptedLog := getEventLog(LimitOrderBookContractAddress, orderCancelAcceptedEventTopics, orderCancelAcceptedEventData, blockNumber.Uint64()+2) + cep.ProcessEvents([]*types.Log{orderCancelAcceptedLog}) + + assert.Equal(t, db.Orders[longOrderId].getOrderStatus().Status, Cancelled) + + // now uncancel it + orderCancelAcceptedLog.Removed = true + cep.ProcessEvents([]*types.Log{orderCancelAcceptedLog}) + assert.Equal(t, db.Orders[longOrderId].getOrderStatus().Status, Placed) + }) + + t.Run("un-fulfill an order when OrderMatched is removed", func(t *testing.T) { + longOrderAcceptedEventLog := getEventLog(LimitOrderBookContractAddress, longOrderAcceptedEventTopics, longOrderAcceptedEventData, blockNumber.Uint64()) + shortOrderAcceptedEventLog := getEventLog(LimitOrderBookContractAddress, shortOrderAcceptedEventTopics, shortOrderAcceptedEventData, blockNumber.Uint64()) + cep.ProcessEvents([]*types.Log{longOrderAcceptedEventLog, shortOrderAcceptedEventLog}) + + // orders exist in memory now + assert.Equal(t, db.Orders[longOrderId].Salt, longOrder.Salt) + assert.Equal(t, db.Orders[shortOrderId].Salt, shortOrder.Salt) + + // fulfill them + orderMatchedEvent := getEventFromABI(orderBookABI, "OrderMatched") + orderMatchedEventTopics := []common.Hash{orderMatchedEvent.ID, traderAddress.Hash(), longOrderId} + orderMatchedEventData, err := orderMatchedEvent.Inputs.NonIndexed().Pack(baseAssetQuantity, price, big.NewInt(0).Mul(baseAssetQuantity, price), timestamp, false) + if err != nil { + t.Fatal(err) + } + orderMatchedLog := getEventLog(OrderBookContractAddress, orderMatchedEventTopics, orderMatchedEventData, blockNumber.Uint64()+2) + cep.ProcessEvents([]*types.Log{orderMatchedLog}) + + assert.Equal(t, db.Orders[longOrderId].getOrderStatus().Status, FulFilled) + + // now un-fulfill it + orderMatchedLog.Removed = true + cep.ProcessEvents([]*types.Log{orderMatchedLog}) + assert.Equal(t, db.Orders[longOrderId].getOrderStatus().Status, Placed) + }) + + t.Run("revert state of an order when OrderMatchingError is removed", func(t *testing.T) { + // change salt + longOrder.Salt.Add(longOrder.Salt, big.NewInt(20)) + longOrderId = getIdFromLimitOrder(longOrder) + longOrderAcceptedEventTopics = []common.Hash{orderAcceptedEvent.ID, traderAddress.Hash(), longOrderId} + longOrderAcceptedEventData, _ = orderAcceptedEvent.Inputs.NonIndexed().Pack(longOrder, timestamp) + longOrderAcceptedEventLog := getEventLog(LimitOrderBookContractAddress, longOrderAcceptedEventTopics, longOrderAcceptedEventData, blockNumber.Uint64()) + cep.ProcessEvents([]*types.Log{longOrderAcceptedEventLog}) + + // orders exist in memory now + assert.Equal(t, db.Orders[longOrderId].Salt, longOrder.Salt) + assert.Equal(t, db.Orders[longOrderId].getOrderStatus().Status, Placed) + + // fail matching + orderMatchingError := getEventFromABI(orderBookABI, "OrderMatchingError") + orderMatchingErrorTopics := []common.Hash{orderMatchingError.ID, longOrderId} + orderMatchingErrorData, _ := orderMatchingError.Inputs.NonIndexed().Pack("INSUFFICIENT_MARGIN") + orderMatchingErrorLog := getEventLog(OrderBookContractAddress, orderMatchingErrorTopics, orderMatchingErrorData, blockNumber.Uint64()+2) + cep.ProcessEvents([]*types.Log{orderMatchingErrorLog}) + + assert.Equal(t, db.Orders[longOrderId].getOrderStatus().Status, Execution_Failed) + + // now un-fail it + orderMatchingErrorLog.Removed = true + cep.ProcessEvents([]*types.Log{orderMatchingErrorLog}) + assert.Equal(t, db.Orders[longOrderId].getOrderStatus().Status, Placed) + }) +} + +func newcep(t *testing.T, db LimitOrderDatabase) *ContractEventsProcessor { + return NewContractEventsProcessor(db, common.HexToAddress("0x4c5859f0F772848b2D91F1D83E2Fe57935348029")) +} + +func getABIfromJson(jsonBytes []byte) abi.ABI { + returnedABI, err := abi.FromSolidityJson(string(jsonBytes)) + if err != nil { + panic(err) + } + return returnedABI +} + +func getEventFromABI(contractABI abi.ABI, eventName string) abi.Event { + for _, event := range contractABI.Events { + if event.Name == eventName { + return event + } + } + return abi.Event{} +} + +func getLimitOrder(ammIndex *big.Int, traderAddress common.Address, baseAssetQuantity *big.Int, price *big.Int, salt *big.Int) LimitOrder { + return LimitOrder{ + BaseOrder: hu.BaseOrder{ + AmmIndex: ammIndex, + Trader: traderAddress, + BaseAssetQuantity: baseAssetQuantity, + Price: price, + Salt: salt, + ReduceOnly: false, + }, + PostOnly: false, + } +} + +func getEventLog(contractAddress common.Address, topics []common.Hash, eventData []byte, blockNumber uint64) *types.Log { + return &types.Log{ + Address: contractAddress, + Topics: topics, + Data: eventData, + BlockNumber: blockNumber, + } +} + +// @todo change this to return the EIP712 hash instead +func getIdFromOrder(order Order) common.Hash { + return crypto.Keccak256Hash([]byte(order.Trader.String() + order.Salt.String())) +} + +// @todo change this to return the EIP712 hash instead +func getIdFromLimitOrder(order LimitOrder) common.Hash { + return crypto.Keccak256Hash([]byte(order.Trader.String() + order.Salt.String())) +} diff --git a/plugin/evm/orderbook/errors.go b/plugin/evm/orderbook/errors.go new file mode 100644 index 0000000000..6047f1bedb --- /dev/null +++ b/plugin/evm/orderbook/errors.go @@ -0,0 +1,11 @@ +package orderbook + +const ( + HandleChainAcceptedEventPanicMessage = "panic while processing chainAcceptedEvent" + HandleChainAcceptedLogsPanicMessage = "panic while processing chainAcceptedLogs" + HandleHubbleFeedLogsPanicMessage = "panic while processing hubbleFeedLogs" + RunMatchingPipelinePanicMessage = "panic while running matching pipeline" + RunSanitaryPipelinePanicMessage = "panic while running sanitary pipeline" + MakerBookFileWriteChannelPanicMessage = "panic while sending to makerbook file write channel" + SaveSnapshotPanicMessage = "panic while saving snapshot" +) diff --git a/plugin/evm/orderbook/hubbleutils/config.go b/plugin/evm/orderbook/hubbleutils/config.go new file mode 100644 index 0000000000..d27f9e0c84 --- /dev/null +++ b/plugin/evm/orderbook/hubbleutils/config.go @@ -0,0 +1,11 @@ +package hubbleutils + +var ( + ChainId int64 + VerifyingContract string +) + +func SetChainIdAndVerifyingSignedOrdersContract(chainId int64, verifyingContract string) { + ChainId = chainId + VerifyingContract = verifyingContract +} diff --git a/plugin/evm/orderbook/hubbleutils/data_structures.go b/plugin/evm/orderbook/hubbleutils/data_structures.go new file mode 100644 index 0000000000..0b441b842c --- /dev/null +++ b/plugin/evm/orderbook/hubbleutils/data_structures.go @@ -0,0 +1,297 @@ +package hubbleutils + +import ( + "encoding/json" + "fmt" + "math/big" + + "github.com/ava-labs/subnet-evm/accounts/abi" + "github.com/ava-labs/subnet-evm/utils" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" +) + +type MarginMode = uint8 + +const ( + Maintenance_Margin MarginMode = iota + Min_Allowable_Margin +) + +type Collateral struct { + Price *big.Int // scaled by 1e6 + Weight *big.Int // scaled by 1e6 + Decimals uint8 +} + +type Market = int + +type Position struct { + OpenNotional *big.Int `json:"open_notional"` + Size *big.Int `json:"size"` +} + +type Trader struct { + Positions map[Market]*Position `json:"positions"` // position for every market + Margin Margin `json:"margin"` // available margin/balance for every market +} + +type Margin struct { + Reserved *big.Int `json:"reserved"` + Deposited map[Collateral]*big.Int `json:"deposited"` +} + +type Side uint8 + +const ( + Long Side = iota + Short + Liquidation +) + +type OrderStatus uint8 + +// has to be exact same as IOrderHandler +const ( + Invalid OrderStatus = iota + Placed + Filled + Cancelled +) + +type OrderType uint8 + +const ( + Limit OrderType = iota + IOC + Signed +) + +func (o OrderType) String() string { + return [...]string{"limit", "ioc", "signed"}[o] +} + +type BaseOrder struct { + AmmIndex *big.Int `json:"ammIndex"` + Trader common.Address `json:"trader"` + BaseAssetQuantity *big.Int `json:"baseAssetQuantity"` + Price *big.Int `json:"price"` + Salt *big.Int `json:"salt"` + ReduceOnly bool `json:"reduceOnly"` +} + +// LimitOrder type is copy of Order struct defined in LimitOrderbook contract +type LimitOrder struct { + BaseOrder + PostOnly bool `json:"postOnly"` +} + +// IOCOrder type is copy of IOCOrder struct defined in Orderbook contract +type IOCOrder struct { + BaseOrder + OrderType uint8 `json:"orderType"` + ExpireAt *big.Int `json:"expireAt"` +} + +// LimitOrder +func (order *LimitOrder) EncodeToABIWithoutType() ([]byte, error) { + limitOrderType, err := getOrderType("limit") + if err != nil { + return nil, err + } + encodedLimitOrder, err := abi.Arguments{{Type: limitOrderType}}.Pack(order) + if err != nil { + return nil, err + } + return encodedLimitOrder, nil +} + +func (order *LimitOrder) EncodeToABI() ([]byte, error) { + encodedLimitOrder, err := order.EncodeToABIWithoutType() + if err != nil { + return nil, fmt.Errorf("limit order packing failed: %w", err) + } + orderType, _ := abi.NewType("uint8", "uint8", nil) + orderBytesType, _ := abi.NewType("bytes", "bytes", nil) + // 0 means ordertype = limit order + encodedOrder, err := abi.Arguments{{Type: orderType}, {Type: orderBytesType}}.Pack(uint8(0) /* Limit Order */, encodedLimitOrder) + if err != nil { + return nil, fmt.Errorf("order encoding failed: %w", err) + } + return encodedOrder, nil +} + +func (order *LimitOrder) DecodeFromRawOrder(rawOrder interface{}) { + marshalledOrder, _ := json.Marshal(rawOrder) + json.Unmarshal(marshalledOrder, &order) +} + +func (order *LimitOrder) Map() map[string]interface{} { + return map[string]interface{}{ + "ammIndex": order.AmmIndex, + "trader": order.Trader, + "baseAssetQuantity": utils.BigIntToFloat(order.BaseAssetQuantity, 18), + "price": utils.BigIntToFloat(order.Price, 6), + "reduceOnly": order.ReduceOnly, + "postOnly": order.PostOnly, + "salt": order.Salt, + } +} + +func DecodeLimitOrder(encodedOrder []byte) (*LimitOrder, error) { + limitOrderType, err := getOrderType("limit") + if err != nil { + return nil, fmt.Errorf("failed getting abi type: %w", err) + } + order, err := abi.Arguments{{Type: limitOrderType}}.Unpack(encodedOrder) + if err != nil { + return nil, err + } + limitOrder := &LimitOrder{} + limitOrder.DecodeFromRawOrder(order[0]) + return limitOrder, nil +} + +func (order *LimitOrder) Hash() (common.Hash, error) { + data, err := order.EncodeToABIWithoutType() + if err != nil { + return common.Hash{}, err + } + return common.BytesToHash(crypto.Keccak256(data)), nil +} + +func (o *LimitOrder) String() string { + return fmt.Sprintf("LimitOrder{AmmIndex: %v, Trader: %v, BaseAssetQuantity: %v, Price: %v, Salt: %v, ReduceOnly: %v, PostOnly: %v}", + o.AmmIndex, o.Trader, o.BaseAssetQuantity, o.Price, o.Salt, o.ReduceOnly, o.PostOnly) +} + +// ---------------------------------------------------------------------------- +// IOCOrder + +func (order *IOCOrder) EncodeToABIWithoutType() ([]byte, error) { + iocOrderType, err := getOrderType("ioc") + if err != nil { + return nil, err + } + encodedOrder, err := abi.Arguments{{Type: iocOrderType}}.Pack(order) + if err != nil { + return nil, err + } + return encodedOrder, nil +} + +func (order *IOCOrder) EncodeToABI() ([]byte, error) { + encodedIOCOrder, err := order.EncodeToABIWithoutType() + if err != nil { + return nil, fmt.Errorf("limit order packing failed: %w", err) + } + + orderType, _ := abi.NewType("uint8", "uint8", nil) + orderBytesType, _ := abi.NewType("bytes", "bytes", nil) + // 1 means ordertype = IOC/market order + encodedOrder, err := abi.Arguments{{Type: orderType}, {Type: orderBytesType}}.Pack(uint8(IOC), encodedIOCOrder) + if err != nil { + return nil, fmt.Errorf("order encoding failed: %w", err) + } + return encodedOrder, nil +} + +func (order *IOCOrder) DecodeFromRawOrder(rawOrder interface{}) { + marshalledOrder, _ := json.Marshal(rawOrder) + json.Unmarshal(marshalledOrder, &order) +} + +func (order *IOCOrder) Map() map[string]interface{} { + return map[string]interface{}{ + "ammIndex": order.AmmIndex, + "trader": order.Trader, + "baseAssetQuantity": utils.BigIntToFloat(order.BaseAssetQuantity, 18), + "price": utils.BigIntToFloat(order.Price, 6), + "reduceOnly": order.ReduceOnly, + "salt": order.Salt, + "orderType": order.OrderType, + "expireAt": order.ExpireAt, + } +} + +func DecodeIOCOrder(encodedOrder []byte) (*IOCOrder, error) { + iocOrderType, err := getOrderType("ioc") + if err != nil { + return nil, fmt.Errorf("failed getting abi type: %w", err) + } + order, err := abi.Arguments{{Type: iocOrderType}}.Unpack(encodedOrder) + if err != nil { + return nil, err + } + iocOrder := &IOCOrder{} + iocOrder.DecodeFromRawOrder(order[0]) + return iocOrder, nil +} + +func (order *IOCOrder) Hash() (hash common.Hash, err error) { + data, err := order.EncodeToABIWithoutType() + if err != nil { + return common.Hash{}, err + } + return common.BytesToHash(crypto.Keccak256(data)), nil +} + +// ---------------------------------------------------------------------------- +// Helper functions +type DecodeStep struct { + OrderType OrderType + EncodedOrder []byte +} + +func DecodeTypeAndEncodedOrder(data []byte) (*DecodeStep, error) { + orderType, _ := abi.NewType("uint8", "uint8", nil) + orderBytesType, _ := abi.NewType("bytes", "bytes", nil) + decodedValues, err := abi.Arguments{{Type: orderType}, {Type: orderBytesType}}.Unpack(data) + if err != nil { + return nil, err + } + return &DecodeStep{ + OrderType: OrderType(decodedValues[0].(uint8)), + EncodedOrder: decodedValues[1].([]byte), + }, nil +} + +func getOrderType(orderType string) (abi.Type, error) { + if orderType == "limit" { + return abi.NewType("tuple", "", []abi.ArgumentMarshaling{ + {Name: "ammIndex", Type: "uint256"}, + {Name: "trader", Type: "address"}, + {Name: "baseAssetQuantity", Type: "int256"}, + {Name: "price", Type: "uint256"}, + {Name: "salt", Type: "uint256"}, + {Name: "reduceOnly", Type: "bool"}, + {Name: "postOnly", Type: "bool"}, + }) + } + if orderType == "ioc" { + return abi.NewType("tuple", "", []abi.ArgumentMarshaling{ + {Name: "orderType", Type: "uint8"}, + {Name: "expireAt", Type: "uint256"}, + {Name: "ammIndex", Type: "uint256"}, + {Name: "trader", Type: "address"}, + {Name: "baseAssetQuantity", Type: "int256"}, + {Name: "price", Type: "uint256"}, + {Name: "salt", Type: "uint256"}, + {Name: "reduceOnly", Type: "bool"}, + }) + } + if orderType == "signed" { + return abi.NewType("tuple", "", []abi.ArgumentMarshaling{ + {Name: "orderType", Type: "uint8"}, + {Name: "expireAt", Type: "uint256"}, + {Name: "ammIndex", Type: "uint256"}, + {Name: "trader", Type: "address"}, + {Name: "baseAssetQuantity", Type: "int256"}, + {Name: "price", Type: "uint256"}, + {Name: "salt", Type: "uint256"}, + {Name: "reduceOnly", Type: "bool"}, + {Name: "postOnly", Type: "bool"}, + }) + } + return abi.Type{}, fmt.Errorf("invalid order type") +} diff --git a/plugin/evm/orderbook/hubbleutils/eip712.go b/plugin/evm/orderbook/hubbleutils/eip712.go new file mode 100644 index 0000000000..b444484375 --- /dev/null +++ b/plugin/evm/orderbook/hubbleutils/eip712.go @@ -0,0 +1,83 @@ +package hubbleutils + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/signer/core/apitypes" +) + +// EncodeForSigning - Encoding the typed data +func EncodeForSigning(typedData apitypes.TypedData) (hash common.Hash, err error) { + domainSeparator, err := typedData.HashStruct("EIP712Domain", typedData.Domain.Map()) + if err != nil { + return + } + typedDataHash, err := typedData.HashStruct(typedData.PrimaryType, typedData.Message) + if err != nil { + return + } + rawData := []byte(fmt.Sprintf("\x19\x01%s%s", string(domainSeparator), string(typedDataHash))) + hash = common.BytesToHash(crypto.Keccak256(rawData)) + return +} + +var Eip712OrderTypes = apitypes.Types{ + "EIP712Domain": { + { + Name: "name", + Type: "string", + }, + { + Name: "version", + Type: "string", + }, + { + Name: "chainId", + Type: "uint256", + }, + { + Name: "verifyingContract", + Type: "address", + }, + }, + "Order": { // has to be same as the struct name or whatever was passed when building the typed hash + { + Name: "orderType", + Type: "uint8", + }, + { + Name: "expireAt", + Type: "uint256", + }, + { + Name: "ammIndex", + Type: "uint256", + }, + { + Name: "trader", + Type: "address", + }, + { + Name: "baseAssetQuantity", + Type: "int256", + }, + { + Name: "price", + Type: "uint256", + }, + { + Name: "salt", + Type: "uint256", + }, + { + Name: "reduceOnly", + Type: "bool", + }, + { + Name: "postOnly", + Type: "bool", + }, + }, +} diff --git a/plugin/evm/orderbook/hubbleutils/hubble_math.go b/plugin/evm/orderbook/hubbleutils/hubble_math.go new file mode 100644 index 0000000000..6372ca3b14 --- /dev/null +++ b/plugin/evm/orderbook/hubbleutils/hubble_math.go @@ -0,0 +1,96 @@ +package hubbleutils + +import ( + "fmt" + "math/big" + + // "github.com/ava-labs/subnet-evm/accounts" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/crypto" +) + +var ( + ONE_E_6 = big.NewInt(1e6) + ONE_E_12 = big.NewInt(1e12) + ONE_E_18 = big.NewInt(1e18) +) + +func Add1e6(a *big.Int) *big.Int { + return Add(a, ONE_E_6) +} + +func Mul1e6(a *big.Int) *big.Int { + return Mul(a, ONE_E_6) +} + +func Div1e6(a *big.Int) *big.Int { + return Div(a, ONE_E_6) +} + +func Mul1e18(a *big.Int) *big.Int { + return Mul(a, ONE_E_18) +} + +func Div1e18(a *big.Int) *big.Int { + return Div(a, ONE_E_18) +} + +func Add(a, b *big.Int) *big.Int { + return new(big.Int).Add(a, b) +} + +func Sub(a, b *big.Int) *big.Int { + return new(big.Int).Sub(a, b) +} + +func Mul(a, b *big.Int) *big.Int { + return new(big.Int).Mul(a, b) +} + +func Div(a, b *big.Int) *big.Int { + return new(big.Int).Div(a, b) +} + +func Abs(a *big.Int) *big.Int { + return new(big.Int).Abs(a) +} + +func RoundOff(a, b *big.Int) *big.Int { + return Mul(Div(a, b), b) +} + +func Mod(a, b *big.Int) *big.Int { + return new(big.Int).Mod(a, b) +} + +func Neg(a *big.Int) *big.Int { + return new(big.Int).Neg(a) +} + +func Scale(a *big.Int, decimals uint8) *big.Int { + return Mul(a, new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(decimals)), nil)) +} + +func Unscale(a *big.Int, decimals uint8) *big.Int { + return Div(a, new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(decimals)), nil)) +} + +func ECRecover(data, sign hexutil.Bytes) (common.Address, error) { + sig := make([]byte, len(sign)) + copy(sig, sign) + + if len(sig) != crypto.SignatureLength { + return common.Address{}, fmt.Errorf("signature must be %d bytes long", crypto.SignatureLength) + } + if sig[crypto.RecoveryIDOffset] != 27 && sig[crypto.RecoveryIDOffset] != 28 { + return common.Address{}, fmt.Errorf("invalid Ethereum signature (V is not 27 or 28)") + } + sig[crypto.RecoveryIDOffset] -= 27 // Transform yellow paper V from 27/28 to 0/1 + + rpk, err := crypto.Ecrecover(data, sig) + if err != nil { + return common.Address{}, err + } + return common.BytesToAddress(common.LeftPadBytes(crypto.Keccak256(rpk[1:])[12:], 32)), nil +} diff --git a/plugin/evm/orderbook/hubbleutils/hubble_math_test.go b/plugin/evm/orderbook/hubbleutils/hubble_math_test.go new file mode 100644 index 0000000000..14c8cb9d59 --- /dev/null +++ b/plugin/evm/orderbook/hubbleutils/hubble_math_test.go @@ -0,0 +1,16 @@ +package hubbleutils + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/assert" +) + +func TestECRecovers(t *testing.T) { + // 1. Test case from + orderHash := "0xee4b26ae386d1c88f89eb2f8b4b4205271576742f5ff4e0488633612f7a9a5e7" + address, err := ECRecover(common.FromHex(orderHash), common.FromHex("0xb2704b73b99f2700ecc90a218f514c254d1f5d46af47117f5317f6cc0348ce962dcfb024c7264fdeb1f1513e4564c2a7cd9c1d0be33d7b934cd5a73b96440eaf1c")) + assert.Nil(t, err) + assert.Equal(t, "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", address.String()) +} diff --git a/plugin/evm/orderbook/hubbleutils/margin_math.go b/plugin/evm/orderbook/hubbleutils/margin_math.go new file mode 100644 index 0000000000..0694528625 --- /dev/null +++ b/plugin/evm/orderbook/hubbleutils/margin_math.go @@ -0,0 +1,177 @@ +package hubbleutils + +import ( + "math" + "math/big" +) + +type UpgradeVersion uint8 + +const ( + V0 UpgradeVersion = iota + V1 + V2 +) + +const V1ActivationTime = uint64(1697129100) // Thursday, 12 October 2023 16:45:00 GMT +type HubbleState struct { + Assets []Collateral + OraclePrices map[Market]*big.Int + MidPrices map[Market]*big.Int + SettlementPrices map[Market]*big.Int + ActiveMarkets []Market + MinAllowableMargin *big.Int + MaintenanceMargin *big.Int + TakerFee *big.Int + UpgradeVersion UpgradeVersion +} + +type UserState struct { + Positions map[Market]*Position + ReduceOnlyAmounts []*big.Int + Margins []*big.Int + PendingFunding *big.Int + ReservedMargin *big.Int +} + +func UpgradeVersionV0orV1(blockTimestamp uint64) UpgradeVersion { + if blockTimestamp >= V1ActivationTime { + return V1 + } + return V0 +} + +func GetAvailableMargin(hState *HubbleState, userState *UserState) *big.Int { + notionalPosition, margin := GetNotionalPositionAndMargin(hState, userState, Min_Allowable_Margin) + return GetAvailableMargin_(notionalPosition, margin, userState.ReservedMargin, hState.MinAllowableMargin) +} + +func GetAvailableMargin_(notionalPosition, margin, reservedMargin, minAllowableMargin *big.Int) *big.Int { + utilisedMargin := Div1e6(Mul(notionalPosition, minAllowableMargin)) + return Sub(Sub(margin, utilisedMargin), reservedMargin) +} + +func GetMarginFraction(hState *HubbleState, userState *UserState) *big.Int { + notionalPosition, margin := GetNotionalPositionAndMargin(hState, userState, Maintenance_Margin) + if notionalPosition.Sign() == 0 { + return big.NewInt(math.MaxInt64) + } + return Div(Mul1e6(margin), notionalPosition) +} + +func GetNotionalPositionAndMargin(hState *HubbleState, userState *UserState, marginMode MarginMode) (*big.Int, *big.Int) { + margin := Sub(GetNormalizedMargin(hState.Assets, userState.Margins), userState.PendingFunding) + notionalPosition, unrealizedPnl := GetTotalNotionalPositionAndUnrealizedPnl(hState, userState, margin, marginMode) + return notionalPosition, Add(margin, unrealizedPnl) +} + +// SUNSET: `hState.ActiveMarkets` contains active markets and ` hState.SettlementPrices` contains settlement prices +func GetTotalNotionalPositionAndUnrealizedPnl(hState *HubbleState, userState *UserState, margin *big.Int, marginMode MarginMode) (*big.Int, *big.Int) { + notionalPosition := big.NewInt(0) + unrealizedPnl := big.NewInt(0) + + for _, market := range hState.ActiveMarkets { + _notionalPosition, _unrealizedPnl := getOptimalPnl(hState, userState.Positions[market], margin, market, marginMode) + notionalPosition.Add(notionalPosition, _notionalPosition) + unrealizedPnl.Add(unrealizedPnl, _unrealizedPnl) + } + return notionalPosition, unrealizedPnl +} + +func getOptimalPnl(hState *HubbleState, position *Position, margin *big.Int, market Market, marginMode MarginMode) (notionalPosition *big.Int, uPnL *big.Int) { + if position == nil || position.Size.Sign() == 0 { + return big.NewInt(0), big.NewInt(0) + } + + price := hState.OraclePrices[market] + if hState.SettlementPrices[market] != nil && hState.SettlementPrices[market].Sign() != 0 { + price = hState.SettlementPrices[market] + } + + // based on oracle price + oracleBasedNotional, oracleBasedUnrealizedPnl, oracleBasedMF := GetPositionMetadata( + price, + position.OpenNotional, + position.Size, + margin, + ) + + // convert to uint8 so that it auto-applies to future version upgrades that may touch unrelated parts of the code + if uint8(hState.UpgradeVersion) >= uint8(V2) { + return oracleBasedNotional, oracleBasedUnrealizedPnl + } + + // based on last price + notionalPosition, unrealizedPnl, midPriceBasedMF := GetPositionMetadata( + hState.MidPrices[market], + position.OpenNotional, + position.Size, + margin, + ) + + if hState.UpgradeVersion == V1 { + if (marginMode == Maintenance_Margin && oracleBasedUnrealizedPnl.Cmp(unrealizedPnl) == 1) || // for liquidations + (marginMode == Min_Allowable_Margin && oracleBasedUnrealizedPnl.Cmp(unrealizedPnl) == -1) { // for increasing leverage + return oracleBasedNotional, oracleBasedUnrealizedPnl + } + return notionalPosition, unrealizedPnl + } + + // use V0 logic + if (marginMode == Maintenance_Margin && oracleBasedMF.Cmp(midPriceBasedMF) == 1) || // for liquidations + (marginMode == Min_Allowable_Margin && oracleBasedMF.Cmp(midPriceBasedMF) == -1) { // for increasing leverage + return oracleBasedNotional, oracleBasedUnrealizedPnl + } + return notionalPosition, unrealizedPnl +} + +func GetPositionMetadata(price *big.Int, openNotional *big.Int, size *big.Int, margin *big.Int) (notionalPosition *big.Int, unrealisedPnl *big.Int, marginFraction *big.Int) { + notionalPosition = GetNotionalPosition(price, size) + uPnL := new(big.Int) + if notionalPosition.Sign() == 0 { + return big.NewInt(0), big.NewInt(0), big.NewInt(0) + } + if size.Sign() > 0 { + uPnL = Sub(notionalPosition, openNotional) + } else { + uPnL = Sub(openNotional, notionalPosition) + } + mf := Div(Mul1e6(Add(margin, uPnL)), notionalPosition) + return notionalPosition, uPnL, mf +} + +func GetNotionalPosition(price *big.Int, size *big.Int) *big.Int { + return big.NewInt(0).Abs(Div1e18(Mul(price, size))) +} + +func GetNormalizedMargin(assets []Collateral, margins []*big.Int) *big.Int { + weighted, _ := WeightedAndSpotCollateral(assets, margins) + return weighted +} + +func WeightedAndSpotCollateral(assets []Collateral, margins []*big.Int) (weighted, spot *big.Int) { + weighted = big.NewInt(0) + spot = big.NewInt(0) + for i, asset := range assets { + if margins[i] == nil || margins[i].Sign() == 0 { + continue + } + numerator := Mul(margins[i], asset.Price) // margin[i] is scaled by asset.Decimal + spot.Add(spot, Unscale(numerator, asset.Decimals)) + weighted.Add(weighted, Unscale(Mul(numerator, asset.Weight), asset.Decimals+6)) + } + return weighted, spot +} + +func GetRequiredMargin(price, fillAmount, minAllowableMargin, takerFee *big.Int) *big.Int { + quoteAsset := Div1e18(Mul(fillAmount, price)) + return Add(Div1e6(Mul(quoteAsset, minAllowableMargin)), Div1e6(Mul(quoteAsset, takerFee))) +} + +func ArrayToMap(prices []*big.Int) map[Market]*big.Int { + underlyingPrices := make(map[Market]*big.Int) + for market, price := range prices { + underlyingPrices[Market(market)] = price + } + return underlyingPrices +} diff --git a/plugin/evm/orderbook/hubbleutils/margin_math_test.go b/plugin/evm/orderbook/hubbleutils/margin_math_test.go new file mode 100644 index 0000000000..cbbe9471a9 --- /dev/null +++ b/plugin/evm/orderbook/hubbleutils/margin_math_test.go @@ -0,0 +1,240 @@ +package hubbleutils + +import ( + "fmt" + "math/big" + "testing" + + "github.com/stretchr/testify/assert" +) + +var _hState = &HubbleState{ + Assets: []Collateral{ + { + Price: big.NewInt(1.01 * 1e6), // 1.01 + Weight: big.NewInt(1e6), // 1 + Decimals: 6, + }, + { + Price: big.NewInt(54.36 * 1e6), // 54.36 + Weight: big.NewInt(0.7 * 1e6), // 0.7 + Decimals: 6, + }, + }, + MidPrices: map[Market]*big.Int{ + 0: big.NewInt(1544.21 * 1e6), // 1544.21 + 1: big.NewInt(19.5 * 1e6), // 19.5 + }, + OraclePrices: map[Market]*big.Int{ + 0: big.NewInt(1503.21 * 1e6), + 1: big.NewInt(17.5 * 1e6), + }, + ActiveMarkets: []Market{ + 0, 1, + }, + MinAllowableMargin: big.NewInt(100000), // 0.1 + MaintenanceMargin: big.NewInt(200000), // 0.2 + UpgradeVersion: V1, +} + +var userState = &UserState{ + Positions: map[Market]*Position{ + 0: { + Size: big.NewInt(0.582 * 1e18), // 0.0582 + OpenNotional: big.NewInt(875 * 1e6), // 87.5, openPrice = 1503.43 + }, + 1: { + Size: Scale(big.NewInt(-101), 18), // -101 + OpenNotional: big.NewInt(1767.5 * 1e6), // 1767.5, openPrice = 17.5 + }, + }, + Margins: []*big.Int{ + big.NewInt(30.5 * 1e6), // 30.5 + big.NewInt(14 * 1e6), // 14 + }, + PendingFunding: big.NewInt(0), + ReservedMargin: big.NewInt(0), +} + +func TestWeightedAndSpotCollateral(t *testing.T) { + assets := _hState.Assets + margins := userState.Margins + expectedWeighted := Unscale(Mul(Mul(margins[0], assets[0].Price), assets[0].Weight), assets[0].Decimals+6) + expectedWeighted.Add(expectedWeighted, Unscale(Mul(Mul(margins[1], assets[1].Price), assets[1].Weight), assets[1].Decimals+6)) + + expectedSpot := Unscale(Mul(margins[0], assets[0].Price), assets[0].Decimals) + expectedSpot.Add(expectedSpot, Unscale(Mul(margins[1], assets[1].Price), assets[1].Decimals)) + + resultWeighted, resultSpot := WeightedAndSpotCollateral(assets, margins) + fmt.Println(resultWeighted, resultSpot) + assert.Equal(t, expectedWeighted, resultWeighted) + assert.Equal(t, expectedSpot, resultSpot) + + normalisedMargin := GetNormalizedMargin(assets, margins) + assert.Equal(t, expectedWeighted, normalisedMargin) + +} + +func TestGetNotionalPosition(t *testing.T) { + price := Scale(big.NewInt(1200), 6) + size := Scale(big.NewInt(5), 18) + expected := Scale(big.NewInt(6000), 6) + + result := GetNotionalPosition(price, size) + + assert.Equal(t, expected, result) +} + +func TestGetPositionMetadata(t *testing.T) { + price := big.NewInt(20250000) // 20.25 + openNotional := big.NewInt(75369000) // 75.369 (size * 18.5) + size := Scale(big.NewInt(40740), 14) // 4.074 + margin := big.NewInt(20000000) // 20 + + notionalPosition, unrealisedPnl, marginFraction := GetPositionMetadata(price, openNotional, size, margin) + + expectedNotionalPosition := big.NewInt(82498500) // 82.4985 + expectedUnrealisedPnl := big.NewInt(7129500) // 7.1295 + expectedMarginFraction := big.NewInt(328848) // 0.328848 + + assert.Equal(t, expectedNotionalPosition, notionalPosition) + assert.Equal(t, expectedUnrealisedPnl, unrealisedPnl) + assert.Equal(t, expectedMarginFraction, marginFraction) + + // ------ when size is negative ------ + size = Scale(big.NewInt(-40740), 14) // -4.074 + openNotional = big.NewInt(75369000) // 75.369 (size * 18.5) + notionalPosition, unrealisedPnl, marginFraction = GetPositionMetadata(price, openNotional, size, margin) + fmt.Println("notionalPosition", notionalPosition, "unrealisedPnl", unrealisedPnl, "marginFraction", marginFraction) + + expectedNotionalPosition = big.NewInt(82498500) // 82.4985 + expectedUnrealisedPnl = big.NewInt(-7129500) // -7.1295 + expectedMarginFraction = big.NewInt(156008) // 0.156008 + + assert.Equal(t, expectedNotionalPosition, notionalPosition) + assert.Equal(t, expectedUnrealisedPnl, unrealisedPnl) + assert.Equal(t, expectedMarginFraction, marginFraction) +} + +func TestGetOptimalPnlV2(t *testing.T) { + margin := big.NewInt(20 * 1e6) // 20 + market := 0 + position := userState.Positions[market] + marginMode := Maintenance_Margin + + notionalPosition, uPnL := getOptimalPnl(_hState, position, margin, market, marginMode) + + // mid price pnl is more than oracle price pnl + expectedNotionalPosition := Unscale(Mul(position.Size, _hState.MidPrices[market]), 18) + expectedUPnL := Sub(expectedNotionalPosition, position.OpenNotional) + fmt.Println("Maintenace_Margin_Mode", "notionalPosition", notionalPosition, "uPnL", uPnL) + + assert.Equal(t, expectedNotionalPosition, notionalPosition) + assert.Equal(t, expectedUPnL, uPnL) + + // ------ when marginMode is Min_Allowable_Margin ------ + + marginMode = Min_Allowable_Margin + notionalPosition, uPnL = getOptimalPnl(_hState, position, margin, market, marginMode) + + expectedNotionalPosition = Unscale(Mul(position.Size, _hState.OraclePrices[market]), 18) + expectedUPnL = Sub(expectedNotionalPosition, position.OpenNotional) + fmt.Println("Min_Allowable_Margin_Mode", "notionalPosition", notionalPosition, "uPnL", uPnL) + + assert.Equal(t, expectedNotionalPosition, notionalPosition) + assert.Equal(t, expectedUPnL, uPnL) +} + +func TestGetOptimalPnlV1(t *testing.T) { + margin := big.NewInt(20 * 1e6) // 20 + market := 0 + position := userState.Positions[market] + marginMode := Maintenance_Margin + + notionalPosition, uPnL := getOptimalPnl(_hState, position, margin, market, marginMode) + + // mid price pnl is more than oracle price pnl + expectedNotionalPosition := Unscale(Mul(position.Size, _hState.MidPrices[market]), 18) + expectedUPnL := Sub(expectedNotionalPosition, position.OpenNotional) + fmt.Println("Maintenace_Margin_Mode", "notionalPosition", notionalPosition, "uPnL", uPnL) + + assert.Equal(t, expectedNotionalPosition, notionalPosition) + assert.Equal(t, expectedUPnL, uPnL) + + // ------ when marginMode is Min_Allowable_Margin ------ + + marginMode = Min_Allowable_Margin + notionalPosition, uPnL = getOptimalPnl(_hState, position, margin, market, marginMode) + + expectedNotionalPosition = Unscale(Mul(position.Size, _hState.OraclePrices[market]), 18) + expectedUPnL = Sub(expectedNotionalPosition, position.OpenNotional) + fmt.Println("Min_Allowable_Margin_Mode", "notionalPosition", notionalPosition, "uPnL", uPnL) + + assert.Equal(t, expectedNotionalPosition, notionalPosition) + assert.Equal(t, expectedUPnL, uPnL) +} + +func TestGetTotalNotionalPositionAndUnrealizedPnlV2(t *testing.T) { + margin := GetNormalizedMargin(_hState.Assets, userState.Margins) + marginMode := Maintenance_Margin + notionalPosition, uPnL := GetTotalNotionalPositionAndUnrealizedPnl(_hState, userState, margin, marginMode) + + // mid price pnl is more than oracle price pnl for long position + expectedNotionalPosition := Unscale(Mul(userState.Positions[0].Size, _hState.MidPrices[0]), 18) + expectedUPnL := Sub(expectedNotionalPosition, userState.Positions[0].OpenNotional) + // oracle price pnl is more than mid price pnl for short position + expectedNotional2 := Abs(Unscale(Mul(userState.Positions[1].Size, _hState.OraclePrices[1]), 18)) + expectedNotionalPosition.Add(expectedNotionalPosition, expectedNotional2) + expectedUPnL.Add(expectedUPnL, Sub(userState.Positions[1].OpenNotional, expectedNotional2)) + + assert.Equal(t, expectedNotionalPosition, notionalPosition) + assert.Equal(t, expectedUPnL, uPnL) + + // ------ when marginMode is Min_Allowable_Margin ------ + + marginMode = Min_Allowable_Margin + notionalPosition, uPnL = GetTotalNotionalPositionAndUnrealizedPnl(_hState, userState, margin, marginMode) + fmt.Println("Min_Allowable_Margin_Mode ", "notionalPosition = ", notionalPosition, "uPnL = ", uPnL) + + expectedNotionalPosition = Unscale(Mul(userState.Positions[0].Size, _hState.OraclePrices[0]), 18) + expectedUPnL = Sub(expectedNotionalPosition, userState.Positions[0].OpenNotional) + expectedNotional2 = Abs(Unscale(Mul(userState.Positions[1].Size, _hState.MidPrices[1]), 18)) + expectedNotionalPosition.Add(expectedNotionalPosition, expectedNotional2) + expectedUPnL.Add(expectedUPnL, Sub(userState.Positions[1].OpenNotional, expectedNotional2)) + + assert.Equal(t, expectedNotionalPosition, notionalPosition) + assert.Equal(t, expectedUPnL, uPnL) +} + +func TestGetTotalNotionalPositionAndUnrealizedPnl(t *testing.T) { + margin := GetNormalizedMargin(_hState.Assets, userState.Margins) + marginMode := Maintenance_Margin + _hState.UpgradeVersion = V2 + notionalPosition, uPnL := GetTotalNotionalPositionAndUnrealizedPnl(_hState, userState, margin, marginMode) + + // mid price pnl is more than oracle price pnl for long position + expectedNotionalPosition := Unscale(Mul(userState.Positions[0].Size, _hState.OraclePrices[0]), 18) + expectedUPnL := Sub(expectedNotionalPosition, userState.Positions[0].OpenNotional) + // oracle price pnl is more than mid price pnl for short position + expectedNotional2 := Abs(Unscale(Mul(userState.Positions[1].Size, _hState.OraclePrices[1]), 18)) + expectedNotionalPosition.Add(expectedNotionalPosition, expectedNotional2) + expectedUPnL.Add(expectedUPnL, Sub(userState.Positions[1].OpenNotional, expectedNotional2)) + + assert.Equal(t, expectedNotionalPosition, notionalPosition) + assert.Equal(t, expectedUPnL, uPnL) + + // ------ when marginMode is Min_Allowable_Margin ------ + + marginMode = Min_Allowable_Margin + notionalPosition, uPnL = GetTotalNotionalPositionAndUnrealizedPnl(_hState, userState, margin, marginMode) + fmt.Println("Min_Allowable_Margin_Mode ", "notionalPosition = ", notionalPosition, "uPnL = ", uPnL) + + expectedNotionalPosition = Unscale(Mul(userState.Positions[0].Size, _hState.OraclePrices[0]), 18) + expectedUPnL = Sub(expectedNotionalPosition, userState.Positions[0].OpenNotional) + expectedNotional2 = Abs(Unscale(Mul(userState.Positions[1].Size, _hState.OraclePrices[1]), 18)) + expectedNotionalPosition.Add(expectedNotionalPosition, expectedNotional2) + expectedUPnL.Add(expectedUPnL, Sub(userState.Positions[1].OpenNotional, expectedNotional2)) + + assert.Equal(t, expectedNotionalPosition, notionalPosition) + assert.Equal(t, expectedUPnL, uPnL) +} diff --git a/plugin/evm/orderbook/hubbleutils/signed_orders.go b/plugin/evm/orderbook/hubbleutils/signed_orders.go new file mode 100644 index 0000000000..7f73acc631 --- /dev/null +++ b/plugin/evm/orderbook/hubbleutils/signed_orders.go @@ -0,0 +1,118 @@ +package hubbleutils + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "math/big" + "strconv" + + "github.com/ava-labs/subnet-evm/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/signer/core/apitypes" +) + +type SignedOrder struct { + LimitOrder + OrderType uint8 `json:"orderType"` + ExpireAt *big.Int `json:"expireAt"` + Sig []byte `json:"sig"` +} + +func (order *SignedOrder) EncodeToABIWithoutType() ([]byte, error) { + signedOrderType, err := getOrderType("signed") + if err != nil { + return nil, err + } + bytesTy, _ := abi.NewType("bytes", "bytes", nil) + encodedOrder, err := abi.Arguments{{Type: signedOrderType}, {Type: bytesTy}}.Pack(order, order.Sig) + if err != nil { + return nil, err + } + return encodedOrder, nil +} + +func (order *SignedOrder) EncodeToABI() ([]byte, error) { + encodedSignedOrder, err := order.EncodeToABIWithoutType() + if err != nil { + return nil, fmt.Errorf("failed getting abi type: %w", err) + } + + uint8Ty, _ := abi.NewType("uint8", "uint8", nil) + bytesTy, _ := abi.NewType("bytes", "bytes", nil) + + encodedOrder, err := abi.Arguments{{Type: uint8Ty}, {Type: bytesTy}}.Pack(uint8(Signed), encodedSignedOrder) + if err != nil { + return nil, fmt.Errorf("order encoding failed: %w", err) + } + + return encodedOrder, nil +} + +func DecodeSignedOrder(encodedOrder []byte) (*SignedOrder, error) { + signedOrderType, err := getOrderType("signed") + if err != nil { + return nil, fmt.Errorf("failed getting abi type: %w", err) + } + bytesTy, _ := abi.NewType("bytes", "bytes", nil) + decodedValues, err := abi.Arguments{{Type: signedOrderType}, {Type: bytesTy}}.Unpack(encodedOrder) + if err != nil { + return nil, err + } + signedOrder := &SignedOrder{ + Sig: decodedValues[1].([]byte), + } + signedOrder.DecodeFromRawOrder(decodedValues[0]) + return signedOrder, nil +} + +func (order *SignedOrder) DecodeFromRawOrder(rawOrder interface{}) { + marshalledOrder, _ := json.Marshal(rawOrder) + // fmt.Println("marshalledOrder", string(marshalledOrder)) + err := json.Unmarshal(marshalledOrder, &order) + if err != nil { + log.Error("err in DecodeFromRawOrder", "err", err, "rawOrder", rawOrder) + } +} + +func (o *SignedOrder) String() string { + return fmt.Sprintf( + "Order: %s, OrderType: %d, ExpireAt: %d, Sig: %s", + o.LimitOrder.String(), + o.OrderType, + o.ExpireAt, + hex.EncodeToString(o.Sig), + ) +} + +func (o *SignedOrder) Hash() (hash common.Hash, err error) { + if VerifyingContract == "" || ChainId == 0 { + return common.Hash{}, fmt.Errorf("ChainId or VerifyingContract not set") + } + message := map[string]interface{}{ + "orderType": strconv.FormatUint(uint64(o.OrderType), 10), + "expireAt": o.ExpireAt.String(), + "ammIndex": o.AmmIndex.String(), + "trader": o.Trader.String(), + "baseAssetQuantity": o.BaseAssetQuantity.String(), + "price": o.Price.String(), + "salt": o.Salt.String(), + "reduceOnly": o.ReduceOnly, + "postOnly": o.PostOnly, + } + domain := apitypes.TypedDataDomain{ + Name: "Hubble", + Version: "2.0", + ChainId: math.NewHexOrDecimal256(ChainId), + VerifyingContract: VerifyingContract, + } + typedData := apitypes.TypedData{ + Types: Eip712OrderTypes, + PrimaryType: "Order", + Domain: domain, + Message: message, + } + return EncodeForSigning(typedData) +} diff --git a/plugin/evm/orderbook/hubbleutils/signed_orders_test.go b/plugin/evm/orderbook/hubbleutils/signed_orders_test.go new file mode 100644 index 0000000000..51c76224b2 --- /dev/null +++ b/plugin/evm/orderbook/hubbleutils/signed_orders_test.go @@ -0,0 +1,150 @@ +package hubbleutils + +import ( + "encoding/hex" + "math/big" + "strings" + + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/assert" +) + +func TestDecodeSignedOrder(t *testing.T) { + t.Run("long order", func(t *testing.T) { + SetChainIdAndVerifyingSignedOrdersContract(321123, "0x4c5859f0F772848b2D91F1D83E2Fe57935348029") + orderHash := strings.TrimPrefix("0x73d5196ac9576efaccb6e54b193b894e2cc0afd68ce5af519c901fec7e588595", "0x") + signature := strings.TrimPrefix("0x3027ae4ab98663490d0facab04c71665e41da867a44b7ddc29e14cb8de3a3cfa12985be54945ce040196b2fcdcc4dafc56f7955ee72628bc9e7a634a7f258ce61c", "0x") + encodedOrder := strings.TrimPrefix("0x00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000064ac0426000000000000000000000000000000000000000000000000000000000000000000000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000000000000000000000000000004563918244f40000000000000000000000000000000000000000000000000000000000003b9aca00000000000000000000000000000000000000000000000000000001893fef795900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000413027ae4ab98663490d0facab04c71665e41da867a44b7ddc29e14cb8de3a3cfa12985be54945ce040196b2fcdcc4dafc56f7955ee72628bc9e7a634a7f258ce61c00000000000000000000000000000000000000000000000000000000000000", "0x") + typeEncodedOrder := strings.TrimPrefix("0x0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000064ac0426000000000000000000000000000000000000000000000000000000000000000000000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000000000000000000000000000004563918244f40000000000000000000000000000000000000000000000000000000000003b9aca00000000000000000000000000000000000000000000000000000001893fef795900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000413027ae4ab98663490d0facab04c71665e41da867a44b7ddc29e14cb8de3a3cfa12985be54945ce040196b2fcdcc4dafc56f7955ee72628bc9e7a634a7f258ce61c00000000000000000000000000000000000000000000000000000000000000", "0x") + + sig, err := hex.DecodeString(signature) + assert.Nil(t, err) + order := &SignedOrder{ + LimitOrder: LimitOrder{ + BaseOrder: BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8"), + BaseAssetQuantity: big.NewInt(5000000000000000000), + Price: big.NewInt(1000000000), + Salt: big.NewInt(1688994806105), + ReduceOnly: false, + }, + PostOnly: true, + }, + OrderType: 2, + ExpireAt: big.NewInt(1688994854), + Sig: sig, + } + h, err := order.Hash() + assert.Nil(t, err) + assert.Equal(t, orderHash, strings.TrimPrefix(h.Hex(), "0x")) + + b, err := order.EncodeToABIWithoutType() + assert.Nil(t, err) + assert.Equal(t, encodedOrder, hex.EncodeToString(b)) + + b, err = order.EncodeToABI() + assert.Nil(t, err) + assert.Equal(t, typeEncodedOrder, hex.EncodeToString(b)) + + testDecodeTypeAndEncodedSignedOrder(t, typeEncodedOrder, encodedOrder, Signed, order) + + data, err := hex.DecodeString(orderHash) + assert.Nil(t, err) + signer, err := ECRecover(data, sig) + assert.Nil(t, err) + assert.Equal(t, order.Trader, signer) + + sig_, _ := hex.DecodeString(signature) + assert.Equal(t, sig_, sig) // sig is not changed + assert.Equal(t, sig_, order.Sig) // sig is not changed + }) + + t.Run("short order", func(t *testing.T) { + SetChainIdAndVerifyingSignedOrdersContract(321123, "0x809d550fca64d94Bd9F66E60752A544199cfAC3D") + orderHash := strings.TrimPrefix("0xee4b26ae386d1c88f89eb2f8b4b4205271576742f5ff4e0488633612f7a9a5e7", "0x") + signature := strings.TrimPrefix("0xb2704b73b99f2700ecc90a218f514c254d1f5d46af47117f5317f6cc0348ce962dcfb024c7264fdeb1f1513e4564c2a7cd9c1d0be33d7b934cd5a73b96440eaf1c", "0x") + encodedOrder := strings.TrimPrefix("0x00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000064ac0426000000000000000000000000000000000000000000000000000000000000000000000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8ffffffffffffffffffffffffffffffffffffffffffffffffba9c6e7dbb0c0000000000000000000000000000000000000000000000000000000000003b9aca00000000000000000000000000000000000000000000000000000001893fef79590000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000041b2704b73b99f2700ecc90a218f514c254d1f5d46af47117f5317f6cc0348ce962dcfb024c7264fdeb1f1513e4564c2a7cd9c1d0be33d7b934cd5a73b96440eaf1c00000000000000000000000000000000000000000000000000000000000000", "0x") + typeEncodedOrder := strings.TrimPrefix("0x0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000064ac0426000000000000000000000000000000000000000000000000000000000000000000000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8ffffffffffffffffffffffffffffffffffffffffffffffffba9c6e7dbb0c0000000000000000000000000000000000000000000000000000000000003b9aca00000000000000000000000000000000000000000000000000000001893fef79590000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000041b2704b73b99f2700ecc90a218f514c254d1f5d46af47117f5317f6cc0348ce962dcfb024c7264fdeb1f1513e4564c2a7cd9c1d0be33d7b934cd5a73b96440eaf1c00000000000000000000000000000000000000000000000000000000000000", "0x") + + sig, err := hex.DecodeString(signature) + assert.Nil(t, err) + order := &SignedOrder{ + LimitOrder: LimitOrder{ + BaseOrder: BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8"), + BaseAssetQuantity: big.NewInt(-5000000000000000000), + Price: big.NewInt(1000000000), + Salt: big.NewInt(1688994806105), + ReduceOnly: false, + }, + PostOnly: true, + }, + OrderType: 2, + ExpireAt: big.NewInt(1688994854), + Sig: sig, + } + h, err := order.Hash() + assert.Nil(t, err) + assert.Equal(t, orderHash, strings.TrimPrefix(h.Hex(), "0x")) + + b, err := order.EncodeToABIWithoutType() + assert.Nil(t, err) + assert.Equal(t, encodedOrder, hex.EncodeToString(b)) + + b, err = order.EncodeToABI() + assert.Nil(t, err) + assert.Equal(t, typeEncodedOrder, hex.EncodeToString(b)) + + testDecodeTypeAndEncodedSignedOrder(t, typeEncodedOrder, encodedOrder, Signed, order) + + data, err := hex.DecodeString(orderHash) + assert.Nil(t, err) + signer, err := ECRecover(data, sig) + assert.Nil(t, err) + assert.Equal(t, order.Trader, signer) + + sig_, _ := hex.DecodeString(signature) + assert.Equal(t, sig_, sig) // sig is not changed + assert.Equal(t, sig_, order.Sig) // sig is not changed + }) +} + +func testDecodeTypeAndEncodedSignedOrder(t *testing.T, typedEncodedOrder string, encodedOrder string, orderType OrderType, expectedOutput *SignedOrder) { + testData, err := hex.DecodeString(typedEncodedOrder) + assert.Nil(t, err) + + decodeStep, err := DecodeTypeAndEncodedOrder(testData) + assert.Nil(t, err) + + assert.Equal(t, orderType, decodeStep.OrderType) + assert.Equal(t, encodedOrder, hex.EncodeToString(decodeStep.EncodedOrder)) + assert.Nil(t, err) + testDecodeSignedOrder(t, decodeStep.EncodedOrder, expectedOutput) +} + +func testDecodeSignedOrder(t *testing.T, encodedOrder []byte, expectedOutput *SignedOrder) { + result, err := DecodeSignedOrder(encodedOrder) + assert.NoError(t, err) + assert.NotNil(t, result) + assertSignedOrderEquality(t, expectedOutput, result) +} + +func assertSignedOrderEquality(t *testing.T, expected, actual *SignedOrder) { + assert.Equal(t, expected.OrderType, actual.OrderType) + assert.Equal(t, expected.ExpireAt.Int64(), actual.ExpireAt.Int64()) + assert.Equal(t, expected.Sig, actual.Sig) + assertLimitOrderEquality(t, expected.BaseOrder, actual.BaseOrder) +} + +func assertLimitOrderEquality(t *testing.T, expected, actual BaseOrder) { + assert.Equal(t, expected.AmmIndex.Int64(), actual.AmmIndex.Int64()) + assert.Equal(t, expected.Trader, actual.Trader) + assert.Equal(t, expected.BaseAssetQuantity, actual.BaseAssetQuantity) + assert.Equal(t, expected.Price, actual.Price) + assert.Equal(t, expected.Salt, actual.Salt) + assert.Equal(t, expected.ReduceOnly, actual.ReduceOnly) +} diff --git a/plugin/evm/orderbook/hubbleutils/validations.go b/plugin/evm/orderbook/hubbleutils/validations.go new file mode 100644 index 0000000000..8ec18c3a58 --- /dev/null +++ b/plugin/evm/orderbook/hubbleutils/validations.go @@ -0,0 +1,112 @@ +package hubbleutils + +import ( + "errors" + // "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" +) + +type SignedOrderValidationFields struct { + OrderHash common.Hash + Now uint64 + ActiveMarketsCount int64 + MinSize *big.Int + PriceMultiplier *big.Int + Status int64 +} + +var ( + ErrNotSignedOrder = errors.New("not signed order") + ErrInvalidPrice = errors.New("invalid price") + ErrOrderExpired = errors.New("order expired") + ErrBaseAssetQuantityZero = errors.New("baseAssetQuantity is zero") + ErrNotPostOnly = errors.New("not post only") + ErrInvalidMarket = errors.New("invalid market") + ErrNotMultiple = errors.New("not multiple") + ErrPricePrecision = errors.New("invalid price precision") + ErrOrderAlreadyExists = errors.New("order already exists") + ErrCrossingMarket = errors.New("crossing market") + ErrNoTradingAuthority = errors.New("no trading authority") + ErrInsufficientMargin = errors.New("insufficient margin") + ErrNoReferrer = errors.New("no referrer") +) + +// Common Checks +// 1. orderType == Signed +// 2. Not expired +// 3. order should be post only +// 4. baseAssetQuantity is not 0 and multiple of minSize +// 5. price > 0 and price precision check +// 6. signer is valid trading authority +// 7. market is valid +// 8. order is not already filled or cancelled + +// Place Order Checks +// P1. Order is not already in memdb (placed) +// P2. Margin is available for non-reduce only orders +// P3. Sum of all reduce only orders should not exceed the total position size (not in state, simply compared to other active orders) and/or opposite direction validations +// P4. Post only order shouldn't cross the market +// P5. HasReferrer + +// Matching Order Checks +// M1. order is not being overfilled +// M2. reduce only order should reduce the position size +// M3. HasReferrer +// M4. Not both post only orders are being matched + +func ValidateSignedOrder(order *SignedOrder, fields SignedOrderValidationFields) (trader, signer common.Address, err error) { + if OrderType(order.OrderType) != Signed { // 1. + err = ErrNotSignedOrder + return trader, signer, err + } + + if order.ExpireAt.Uint64() < fields.Now { // 2. + err = ErrOrderExpired + return trader, signer, err + } + + if !order.PostOnly { // 3. + err = ErrNotPostOnly + return trader, signer, err + } + + // 4. + if order.BaseAssetQuantity.Sign() == 0 { + err = ErrBaseAssetQuantityZero + return trader, signer, err + } + if new(big.Int).Mod(order.BaseAssetQuantity, fields.MinSize).Sign() != 0 { + err = ErrNotMultiple + return trader, signer, err + } + + if order.Price.Sign() != 1 { // 5. + err = ErrInvalidPrice + return trader, signer, err + } + if Mod(order.Price, fields.PriceMultiplier).Sign() != 0 { + err = ErrPricePrecision + return trader, signer, err + } + + signer, err = ECRecover(fields.OrderHash.Bytes(), order.Sig[:]) + // fmt.Println("signer", signer) + if err != nil { + return trader, signer, err + } + trader = order.Trader + + // assumes all markets are active and in sequential order + if order.AmmIndex.Int64() >= fields.ActiveMarketsCount { // 7. + err = ErrInvalidMarket + return trader, signer, err + } + + if OrderStatus(fields.Status) != Invalid { // 8. + err = ErrOrderAlreadyExists + return trader, signer, err + } + return trader, signer, nil +} diff --git a/plugin/evm/orderbook/liquidations.go b/plugin/evm/orderbook/liquidations.go new file mode 100644 index 0000000000..fe4a2bfb11 --- /dev/null +++ b/plugin/evm/orderbook/liquidations.go @@ -0,0 +1,112 @@ +package orderbook + +import ( + "math" + "math/big" + "sort" + + hu "github.com/ava-labs/subnet-evm/plugin/evm/orderbook/hubbleutils" + "github.com/ethereum/go-ethereum/common" +) + +type LiquidablePosition struct { + Address common.Address + Market Market + Size *big.Int + MarginFraction *big.Int + FilledSize *big.Int + PositionType PositionType +} + +func (liq LiquidablePosition) GetUnfilledSize() *big.Int { + return new(big.Int).Sub(liq.Size, liq.FilledSize) +} + +func calcMarginFraction(trader *Trader, hState *hu.HubbleState) *big.Int { + // SUNSET: this function is only used in unit tests and a test API; no need to change it + userState := &hu.UserState{ + Positions: translatePositions(trader.Positions), + Margins: getMargins(trader, len(hState.Assets)), + PendingFunding: getTotalFunding(trader, hState.ActiveMarkets), + ReservedMargin: new(big.Int).Set(trader.Margin.Reserved), + } + return hu.GetMarginFraction(hState, userState) +} + +func sortLiquidableSliceByMarginFraction(positions []LiquidablePosition) []LiquidablePosition { + sort.SliceStable(positions, func(i, j int) bool { + return positions[i].MarginFraction.Cmp(positions[j].MarginFraction) == -1 + }) + return positions +} + +func getNormalisedMargin(trader *Trader, assets []hu.Collateral) *big.Int { + return hu.GetNormalizedMargin(assets, getMargins(trader, len(assets))) +} + +func getMargins(trader *Trader, numAssets int) []*big.Int { + margin := make([]*big.Int, numAssets) + if trader.Margin.Deposited == nil { + return margin + } + numAssets_ := len(trader.Margin.Deposited) + if numAssets_ < numAssets { + numAssets = numAssets_ + } + for i := 0; i < numAssets; i++ { + margin[i] = trader.Margin.Deposited[Collateral(i)] + } + return margin +} + +func getTotalFunding(trader *Trader, markets []Market) *big.Int { + totalPendingFunding := big.NewInt(0) + for _, market := range markets { + if trader.Positions[market] != nil && trader.Positions[market].UnrealisedFunding != nil && trader.Positions[market].UnrealisedFunding.Sign() != 0 { + totalPendingFunding.Add(totalPendingFunding, trader.Positions[market].UnrealisedFunding) + } + } + return totalPendingFunding +} + +type MarginMode = hu.MarginMode + +func getTotalNotionalPositionAndUnrealizedPnl(trader *Trader, margin *big.Int, marginMode MarginMode, oraclePrices map[Market]*big.Int, midPrices map[Market]*big.Int, markets []Market) (*big.Int, *big.Int) { + return hu.GetTotalNotionalPositionAndUnrealizedPnl( + &hu.HubbleState{ + OraclePrices: oraclePrices, + MidPrices: midPrices, + ActiveMarkets: markets, + UpgradeVersion: hu.V2, + }, + &hu.UserState{ + Positions: translatePositions(trader.Positions), + }, + margin, + marginMode, + ) +} + +func getPositionMetadata(price *big.Int, openNotional *big.Int, size *big.Int, margin *big.Int) (notionalPosition *big.Int, unrealisedPnl *big.Int, marginFraction *big.Int) { + return hu.GetPositionMetadata(price, openNotional, size, margin) +} + +func prettifyScaledBigInt(number *big.Int, precision int8) string { + if number == nil { + return "0" + } + return new(big.Float).Quo(new(big.Float).SetInt(number), big.NewFloat(math.Pow10(int(precision)))).String() +} + +func translatePositions(positions map[int]*Position) map[int]*hu.Position { + huPositions := make(map[int]*hu.Position) + for key, value := range positions { + if value != nil { + huPositions[key] = &hu.Position{ + Size: new(big.Int).Set(value.Size), + OpenNotional: new(big.Int).Set(value.OpenNotional), + } + } + } + return huPositions +} diff --git a/plugin/evm/orderbook/liquidations_test.go b/plugin/evm/orderbook/liquidations_test.go new file mode 100644 index 0000000000..2ed27836dc --- /dev/null +++ b/plugin/evm/orderbook/liquidations_test.go @@ -0,0 +1,282 @@ +package orderbook + +import ( + "math/big" + "testing" + + hu "github.com/ava-labs/subnet-evm/plugin/evm/orderbook/hubbleutils" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/assert" +) + +func TestGetLiquidableTraders(t *testing.T) { + var market Market = Market(0) + collateral := HUSD + assets := []hu.Collateral{{Price: big.NewInt(1e6), Weight: big.NewInt(1e6), Decimals: 6}} + t.Run("When no trader exist", func(t *testing.T) { + db := getDatabase() + hState := &hu.HubbleState{ + Assets: assets, + OraclePrices: map[Market]*big.Int{market: hu.Mul1e6(big.NewInt(110))}, + ActiveMarkets: []hu.Market{market}, + MinAllowableMargin: db.configService.GetMinAllowableMargin(), + } + liquidablePositions, _, _ := db.GetNaughtyTraders(hState) + assert.Equal(t, 0, len(liquidablePositions)) + }) + + t.Run("When no trader has any positions", func(t *testing.T) { + longTraderAddress := common.HexToAddress("0x22Bb736b64A0b4D4081E103f83bccF864F0404aa") + margin := big.NewInt(10000000000) + db := getDatabase() + db.TraderMap = map[common.Address]*Trader{ + longTraderAddress: { + Margin: Margin{ + Reserved: big.NewInt(0), + Deposited: map[Collateral]*big.Int{collateral: margin}, + }, + Positions: map[Market]*Position{}, + }, + } + hState := &hu.HubbleState{ + Assets: assets, + OraclePrices: map[Market]*big.Int{market: hu.Mul1e6(big.NewInt(110))}, + MidPrices: map[Market]*big.Int{market: hu.Mul1e6(big.NewInt(100))}, + ActiveMarkets: []hu.Market{market}, + MaintenanceMargin: db.configService.GetMaintenanceMargin(), + MinAllowableMargin: db.configService.GetMinAllowableMargin(), + } + liquidablePositions, _, _ := db.GetNaughtyTraders(hState) + assert.Equal(t, 0, len(liquidablePositions)) + }) + + t.Run("long trader", func(t *testing.T) { + longTraderAddress := common.HexToAddress("0x22Bb736b64A0b4D4081E103f83bccF864F0404aa") + marginLong := hu.Mul1e6(big.NewInt(500)) + longSize := hu.Mul1e18(big.NewInt(10)) + longEntryPrice := hu.Mul1e6(big.NewInt(90)) + openNotionalLong := hu.Div1e18(big.NewInt(0).Mul(longEntryPrice, longSize)) + pendingFundingLong := hu.Mul1e6(big.NewInt(42)) + t.Run("is saved from liquidation zone by oracle price", func(t *testing.T) { + // setup db + db := getDatabase() + longTrader := Trader{ + Margin: Margin{ + Reserved: big.NewInt(0), + Deposited: map[Collateral]*big.Int{collateral: marginLong}, + }, + Positions: map[Market]*Position{ + market: getPosition(market, openNotionalLong, longSize, pendingFundingLong, big.NewInt(0), big.NewInt(0), db.configService.getMaxLiquidationRatio(market), db.configService.getMinSizeRequirement(market)), + }, + } + db.TraderMap = map[common.Address]*Trader{ + longTraderAddress: &longTrader, + } + hState := &hu.HubbleState{ + Assets: assets, + OraclePrices: map[Market]*big.Int{market: hu.Mul1e6(big.NewInt(50))}, + MidPrices: map[Market]*big.Int{market: hu.Mul1e6(big.NewInt(49))}, + ActiveMarkets: []hu.Market{market}, + MinAllowableMargin: db.configService.GetMinAllowableMargin(), + MaintenanceMargin: db.configService.GetMaintenanceMargin(), + UpgradeVersion: hu.V2, + } + oraclePrices := hState.OraclePrices + + // assertions begin + // for long trader + _trader := &longTrader + assert.Equal(t, marginLong, getNormalisedMargin(_trader, assets)) + assert.Equal(t, pendingFundingLong, getTotalFunding(_trader, []Market{market})) + + // open notional = 90 * 10 = 900 + // oracle price: notional = 50 * 10 = 500, pnl = 500-900 = -400, mf = (500-42-400)/500 = 0.116 + + availableMargin := getAvailableMargin(_trader, hState) + // availableMargin = 500 - 42 (pendingFundingLong) - 400 (uPnL) - 500/5 = -42 + assert.Equal(t, hu.Mul1e6(big.NewInt(-42)), availableMargin) + + // for hu.Maintenance_Margin we select the max of 2 hence, oracle_mf + notionalPosition, unrealizePnL := getTotalNotionalPositionAndUnrealizedPnl(_trader, new(big.Int).Add(marginLong, pendingFundingLong), hu.Maintenance_Margin, oraclePrices, hState.MidPrices, []Market{market}) + assert.Equal(t, hu.Mul1e6(big.NewInt(500)), notionalPosition) + assert.Equal(t, hu.Mul1e6(big.NewInt(-400)), unrealizePnL) + + marginFraction := calcMarginFraction(_trader, hState) + assert.Equal(t, new(big.Int).Div(hu.Mul1e6(new(big.Int).Add(new(big.Int).Sub(marginLong, pendingFundingLong), unrealizePnL)), notionalPosition), marginFraction) + + liquidablePositions, _, _ := db.GetNaughtyTraders(hState) + assert.Equal(t, 0, len(liquidablePositions)) + }) + }) + + t.Run("short trader is saved from liquidation zone by mark price", func(t *testing.T) { + shortTraderAddress := common.HexToAddress("0x710bf5F942331874dcBC7783319123679033b63b") + marginShort := hu.Mul1e6(big.NewInt(1000)) + shortSize := hu.Mul1e18(big.NewInt(-20)) + shortEntryPrice := hu.Mul1e6(big.NewInt(105)) + openNotionalShort := hu.Div1e18(big.NewInt(0).Abs(big.NewInt(0).Mul(shortEntryPrice, shortSize))) + pendingFundingShort := hu.Mul1e6(big.NewInt(-37)) + t.Run("is saved from liquidation zone by oracle price", func(t *testing.T) { + // setup db + db := getDatabase() + shortTrader := Trader{ + Margin: Margin{ + Reserved: big.NewInt(0), + Deposited: map[Collateral]*big.Int{collateral: marginShort}, + }, + Positions: map[Market]*Position{ + market: getPosition(market, openNotionalShort, shortSize, pendingFundingShort, big.NewInt(0), big.NewInt(0), db.configService.getMaxLiquidationRatio(market), db.configService.getMinSizeRequirement(market)), + }, + } + db.TraderMap = map[common.Address]*Trader{ + shortTraderAddress: &shortTrader, + } + hState := &hu.HubbleState{ + Assets: assets, + OraclePrices: map[Market]*big.Int{market: hu.Mul1e6(big.NewInt(142))}, + MidPrices: map[Market]*big.Int{market: hu.Mul1e6(big.NewInt(143))}, + ActiveMarkets: []hu.Market{market}, + MinAllowableMargin: db.configService.GetMinAllowableMargin(), + MaintenanceMargin: db.configService.GetMaintenanceMargin(), + } + oraclePrices := hState.OraclePrices + + // assertions begin + _trader := &shortTrader + assert.Equal(t, marginShort, getNormalisedMargin(_trader, assets)) + assert.Equal(t, pendingFundingShort, getTotalFunding(_trader, []Market{market})) + + // open notional = 105 * 20 = 2100 + // oracle price: notional = 142 * 20 = 2840, pnl = 2100-2840 = -740, mf = (1000+37-740)/2840 = 0.104 + + availableMargin := getAvailableMargin(_trader, hState) + // availableMargin = 1000 + 37 (pendingFundingShort) - 760 (uPnL) - 2860/5 = -295 + assert.Equal(t, hu.Mul1e6(big.NewInt(-295)), availableMargin) + + // for hu.Maintenance_Margin we select the max of 2 hence, oracle_mf + notionalPosition, unrealizePnL := getTotalNotionalPositionAndUnrealizedPnl(_trader, new(big.Int).Add(marginShort, pendingFundingShort), hu.Maintenance_Margin, oraclePrices, hState.MidPrices, []Market{market}) + assert.Equal(t, hu.Mul1e6(big.NewInt(2840)), notionalPosition) + assert.Equal(t, hu.Mul1e6(big.NewInt(-740)), unrealizePnL) + + marginFraction := calcMarginFraction(_trader, hState) + assert.Equal(t, new(big.Int).Div(hu.Mul1e6(new(big.Int).Add(new(big.Int).Sub(marginShort, pendingFundingShort), unrealizePnL)), notionalPosition), marginFraction) + + liquidablePositions, _, _ := db.GetNaughtyTraders(hState) + assert.Equal(t, 0, len(liquidablePositions)) + }) + }) +} + +func TestGetNormalisedMargin(t *testing.T) { + assets := []hu.Collateral{{Price: big.NewInt(1e6), Weight: big.NewInt(1e6), Decimals: 6}} + t.Run("When trader has no margin", func(t *testing.T) { + trader := Trader{} + assert.Equal(t, big.NewInt(0), getNormalisedMargin(&trader, assets)) + }) + t.Run("When trader has margin in HUSD", func(t *testing.T) { + margin := hu.Mul1e6(big.NewInt(10)) + trader := Trader{ + Margin: Margin{Deposited: map[Collateral]*big.Int{ + HUSD: margin, + }}, + } + assert.Equal(t, margin, getNormalisedMargin(&trader, assets)) + }) +} + +func TestGetNotionalPosition(t *testing.T) { + t.Run("When size is positive, it return abs value", func(t *testing.T) { + price := hu.Mul1e6(big.NewInt(10)) + size := hu.Mul1e18(big.NewInt(20)) + expectedNotionalPosition := hu.Div1e18(big.NewInt(0).Mul(price, size)) + assert.Equal(t, expectedNotionalPosition, hu.GetNotionalPosition(price, size)) + }) + t.Run("When size is negative, it return abs value", func(t *testing.T) { + price := hu.Mul1e6(big.NewInt(10)) + size := hu.Mul1e18(big.NewInt(-20)) + expectedNotionalPosition := hu.Div1e18(big.NewInt(0).Abs(big.NewInt(0).Mul(price, size))) + assert.Equal(t, expectedNotionalPosition, hu.GetNotionalPosition(price, size)) + }) +} + +func TestGetPositionMetadata(t *testing.T) { + t.Run("When newPrice is > entryPrice", func(t *testing.T) { + t.Run("When size is positive", func(t *testing.T) { + size := hu.Mul1e18(big.NewInt(10)) + entryPrice := hu.Mul1e6(big.NewInt(10)) + newPrice := hu.Mul1e6(big.NewInt(15)) + position := &Position{ + Position: hu.Position{OpenNotional: hu.GetNotionalPosition(entryPrice, size), Size: size}, + } + + arbitaryMarginValue := hu.Mul1e6(big.NewInt(69)) + notionalPosition, uPnL, mf := getPositionMetadata(newPrice, position.OpenNotional, position.Size, arbitaryMarginValue) + assert.Equal(t, hu.GetNotionalPosition(newPrice, size), notionalPosition) + expectedPnl := hu.Div1e18(big.NewInt(0).Mul(big.NewInt(0).Sub(newPrice, entryPrice), size)) + assert.Equal(t, expectedPnl, uPnL) + assert.Equal(t, new(big.Int).Div(hu.Mul1e6(new(big.Int).Add(arbitaryMarginValue, uPnL)), notionalPosition), mf) + }) + t.Run("When size is negative", func(t *testing.T) { + size := hu.Mul1e18(big.NewInt(-10)) + entryPrice := hu.Mul1e6(big.NewInt(10)) + newPrice := hu.Mul1e6(big.NewInt(15)) + position := &Position{ + Position: hu.Position{OpenNotional: hu.GetNotionalPosition(entryPrice, size), Size: size}, + } + + notionalPosition, uPnL, _ := getPositionMetadata(newPrice, position.OpenNotional, position.Size, big.NewInt(0)) + assert.Equal(t, hu.GetNotionalPosition(newPrice, size), notionalPosition) + expectedPnl := hu.Div1e18(big.NewInt(0).Mul(big.NewInt(0).Sub(newPrice, entryPrice), size)) + assert.Equal(t, expectedPnl, uPnL) + }) + }) + t.Run("When newPrice is < entryPrice", func(t *testing.T) { + t.Run("When size is positive", func(t *testing.T) { + size := hu.Mul1e18(big.NewInt(10)) + entryPrice := hu.Mul1e6(big.NewInt(10)) + newPrice := hu.Mul1e6(big.NewInt(5)) + position := &Position{ + Position: hu.Position{OpenNotional: hu.GetNotionalPosition(entryPrice, size), Size: size}, + } + + notionalPosition, uPnL, _ := getPositionMetadata(newPrice, position.OpenNotional, position.Size, big.NewInt(0)) + assert.Equal(t, hu.GetNotionalPosition(newPrice, size), notionalPosition) + expectedPnl := hu.Div1e18(big.NewInt(0).Mul(big.NewInt(0).Sub(newPrice, entryPrice), size)) + assert.Equal(t, expectedPnl, uPnL) + }) + t.Run("When size is negative", func(t *testing.T) { + size := hu.Mul1e18(big.NewInt(-10)) + entryPrice := hu.Mul1e6(big.NewInt(10)) + newPrice := hu.Mul1e6(big.NewInt(5)) + position := &Position{ + Position: hu.Position{OpenNotional: hu.GetNotionalPosition(entryPrice, size), Size: size}, + } + notionalPosition, uPnL, _ := getPositionMetadata(newPrice, position.OpenNotional, position.Size, big.NewInt(0)) + assert.Equal(t, hu.GetNotionalPosition(newPrice, size), notionalPosition) + expectedPnl := hu.Div1e18(big.NewInt(0).Mul(big.NewInt(0).Sub(newPrice, entryPrice), size)) + assert.Equal(t, expectedPnl, uPnL) + }) + }) +} + +func getPosition(market Market, openNotional *big.Int, size *big.Int, unrealizedFunding *big.Int, lastPremiumFraction *big.Int, liquidationThreshold *big.Int, maxLiquidationRatio *big.Int, minSizeRequirement *big.Int) *Position { + if liquidationThreshold.Sign() == 0 { + liquidationThreshold = getLiquidationThreshold(maxLiquidationRatio, minSizeRequirement, size) + } + return &Position{ + Position: hu.Position{OpenNotional: openNotional, Size: size}, + UnrealisedFunding: unrealizedFunding, + LastPremiumFraction: lastPremiumFraction, + LiquidationThreshold: liquidationThreshold, + } +} + +func getDatabase() *InMemoryDatabase { + configService := NewMockConfigService() + configService.Mock.On("GetMaintenanceMargin").Return(big.NewInt(1e5)) + configService.Mock.On("GetMinAllowableMargin").Return(big.NewInt(2e5)) + configService.Mock.On("getMaxLiquidationRatio").Return(big.NewInt(1e6)) + configService.Mock.On("getMinSizeRequirement").Return(big.NewInt(1e16)) + + return NewInMemoryDatabase(configService) +} diff --git a/plugin/evm/orderbook/matching_pipeline.go b/plugin/evm/orderbook/matching_pipeline.go new file mode 100644 index 0000000000..6f17c673a5 --- /dev/null +++ b/plugin/evm/orderbook/matching_pipeline.go @@ -0,0 +1,456 @@ +package orderbook + +import ( + "fmt" + "math" + "math/big" + "sync" + "time" + + "github.com/ava-labs/subnet-evm/core/types" + hu "github.com/ava-labs/subnet-evm/plugin/evm/orderbook/hubbleutils" + "github.com/ava-labs/subnet-evm/utils" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" +) + +const ( + // ticker frequency for calling signalTxsReady + matchingTickerDuration = 5 * time.Second + sanitaryTickerDuration = 1 * time.Second +) + +type MatchingPipeline struct { + mu sync.Mutex + db LimitOrderDatabase + lotp LimitOrderTxProcessor + configService IConfigService + MatchingTicker *time.Ticker + SanitaryTicker *time.Ticker +} + +func NewMatchingPipeline( + db LimitOrderDatabase, + lotp LimitOrderTxProcessor, + configService IConfigService) *MatchingPipeline { + + return &MatchingPipeline{ + db: db, + lotp: lotp, + configService: configService, + MatchingTicker: time.NewTicker(matchingTickerDuration), + SanitaryTicker: time.NewTicker(sanitaryTickerDuration), + } +} + +func NewTemporaryMatchingPipeline( + db LimitOrderDatabase, + lotp LimitOrderTxProcessor, + configService IConfigService) *MatchingPipeline { + + return &MatchingPipeline{ + db: db, + lotp: lotp, + configService: configService, + } +} + +func (pipeline *MatchingPipeline) RunSanitization() { + pipeline.db.RemoveExpiredSignedOrders() +} + +func (pipeline *MatchingPipeline) Run(blockNumber *big.Int) bool { + pipeline.mu.Lock() + defer pipeline.mu.Unlock() + + // reset ticker + pipeline.MatchingTicker.Reset(matchingTickerDuration) + // SUNSET: this is ok, we can skip matching, liquidation, settleFunding, commitSampleLiquidity when markets are settled + markets := pipeline.GetActiveMarkets() + log.Info("MatchingPipeline:Run", "blockNumber", blockNumber) + + if len(markets) == 0 { + return false + } + + // start fresh and purge all local transactions + pipeline.lotp.PurgeOrderBookTxs() + + if isFundingPaymentTime(pipeline.db.GetNextFundingTime()) { + log.Info("MatchingPipeline:isFundingPaymentTime") + err := executeFundingPayment(pipeline.lotp) + if err != nil { + log.Error("Funding payment job failed", "err", err) + } + } + + // check nextSamplePITime + if isSamplePITime(pipeline.db.GetNextSamplePITime(), pipeline.db.GetSamplePIAttemptedTime()) { + log.Info("MatchingPipeline:isSamplePITime") + err := pipeline.lotp.ExecuteSamplePITx() + if err != nil { + log.Error("Sample PI job failed", "err", err) + } + } + + // fetch various hubble market params and run the matching engine + hState := GetHubbleState(pipeline.configService) + + // build trader map + liquidablePositions, ordersToCancel, marginMap := pipeline.db.GetNaughtyTraders(hState) + cancellableOrderIds := pipeline.cancelLimitOrders(ordersToCancel) + orderMap := make(map[Market]*Orders) + for _, market := range markets { + orderMap[market] = pipeline.fetchOrders(market, hState.OraclePrices[market], cancellableOrderIds, blockNumber) + } + pipeline.runLiquidations(liquidablePositions, orderMap, hState.OraclePrices, marginMap) + for _, market := range markets { + // @todo should we prioritize matching in any particular market? + upperBound, _ := pipeline.configService.GetAcceptableBounds(market) + pipeline.runMatchingEngine(pipeline.lotp, orderMap[market].longOrders, orderMap[market].shortOrders, marginMap, hState.MinAllowableMargin, hState.TakerFee, upperBound) + } + + orderBookTxsCount := pipeline.lotp.GetOrderBookTxsCount() + log.Info("MatchingPipeline:Complete", "orderBookTxsCount", orderBookTxsCount) + if orderBookTxsCount > 0 { + pipeline.lotp.SetOrderBookTxsBlockNumber(blockNumber.Uint64()) + return true + } + return false +} + +func (pipeline *MatchingPipeline) GetOrderMatchingTransactions(blockNumber *big.Int, markets []Market) map[common.Address]types.Transactions { + pipeline.mu.Lock() + defer pipeline.mu.Unlock() + + // SUNSET: ok to skip when markets are settled + activeMarkets := pipeline.GetActiveMarkets() + log.Info("MatchingPipeline:GetOrderMatchingTransactions") + + if len(activeMarkets) == 0 { + return nil + } + + // start fresh and purge all local transactions + pipeline.lotp.PurgeOrderBookTxs() + + // fetch various hubble market params and run the matching engine + hState := GetHubbleState(pipeline.configService) + hState.OraclePrices = hu.ArrayToMap(pipeline.configService.GetUnderlyingPrices()) + + marginMap := make(map[common.Address]*big.Int) + for addr, trader := range pipeline.db.GetAllTraders() { + userState := &hu.UserState{ + Positions: translatePositions(trader.Positions), + Margins: getMargins(&trader, len(hState.Assets)), + PendingFunding: getTotalFunding(&trader, hState.ActiveMarkets), + ReservedMargin: new(big.Int).Set(trader.Margin.Reserved), + // this is the only leveldb read, others above are in-memory reads + ReduceOnlyAmounts: pipeline.configService.GetReduceOnlyAmounts(addr), + } + marginMap[addr] = hu.GetAvailableMargin(hState, userState) + } + for _, market := range markets { + orders := pipeline.fetchOrders(market, hState.OraclePrices[market], map[common.Hash]struct{}{}, blockNumber) + upperBound, _ := pipeline.configService.GetAcceptableBounds(market) + pipeline.runMatchingEngine(pipeline.lotp, orders.longOrders, orders.shortOrders, marginMap, hState.MinAllowableMargin, hState.TakerFee, upperBound) + } + + orderbookTxs := pipeline.lotp.GetOrderBookTxs() + pipeline.lotp.PurgeOrderBookTxs() + return orderbookTxs +} + +type Orders struct { + longOrders []Order + shortOrders []Order +} + +func (pipeline *MatchingPipeline) GetActiveMarkets() []Market { + count := pipeline.configService.GetActiveMarketsCount() + markets := make([]Market, count) + for i := int64(0); i < count; i++ { + markets[i] = Market(i) + } + return markets +} + +func (pipeline *MatchingPipeline) GetCollaterals() []hu.Collateral { + return pipeline.configService.GetCollaterals() +} + +func (pipeline *MatchingPipeline) cancelLimitOrders(cancellableOrders map[common.Address][]Order) map[common.Hash]struct{} { + cancellableOrderIds := map[common.Hash]struct{}{} + // @todo: if there are too many cancellable orders, they might not fit in a single block. Need to adjust for that. + for _, orders := range cancellableOrders { + if len(orders) == 0 { + continue + } + rawOrders := make([]LimitOrder, 0) + for _, order := range orders { + rawOrders = append(rawOrders, *order.RawOrder.(*LimitOrder)) + cancellableOrderIds[order.Id] = struct{}{} // do not attempt to match these orders + } + + log.Info("orders to cancel", "num", len(orders)) + // cancel max of 5 orders. change this if the tx gas limit (1.5m) is changed + if err := pipeline.lotp.ExecuteLimitOrderCancel(rawOrders[0:int(math.Min(float64(len(rawOrders)), 5))]); err != nil { + log.Error("Error in ExecuteOrderCancel", "orders", orders, "err", err) + } + } + return cancellableOrderIds +} + +func (pipeline *MatchingPipeline) fetchOrders(market Market, underlyingPrice *big.Int, cancellableOrderIds map[common.Hash]struct{}, blockNumber *big.Int) *Orders { + _, lowerBoundForLongs := pipeline.configService.GetAcceptableBounds(market) + // any long orders below the permissible lowerbound are irrelevant, because they won't be matched no matter what. + // this assumes that all above cancelOrder transactions got executed successfully (or atleast they are not meant to be executed anyway if they passed the cancellation criteria) + longOrders := removeOrdersWithIds(pipeline.db.GetLongOrders(market, lowerBoundForLongs, blockNumber), cancellableOrderIds) + + // say if there were no long orders, then shord orders above liquidation upper bound are irrelevant, because they won't be matched no matter what + // note that this assumes that permissible liquidation spread <= oracle spread + upperBoundforShorts, _ := pipeline.configService.GetAcceptableBoundsForLiquidation(market) + + // however, if long orders exist, then + if len(longOrders) != 0 { + oracleUpperBound, _ := pipeline.configService.GetAcceptableBounds(market) + // take the max of price of highest long and liq upper bound. But say longOrders[0].Price > oracleUpperBound ? - then we discard orders above oracleUpperBound, because they won't be matched no matter what + upperBoundforShorts = utils.BigIntMin(utils.BigIntMax(longOrders[0].Price, upperBoundforShorts), oracleUpperBound) + } + shortOrders := removeOrdersWithIds(pipeline.db.GetShortOrders(market, upperBoundforShorts, blockNumber), cancellableOrderIds) + return &Orders{longOrders, shortOrders} +} + +func (pipeline *MatchingPipeline) runLiquidations(liquidablePositions []LiquidablePosition, orderMap map[Market]*Orders, underlyingPrices map[Market]*big.Int, marginMap map[common.Address]*big.Int) { + if len(liquidablePositions) == 0 { + return + } + + log.Info("found positions to liquidate", "num", len(liquidablePositions)) + + // we need to retreive permissible bounds for liquidations in each market + // SUNSET: this is ok, we can skip liquidations when markets are settled + markets := pipeline.GetActiveMarkets() + type S struct { + Upperbound *big.Int + Lowerbound *big.Int + } + if len(markets) == 0 { + return + } + liquidationBounds := make([]S, len(markets)) + for _, market := range markets { + upperbound, lowerbound := pipeline.configService.GetAcceptableBoundsForLiquidation(market) + liquidationBounds[market] = S{Upperbound: upperbound, Lowerbound: lowerbound} + } + + minAllowableMargin := pipeline.configService.GetMinAllowableMargin() + takerFee := pipeline.configService.GetTakerFee() + for _, liquidable := range liquidablePositions { + market := liquidable.Market + numOrdersExhausted := 0 + switch liquidable.PositionType { + case LONG: + for _, order := range orderMap[market].longOrders { + if order.Price.Cmp(liquidationBounds[market].Lowerbound) == -1 { + // further orders are not not eligible to liquidate with + break + } + fillAmount := utils.BigIntMinAbs(liquidable.GetUnfilledSize(), order.GetUnFilledBaseAssetQuantity()) + if marginMap[order.Trader] == nil { + // compatibility with existing tests + marginMap[order.Trader] = big.NewInt(0) + } + requiredMargin, err := isExecutable(&order, fillAmount, minAllowableMargin, takerFee, liquidationBounds[market].Upperbound, marginMap[order.Trader]) + if err != nil { + log.Error("order is not executable", "order", order, "err", err) + numOrdersExhausted++ + continue + } + marginMap[order.Trader].Sub(marginMap[order.Trader], requiredMargin) // deduct available margin for this run + pipeline.lotp.ExecuteLiquidation(liquidable.Address, order, fillAmount) + order.FilledBaseAssetQuantity.Add(order.FilledBaseAssetQuantity, fillAmount) + liquidable.FilledSize.Add(liquidable.FilledSize, fillAmount) + if order.GetUnFilledBaseAssetQuantity().Sign() == 0 { + numOrdersExhausted++ + } + if liquidable.GetUnfilledSize().Sign() == 0 { + break // partial/full liquidation for this position slated for this run is complete + } + } + orderMap[market].longOrders = orderMap[market].longOrders[numOrdersExhausted:] + case SHORT: + for _, order := range orderMap[market].shortOrders { + if order.Price.Cmp(liquidationBounds[market].Upperbound) == 1 { + // further orders are not not eligible to liquidate with + break + } + fillAmount := utils.BigIntMinAbs(liquidable.GetUnfilledSize(), order.GetUnFilledBaseAssetQuantity()) + if marginMap[order.Trader] == nil { + marginMap[order.Trader] = big.NewInt(0) + } + requiredMargin, err := isExecutable(&order, fillAmount, minAllowableMargin, takerFee, liquidationBounds[market].Upperbound, marginMap[order.Trader]) + if err != nil { + log.Error("order is not executable", "order", order, "err", err) + numOrdersExhausted++ + continue + } + marginMap[order.Trader].Sub(marginMap[order.Trader], requiredMargin) // deduct available margin for this run + pipeline.lotp.ExecuteLiquidation(liquidable.Address, order, fillAmount) + order.FilledBaseAssetQuantity.Sub(order.FilledBaseAssetQuantity, fillAmount) + liquidable.FilledSize.Sub(liquidable.FilledSize, fillAmount) + if order.GetUnFilledBaseAssetQuantity().Sign() == 0 { + numOrdersExhausted++ + } + if liquidable.GetUnfilledSize().Sign() == 0 { + break // partial/full liquidation for this position slated for this run is complete + } + } + orderMap[market].shortOrders = orderMap[market].shortOrders[numOrdersExhausted:] + } + if liquidable.GetUnfilledSize().Sign() != 0 { + unquenchedLiquidationsCounter.Inc(1) + log.Info("unquenched liquidation", "liquidable", liquidable) + } + } +} + +func (pipeline *MatchingPipeline) runMatchingEngine(lotp LimitOrderTxProcessor, longOrders []Order, shortOrders []Order, marginMap map[common.Address]*big.Int, minAllowableMargin, takerFee, upperBound *big.Int) { + for i := 0; i < len(longOrders); i++ { + // if there are no short orders or if the price of the first long order is < the price of the first short order, then we can stop matching + if len(shortOrders) == 0 || longOrders[i].Price.Cmp(shortOrders[0].Price) == -1 { + break + } + numOrdersExhausted := 0 + for j := 0; j < len(shortOrders); j++ { + fillAmount, err := areMatchingOrders(longOrders[i], shortOrders[j], marginMap, minAllowableMargin, takerFee, upperBound) + if err != nil { + log.Error("orders not matcheable", "longOrder", longOrders[i], "shortOrder", shortOrders[i], "err", err) + continue + } + longOrders[i], shortOrders[j] = ExecuteMatchedOrders(lotp, longOrders[i], shortOrders[j], fillAmount) + if shortOrders[j].GetUnFilledBaseAssetQuantity().Sign() == 0 { + numOrdersExhausted++ + } + if longOrders[i].GetUnFilledBaseAssetQuantity().Sign() == 0 { + break + } + } + shortOrders = shortOrders[numOrdersExhausted:] + } +} + +func areMatchingOrders(longOrder, shortOrder Order, marginMap map[common.Address]*big.Int, minAllowableMargin, takerFee, upperBound *big.Int) (*big.Int, error) { + if longOrder.Price.Cmp(shortOrder.Price) == -1 { + return nil, fmt.Errorf("long order price %s is less than short order price %s", longOrder.Price, shortOrder.Price) + } + blockDiff := longOrder.BlockNumber.Cmp(shortOrder.BlockNumber) + if blockDiff == -1 && (longOrder.OrderType == IOC || shortOrder.isPostOnly()) || + blockDiff == 1 && (shortOrder.OrderType == IOC || longOrder.isPostOnly()) { + return nil, fmt.Errorf("resting order semantics mismatch") + } + fillAmount := utils.BigIntMinAbs(longOrder.GetUnFilledBaseAssetQuantity(), shortOrder.GetUnFilledBaseAssetQuantity()) + if fillAmount.Sign() == 0 { + return nil, fmt.Errorf("no fill amount") + } + + longMargin, err := isExecutable(&longOrder, fillAmount, minAllowableMargin, takerFee, upperBound, marginMap[longOrder.Trader]) + if err != nil { + return nil, err + } + + shortMargin, err := isExecutable(&shortOrder, fillAmount, minAllowableMargin, takerFee, upperBound, marginMap[shortOrder.Trader]) + if err != nil { + return nil, err + } + marginMap[longOrder.Trader].Sub(marginMap[longOrder.Trader], longMargin) + marginMap[shortOrder.Trader].Sub(marginMap[shortOrder.Trader], shortMargin) + return fillAmount, nil +} + +func isExecutable(order *Order, fillAmount, minAllowableMargin, takerFee, upperBound, availableMargin *big.Int) (*big.Int, error) { + if order.OrderType == Limit || order.ReduceOnly { + return big.NewInt(0), nil // no extra margin required because for limit orders it is already reserved + } + requiredMargin := big.NewInt(0) + if order.OrderType == IOC { + requiredMargin = getRequiredMargin(order, fillAmount, minAllowableMargin, takerFee, upperBound) + } + if order.OrderType == Signed { + requiredMargin = getRequiredMargin(order, fillAmount, minAllowableMargin, big.NewInt(0) /* signed orders are always maker */, upperBound) + } + if requiredMargin.Cmp(availableMargin) > 0 { + return nil, fmt.Errorf("insufficient margin. trader %s, required: %s, available: %s", order.Trader, requiredMargin, availableMargin) + } + return requiredMargin, nil +} + +func getRequiredMargin(order *Order, fillAmount, minAllowableMargin, takerFee, upperBound *big.Int) *big.Int { + price := order.Price + if order.BaseAssetQuantity.Sign() == -1 && order.Price.Cmp(upperBound) == -1 { + price = upperBound + } + return hu.GetRequiredMargin(price, fillAmount, minAllowableMargin, takerFee) +} + +func ExecuteMatchedOrders(lotp LimitOrderTxProcessor, longOrder, shortOrder Order, fillAmount *big.Int) (Order, Order) { + lotp.ExecuteMatchedOrdersTx(longOrder, shortOrder, fillAmount) + longOrder.FilledBaseAssetQuantity = big.NewInt(0).Add(longOrder.FilledBaseAssetQuantity, fillAmount) + shortOrder.FilledBaseAssetQuantity = big.NewInt(0).Sub(shortOrder.FilledBaseAssetQuantity, fillAmount) + return longOrder, shortOrder +} + +func matchLongAndShortOrder(lotp LimitOrderTxProcessor, longOrder, shortOrder Order) (Order, Order, bool) { + fillAmount := utils.BigIntMinAbs(longOrder.GetUnFilledBaseAssetQuantity(), shortOrder.GetUnFilledBaseAssetQuantity()) + if longOrder.Price.Cmp(shortOrder.Price) == -1 || fillAmount.Sign() == 0 { + return longOrder, shortOrder, false + } + if longOrder.BlockNumber.Cmp(shortOrder.BlockNumber) > 0 && longOrder.isPostOnly() { + log.Warn("post only long order matched with a resting order", "longOrder", longOrder, "shortOrder", shortOrder) + return longOrder, shortOrder, false + } + if shortOrder.BlockNumber.Cmp(longOrder.BlockNumber) > 0 && shortOrder.isPostOnly() { + log.Warn("post only short order matched with a resting order", "longOrder", longOrder, "shortOrder", shortOrder) + return longOrder, shortOrder, false + } + if err := lotp.ExecuteMatchedOrdersTx(longOrder, shortOrder, fillAmount); err != nil { + return longOrder, shortOrder, false + } + longOrder.FilledBaseAssetQuantity = big.NewInt(0).Add(longOrder.FilledBaseAssetQuantity, fillAmount) + shortOrder.FilledBaseAssetQuantity = big.NewInt(0).Sub(shortOrder.FilledBaseAssetQuantity, fillAmount) + return longOrder, shortOrder, true +} + +func isFundingPaymentTime(nextFundingTime uint64) bool { + if nextFundingTime == 0 { + return false + } + + now := uint64(time.Now().Unix()) + return now >= nextFundingTime +} + +func isSamplePITime(nextSamplePITime, lastAttempt uint64) bool { + if nextSamplePITime == 0 { + return false + } + + now := uint64(time.Now().Unix()) + return now >= nextSamplePITime && now >= lastAttempt+5 // give 5 secs for the tx to be mined +} + +func executeFundingPayment(lotp LimitOrderTxProcessor) error { + // @todo get index twap for each market with warp msging + + return lotp.ExecuteFundingPaymentTx() +} + +func removeOrdersWithIds(orders []Order, orderIds map[common.Hash]struct{}) []Order { + var filteredOrders []Order + for _, order := range orders { + if _, ok := orderIds[order.Id]; !ok { + filteredOrders = append(filteredOrders, order) + } + } + return filteredOrders +} diff --git a/plugin/evm/orderbook/matching_pipeline_test.go b/plugin/evm/orderbook/matching_pipeline_test.go new file mode 100644 index 0000000000..35aa809cd3 --- /dev/null +++ b/plugin/evm/orderbook/matching_pipeline_test.go @@ -0,0 +1,787 @@ +package orderbook + +import ( + "fmt" + "math/big" + "testing" + "time" + + hu "github.com/ava-labs/subnet-evm/plugin/evm/orderbook/hubbleutils" + "github.com/ava-labs/subnet-evm/utils" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestRunLiquidations(t *testing.T) { + traderAddress := common.HexToAddress("0x710bf5f942331874dcbc7783319123679033b63b") + traderAddress1 := common.HexToAddress("0x376c47978271565f56DEB45495afa69E59c16Ab2") + market := Market(0) + liqUpperBound := big.NewInt(22) + liqLowerBound := big.NewInt(18) + + t.Run("when there are no liquidable positions", func(t *testing.T) { + _, lotp, pipeline, underlyingPrices, _ := setupDependencies(t) + longOrders := []Order{getLongOrder()} + shortOrders := []Order{getShortOrder()} + + orderMap := map[Market]*Orders{market: {longOrders, shortOrders}} + pipeline.runLiquidations([]LiquidablePosition{}, orderMap, underlyingPrices, map[common.Address]*big.Int{}) + assert.Equal(t, longOrders, orderMap[market].longOrders) + assert.Equal(t, shortOrders, orderMap[market].shortOrders) + lotp.AssertNotCalled(t, "ExecuteLiquidation", mock.Anything, mock.Anything, mock.Anything) + }) + + t.Run("when liquidable position is long", func(t *testing.T) { + t.Run("when no long orders are present in database for matching", func(t *testing.T) { + _, lotp, pipeline, underlyingPrices, cs := setupDependencies(t) + longOrders := []Order{} + shortOrders := []Order{getShortOrder()} + + orderMap := map[Market]*Orders{market: {longOrders, shortOrders}} + + cs.On("GetAcceptableBoundsForLiquidation", market).Return(liqUpperBound, liqLowerBound) + cs.On("GetMinAllowableMargin").Return(big.NewInt(1e5)) + cs.On("GetTakerFee").Return(big.NewInt(1e5)) + pipeline.runLiquidations([]LiquidablePosition{getLiquidablePos(traderAddress, LONG, 7)}, orderMap, underlyingPrices, map[common.Address]*big.Int{}) + assert.Equal(t, longOrders, orderMap[market].longOrders) + assert.Equal(t, shortOrders, orderMap[market].shortOrders) + lotp.AssertNotCalled(t, "ExecuteLiquidation", mock.Anything, mock.Anything, mock.Anything) + cs.AssertCalled(t, "GetAcceptableBoundsForLiquidation", market) + }) + t.Run("when long orders are present in database for matching", func(t *testing.T) { + liquidablePositions := []LiquidablePosition{getLiquidablePos(traderAddress, LONG, 7)} + _, lotp, pipeline, underlyingPrices, cs := setupDependencies(t) + longOrder := getLongOrder() + shortOrder := getShortOrder() + expectedFillAmount := utils.BigIntMinAbs(longOrder.BaseAssetQuantity, liquidablePositions[0].Size) + cs.On("GetAcceptableBoundsForLiquidation", market).Return(liqUpperBound, liqLowerBound) + cs.On("GetMinAllowableMargin").Return(big.NewInt(1e5)) + cs.On("GetTakerFee").Return(big.NewInt(1e5)) + lotp.On("ExecuteLiquidation", traderAddress, longOrder, expectedFillAmount).Return(nil) + + orderMap := map[Market]*Orders{market: {[]Order{longOrder}, []Order{shortOrder}}} + + pipeline.runLiquidations(liquidablePositions, orderMap, underlyingPrices, map[common.Address]*big.Int{}) + + lotp.AssertCalled(t, "ExecuteLiquidation", traderAddress, longOrder, expectedFillAmount) + cs.AssertCalled(t, "GetAcceptableBoundsForLiquidation", market) + assert.Equal(t, shortOrder, orderMap[market].shortOrders[0]) + assert.Equal(t, expectedFillAmount.Uint64(), orderMap[market].longOrders[0].FilledBaseAssetQuantity.Uint64()) + }) + t.Run("2nd long order < liqLowerBound", func(t *testing.T) { + liquidablePositions := []LiquidablePosition{getLiquidablePos(traderAddress, LONG, 7)} + _, lotp, pipeline, underlyingPrices, cs := setupDependencies(t) + longOrder := getLongOrder() + longOrder.BaseAssetQuantity = big.NewInt(5) // 5 < liquidable.Size (7) + + longOrder2 := getLongOrder() + longOrder2.Price = big.NewInt(17) // 17 < lower bound (18) + + expectedFillAmount := utils.BigIntMinAbs(longOrder.BaseAssetQuantity, liquidablePositions[0].Size) + cs.On("GetAcceptableBoundsForLiquidation", market).Return(liqUpperBound, liqLowerBound) + cs.On("GetMinAllowableMargin").Return(big.NewInt(1e5)) + cs.On("GetTakerFee").Return(big.NewInt(1e5)) + lotp.On("ExecuteLiquidation", traderAddress, longOrder, expectedFillAmount).Return(nil) + + orderMap := map[Market]*Orders{market: {[]Order{longOrder, longOrder2}, []Order{}}} + + pipeline.runLiquidations(liquidablePositions, orderMap, underlyingPrices, map[common.Address]*big.Int{}) + + lotp.AssertCalled(t, "ExecuteLiquidation", traderAddress, longOrder, expectedFillAmount) + cs.AssertCalled(t, "GetAcceptableBoundsForLiquidation", market) + assert.Equal(t, 1, len(orderMap[market].longOrders)) // 0th order was consumed + assert.Equal(t, longOrder2, orderMap[market].longOrders[0]) // untouched + assert.Equal(t, big.NewInt(5), liquidablePositions[0].FilledSize) // 7 - 5 + }) + + t.Run("4 liquidable positions", func(t *testing.T) { + liquidablePositions := []LiquidablePosition{getLiquidablePos(traderAddress, LONG, 7), getLiquidablePos(traderAddress, SHORT, -8), getLiquidablePos(traderAddress1, LONG, 9), getLiquidablePos(traderAddress1, SHORT, -2)} + _, lotp, pipeline, underlyingPrices, cs := setupDependencies(t) + longOrder0 := buildLongOrder(20, 5) + longOrder1 := buildLongOrder(19, 12) + + shortOrder0 := buildShortOrder(19, -9) + shortOrder1 := buildShortOrder(liqLowerBound.Int64()-1, -8) + orderMap := map[Market]*Orders{market: {[]Order{longOrder0, longOrder1}, []Order{shortOrder0, shortOrder1}}} + + cs.On("GetAcceptableBoundsForLiquidation", market).Return(liqUpperBound, liqLowerBound) + cs.On("GetMinAllowableMargin").Return(big.NewInt(1e5)) + cs.On("GetTakerFee").Return(big.NewInt(1e5)) + lotp.On("ExecuteLiquidation", traderAddress, orderMap[market].longOrders[0], big.NewInt(5)).Return(nil) + lotp.On("ExecuteLiquidation", traderAddress, orderMap[market].longOrders[1], big.NewInt(2)).Return(nil) + lotp.On("ExecuteLiquidation", traderAddress1, orderMap[market].longOrders[1], big.NewInt(9)).Return(nil) + lotp.On("ExecuteLiquidation", traderAddress, orderMap[market].shortOrders[0], big.NewInt(8)).Return(nil) + lotp.On("ExecuteLiquidation", traderAddress1, orderMap[market].shortOrders[0], big.NewInt(1)).Return(nil) + lotp.On("ExecuteLiquidation", traderAddress1, orderMap[market].shortOrders[1], big.NewInt(1)).Return(nil) + + pipeline.runLiquidations(liquidablePositions, orderMap, underlyingPrices, map[common.Address]*big.Int{}) + cs.AssertCalled(t, "GetAcceptableBoundsForLiquidation", market) + + lotp.AssertCalled(t, "ExecuteLiquidation", traderAddress, longOrder0, big.NewInt(5)) + lotp.AssertCalled(t, "ExecuteLiquidation", traderAddress, longOrder0, big.NewInt(5)) + lotp.AssertCalled(t, "ExecuteLiquidation", traderAddress1, longOrder1, big.NewInt(9)) + lotp.AssertCalled(t, "ExecuteLiquidation", traderAddress, shortOrder0, big.NewInt(8)) + lotp.AssertCalled(t, "ExecuteLiquidation", traderAddress1, shortOrder0, big.NewInt(1)) + lotp.AssertCalled(t, "ExecuteLiquidation", traderAddress1, shortOrder1, big.NewInt(1)) + + assert.Equal(t, 1, len(orderMap[market].longOrders)) // 0th order was consumed + assert.Equal(t, big.NewInt(11), orderMap[market].longOrders[0].FilledBaseAssetQuantity) + assert.Equal(t, big.NewInt(7), liquidablePositions[0].FilledSize) + assert.Equal(t, big.NewInt(9), liquidablePositions[2].FilledSize) + + assert.Equal(t, 1, len(orderMap[market].shortOrders)) + assert.Equal(t, big.NewInt(-1), orderMap[market].shortOrders[0].FilledBaseAssetQuantity) + assert.Equal(t, big.NewInt(-8), liquidablePositions[1].FilledSize) + assert.Equal(t, big.NewInt(-2), liquidablePositions[3].FilledSize) + }) + }) + + t.Run("when liquidable position is short", func(t *testing.T) { + liquidablePositions := []LiquidablePosition{{ + Address: traderAddress, + Market: market, + PositionType: SHORT, + Size: big.NewInt(-7), + FilledSize: big.NewInt(0), + }} + t.Run("when no short orders are present in database for matching", func(t *testing.T) { + _, lotp, pipeline, underlyingPrices, cs := setupDependencies(t) + shortOrders := []Order{} + longOrders := []Order{getLongOrder()} + + orderMap := map[Market]*Orders{market: {longOrders, shortOrders}} + + cs.On("GetAcceptableBoundsForLiquidation", market).Return(liqUpperBound, liqLowerBound) + cs.On("GetMinAllowableMargin").Return(big.NewInt(1e5)) + cs.On("GetTakerFee").Return(big.NewInt(1e5)) + pipeline.runLiquidations(liquidablePositions, orderMap, underlyingPrices, map[common.Address]*big.Int{}) + assert.Equal(t, longOrders, orderMap[market].longOrders) + assert.Equal(t, shortOrders, orderMap[market].shortOrders) + lotp.AssertNotCalled(t, "ExecuteLiquidation", mock.Anything, mock.Anything, mock.Anything) + cs.AssertCalled(t, "GetAcceptableBoundsForLiquidation", market) + }) + t.Run("when short orders are present in database for matching", func(t *testing.T) { + _, lotp, pipeline, underlyingPrices, cs := setupDependencies(t) + longOrder := getLongOrder() + shortOrder := getShortOrder() + expectedFillAmount := utils.BigIntMinAbs(shortOrder.BaseAssetQuantity, liquidablePositions[0].Size) + lotp.On("ExecuteLiquidation", traderAddress, shortOrder, expectedFillAmount).Return(nil) + cs.On("GetAcceptableBoundsForLiquidation", market).Return(liqUpperBound, liqLowerBound) + cs.On("GetMinAllowableMargin").Return(big.NewInt(1e5)) + cs.On("GetTakerFee").Return(big.NewInt(1e5)) + + orderMap := map[Market]*Orders{market: {[]Order{longOrder}, []Order{shortOrder}}} + pipeline.runLiquidations(liquidablePositions, orderMap, underlyingPrices, map[common.Address]*big.Int{}) + + lotp.AssertCalled(t, "ExecuteLiquidation", traderAddress, shortOrder, expectedFillAmount) + cs.AssertCalled(t, "GetAcceptableBoundsForLiquidation", market) + + assert.Equal(t, longOrder, orderMap[market].longOrders[0]) + assert.Equal(t, expectedFillAmount.Uint64(), orderMap[market].shortOrders[0].FilledBaseAssetQuantity.Uint64()) + }) + }) +} + +func TestRunMatchingEngine(t *testing.T) { + minAllowableMargin := big.NewInt(1e6) + takerFee := big.NewInt(1e6) + upperBound := big.NewInt(22) + t.Run("when either long or short orders are not present in memorydb", func(t *testing.T) { + t.Run("when no short and long orders are present", func(t *testing.T) { + _, lotp, pipeline, _, _ := setupDependencies(t) + longOrders := make([]Order, 0) + shortOrders := make([]Order, 0) + pipeline.runMatchingEngine(lotp, longOrders, shortOrders, map[common.Address]*big.Int{}, minAllowableMargin, takerFee, upperBound) + lotp.AssertNotCalled(t, "ExecuteMatchedOrdersTx", mock.Anything, mock.Anything, mock.Anything) + }) + t.Run("when longOrders are not present but short orders are present", func(t *testing.T) { + _, lotp, pipeline, _, _ := setupDependencies(t) + longOrders := make([]Order, 0) + shortOrders := []Order{getShortOrder()} + pipeline.runMatchingEngine(lotp, longOrders, shortOrders, map[common.Address]*big.Int{}, minAllowableMargin, takerFee, upperBound) + lotp.AssertNotCalled(t, "ExecuteMatchedOrdersTx", mock.Anything, mock.Anything, mock.Anything) + }) + t.Run("when short orders are not present but long orders are present", func(t *testing.T) { + db, lotp, pipeline, _, _ := setupDependencies(t) + longOrders := make([]Order, 0) + shortOrders := make([]Order, 0) + longOrder := getLongOrder() + longOrders = append(longOrders, longOrder) + db.On("GetLongOrders").Return(longOrders) + db.On("GetShortOrders").Return(shortOrders) + lotp.On("PurgeLocalTx").Return(nil) + pipeline.runMatchingEngine(lotp, longOrders, shortOrders, map[common.Address]*big.Int{}, minAllowableMargin, takerFee, upperBound) + lotp.AssertNotCalled(t, "ExecuteMatchedOrdersTx", mock.Anything, mock.Anything, mock.Anything) + }) + }) + t.Run("When both long and short orders are present in db", func(t *testing.T) { + t.Run("when longOrder.Price < shortOrder.Price", func(t *testing.T) { + _, lotp, pipeline, _, _ := setupDependencies(t) + shortOrder := getShortOrder() + longOrder := getLongOrder() + longOrder.Price.Sub(shortOrder.Price, big.NewInt(1)) + + pipeline.runMatchingEngine(lotp, []Order{longOrder}, []Order{shortOrder}, map[common.Address]*big.Int{}, minAllowableMargin, takerFee, upperBound) + lotp.AssertNotCalled(t, "ExecuteMatchedOrdersTx", mock.Anything, mock.Anything, mock.Anything) + }) + t.Run("When longOrder.Price >= shortOrder.Price same", func(t *testing.T) { + t.Run("When long order and short order's unfulfilled quantity is same", func(t *testing.T) { + t.Run("When long order and short order's base asset quantity is same", func(t *testing.T) { + //Add 2 long orders + _, lotp, pipeline, _, _ := setupDependencies(t) + longOrders := make([]Order, 0) + longOrder1 := getLongOrder() + longOrders = append(longOrders, longOrder1) + longOrder2 := getLongOrder() + longOrders = append(longOrders, longOrder2) + + // Add 2 short orders + shortOrder1 := getShortOrder() + shortOrders := make([]Order, 0) + shortOrders = append(shortOrders, shortOrder1) + shortOrder2 := getShortOrder() + shortOrders = append(shortOrders, shortOrder2) + + fillAmount1 := longOrder1.BaseAssetQuantity + fillAmount2 := longOrder2.BaseAssetQuantity + marginMap := map[common.Address]*big.Int{ + trader: big.NewInt(0), // limit order doesn't need any available margin + } + lotp.On("ExecuteMatchedOrdersTx", longOrder1, shortOrder1, fillAmount1).Return(nil) + lotp.On("ExecuteMatchedOrdersTx", longOrder2, shortOrder2, fillAmount2).Return(nil) + pipeline.runMatchingEngine(lotp, longOrders, shortOrders, marginMap, minAllowableMargin, takerFee, upperBound) + lotp.AssertCalled(t, "ExecuteMatchedOrdersTx", longOrder1, shortOrder1, fillAmount1) + lotp.AssertCalled(t, "ExecuteMatchedOrdersTx", longOrder2, shortOrder2, fillAmount2) + }) + t.Run("When long order and short order's base asset quantity is different", func(t *testing.T) { + db, lotp, pipeline, _, _ := setupDependencies(t) + //Add 2 long orders with half base asset quantity of short order + longOrders := make([]Order, 0) + longOrder := getLongOrder() + longOrder.BaseAssetQuantity = big.NewInt(20) + longOrder.FilledBaseAssetQuantity = big.NewInt(5) + longOrders = append(longOrders, longOrder) + + // Add 2 short orders + shortOrder := getShortOrder() + shortOrder.BaseAssetQuantity = big.NewInt(-30) + shortOrder.FilledBaseAssetQuantity = big.NewInt(-15) + shortOrders := make([]Order, 0) + shortOrders = append(shortOrders, shortOrder) + + fillAmount := big.NewInt(0).Sub(longOrder.BaseAssetQuantity, longOrder.FilledBaseAssetQuantity) + db.On("GetLongOrders").Return(longOrders) + db.On("GetShortOrders").Return(shortOrders) + lotp.On("PurgeLocalTx").Return(nil) + lotp.On("ExecuteMatchedOrdersTx", longOrder, shortOrder, fillAmount).Return(nil) + marginMap := map[common.Address]*big.Int{ + trader: big.NewInt(0), // limit order doesn't need any available margin + } + pipeline.runMatchingEngine(lotp, longOrders, shortOrders, marginMap, minAllowableMargin, takerFee, upperBound) + lotp.AssertCalled(t, "ExecuteMatchedOrdersTx", longOrder, shortOrder, fillAmount) + }) + }) + t.Run("When long order and short order's unfulfilled quantity is not same", func(t *testing.T) { + db, lotp, pipeline, _, _ := setupDependencies(t) + longOrders := make([]Order, 0) + longOrder1 := getLongOrder() + longOrder1.BaseAssetQuantity = big.NewInt(20) + longOrder1.FilledBaseAssetQuantity = big.NewInt(5) + longOrder2 := getLongOrder() + longOrder2.BaseAssetQuantity = big.NewInt(40) + longOrder2.FilledBaseAssetQuantity = big.NewInt(0) + longOrder3 := getLongOrder() + longOrder3.BaseAssetQuantity = big.NewInt(10) + longOrder3.FilledBaseAssetQuantity = big.NewInt(3) + longOrders = append(longOrders, longOrder1, longOrder2, longOrder3) + + // Add 2 short orders + shortOrders := make([]Order, 0) + shortOrder1 := getShortOrder() + shortOrder1.BaseAssetQuantity = big.NewInt(-30) + shortOrder1.FilledBaseAssetQuantity = big.NewInt(-2) + shortOrder2 := getShortOrder() + shortOrder2.BaseAssetQuantity = big.NewInt(-50) + shortOrder2.FilledBaseAssetQuantity = big.NewInt(-20) + shortOrder3 := getShortOrder() + shortOrder3.BaseAssetQuantity = big.NewInt(-20) + shortOrder3.FilledBaseAssetQuantity = big.NewInt(-10) + shortOrders = append(shortOrders, shortOrder1, shortOrder2, shortOrder3) + + lotp.On("ExecuteMatchedOrdersTx", mock.Anything, mock.Anything, mock.Anything).Return(nil).Times(5) + + db.On("GetLongOrders").Return(longOrders) + db.On("GetShortOrders").Return(shortOrders) + lotp.On("PurgeLocalTx").Return(nil) + log.Info("longOrder1", "longOrder1", longOrder1) + marginMap := map[common.Address]*big.Int{ + trader: big.NewInt(0), // limit order doesn't need any available margin + } + pipeline.runMatchingEngine(lotp, longOrders, shortOrders, marginMap, minAllowableMargin, takerFee, upperBound) + log.Info("longOrder1", "longOrder1", longOrder1) + + //During 1st matching iteration + longOrder1UnfulfilledQuantity := longOrder1.GetUnFilledBaseAssetQuantity() + shortOrder1UnfulfilledQuantity := shortOrder1.GetUnFilledBaseAssetQuantity() + fillAmount := utils.BigIntMinAbs(longOrder1UnfulfilledQuantity, shortOrder1UnfulfilledQuantity) + lotp.AssertCalled(t, "ExecuteMatchedOrdersTx", longOrder1, shortOrder1, fillAmount) + //After 1st matching iteration longOrder1 has been matched fully but shortOrder1 has not + longOrder1.FilledBaseAssetQuantity.Add(longOrder1.FilledBaseAssetQuantity, fillAmount) + shortOrder1.FilledBaseAssetQuantity.Sub(shortOrder1.FilledBaseAssetQuantity, fillAmount) + + //During 2nd iteration longOrder2 with be matched with shortOrder1 + longOrder2UnfulfilledQuantity := longOrder2.GetUnFilledBaseAssetQuantity() + shortOrder1UnfulfilledQuantity = shortOrder1.GetUnFilledBaseAssetQuantity() + fillAmount = utils.BigIntMinAbs(longOrder2UnfulfilledQuantity, shortOrder1UnfulfilledQuantity) + lotp.AssertCalled(t, "ExecuteMatchedOrdersTx", longOrder2, shortOrder1, fillAmount) + //After 2nd matching iteration shortOrder1 has been matched fully but longOrder2 has not + longOrder2.FilledBaseAssetQuantity.Add(longOrder2.FilledBaseAssetQuantity, fillAmount) + shortOrder1.FilledBaseAssetQuantity.Sub(longOrder2.FilledBaseAssetQuantity, fillAmount) + + //During 3rd iteration longOrder2 with be matched with shortOrder2 + longOrder2UnfulfilledQuantity = longOrder2.GetUnFilledBaseAssetQuantity() + shortOrder2UnfulfilledQuantity := shortOrder2.GetUnFilledBaseAssetQuantity() + fillAmount = utils.BigIntMinAbs(longOrder2UnfulfilledQuantity, shortOrder2UnfulfilledQuantity) + lotp.AssertCalled(t, "ExecuteMatchedOrdersTx", longOrder2, shortOrder2, fillAmount) + //After 3rd matching iteration longOrder2 has been matched fully but shortOrder2 has not + longOrder2.FilledBaseAssetQuantity.Add(longOrder2.FilledBaseAssetQuantity, fillAmount) + shortOrder2.FilledBaseAssetQuantity.Sub(shortOrder2.FilledBaseAssetQuantity, fillAmount) + + //So during 4th iteration longOrder3 with be matched with shortOrder2 + longOrder3UnfulfilledQuantity := longOrder3.GetUnFilledBaseAssetQuantity() + shortOrder2UnfulfilledQuantity = shortOrder2.GetUnFilledBaseAssetQuantity() + fillAmount = utils.BigIntMinAbs(longOrder3UnfulfilledQuantity, shortOrder2UnfulfilledQuantity) + lotp.AssertCalled(t, "ExecuteMatchedOrdersTx", longOrder3, shortOrder2, fillAmount) + //After 4rd matching iteration shortOrder2 has been matched fully but longOrder3 has not + longOrder3.FilledBaseAssetQuantity.Add(longOrder3.FilledBaseAssetQuantity, fillAmount) + shortOrder2.FilledBaseAssetQuantity.Sub(shortOrder2.FilledBaseAssetQuantity, fillAmount) + + //So during 5th iteration longOrder3 with be matched with shortOrder3 + longOrder3UnfulfilledQuantity = longOrder3.GetUnFilledBaseAssetQuantity() + shortOrder3UnfulfilledQuantity := shortOrder3.GetUnFilledBaseAssetQuantity() + fillAmount = utils.BigIntMinAbs(longOrder3UnfulfilledQuantity, shortOrder3UnfulfilledQuantity) + lotp.AssertCalled(t, "ExecuteMatchedOrdersTx", longOrder3, shortOrder3, fillAmount) + }) + }) + }) +} + +func TestMatchLongAndShortOrder(t *testing.T) { + t.Run("When longPrice is less than shortPrice ,it returns orders unchanged and ordersMatched=false", func(t *testing.T) { + _, lotp, _, _, _ := setupDependencies(t) + longOrder := getLongOrder() + shortOrder := getShortOrder() + longOrder.Price.Sub(shortOrder.Price, big.NewInt(1)) + changedLongOrder, changedShortOrder, ordersMatched := matchLongAndShortOrder(lotp, longOrder, shortOrder) + lotp.AssertNotCalled(t, "ExecuteMatchedOrdersTx", mock.Anything, mock.Anything, mock.Anything) + assert.Equal(t, longOrder, changedLongOrder) + assert.Equal(t, shortOrder, changedShortOrder) + assert.Equal(t, false, ordersMatched) + }) + t.Run("When longPrice is >= shortPrice", func(t *testing.T) { + t.Run("When either longOrder or/and shortOrder is fully filled ", func(t *testing.T) { + t.Run("When longOrder is fully filled, it returns orders unchanged and ordersMatched=false", func(t *testing.T) { + _, lotp, _, _, _ := setupDependencies(t) + longOrder := getLongOrder() + longOrder.FilledBaseAssetQuantity = longOrder.BaseAssetQuantity + shortOrder := getShortOrder() + longOrder.Price.Add(shortOrder.Price, big.NewInt(1)) + changedLongOrder, changedShortOrder, ordersMatched := matchLongAndShortOrder(lotp, longOrder, shortOrder) + lotp.AssertNotCalled(t, "ExecuteMatchedOrdersTx", mock.Anything, mock.Anything, mock.Anything) + assert.Equal(t, longOrder, changedLongOrder) + assert.Equal(t, shortOrder, changedShortOrder) + assert.Equal(t, false, ordersMatched) + }) + t.Run("When shortOrder is fully filled, it returns orders unchanged and ordersMatched=false", func(t *testing.T) { + _, lotp, _, _, _ := setupDependencies(t) + longOrder := getLongOrder() + shortOrder := getShortOrder() + longOrder.Price.Add(shortOrder.Price, big.NewInt(1)) + shortOrder.FilledBaseAssetQuantity = shortOrder.BaseAssetQuantity + changedLongOrder, changedShortOrder, ordersMatched := matchLongAndShortOrder(lotp, longOrder, shortOrder) + lotp.AssertNotCalled(t, "ExecuteMatchedOrdersTx", mock.Anything, mock.Anything, mock.Anything) + assert.Equal(t, longOrder, changedLongOrder) + assert.Equal(t, shortOrder, changedShortOrder) + assert.Equal(t, false, ordersMatched) + }) + t.Run("When longOrder and shortOrder are fully filled, it returns orders unchanged and ordersMatched=false", func(t *testing.T) { + _, lotp, _, _, _ := setupDependencies(t) + longOrder := getLongOrder() + longOrder.FilledBaseAssetQuantity = longOrder.BaseAssetQuantity + shortOrder := getShortOrder() + longOrder.Price.Add(shortOrder.Price, big.NewInt(1)) + shortOrder.FilledBaseAssetQuantity = shortOrder.BaseAssetQuantity + changedLongOrder, changedShortOrder, ordersMatched := matchLongAndShortOrder(lotp, longOrder, shortOrder) + lotp.AssertNotCalled(t, "ExecuteMatchedOrdersTx", mock.Anything, mock.Anything, mock.Anything) + assert.Equal(t, longOrder, changedLongOrder) + assert.Equal(t, shortOrder, changedShortOrder) + assert.Equal(t, false, ordersMatched) + }) + }) + t.Run("when both long and short order are not fully filled", func(t *testing.T) { + t.Run("when unfilled is same for longOrder and shortOrder", func(t *testing.T) { + t.Run("When filled is zero for long and short order, it returns fully filled longOrder and shortOrder and ordersMatched=true", func(t *testing.T) { + _, lotp, _, _, _ := setupDependencies(t) + longOrder := getLongOrder() + longOrder.FilledBaseAssetQuantity = big.NewInt(0) + shortOrder := getShortOrder() + longOrder.Price.Add(shortOrder.Price, big.NewInt(1)) + shortOrder.FilledBaseAssetQuantity = big.NewInt(0) + shortOrder.BaseAssetQuantity = big.NewInt(0).Neg(longOrder.BaseAssetQuantity) + + expectedFillAmount := longOrder.BaseAssetQuantity + lotp.On("ExecuteMatchedOrdersTx", longOrder, shortOrder, expectedFillAmount).Return(nil) + + changedLongOrder, changedShortOrder, ordersMatched := matchLongAndShortOrder(lotp, longOrder, shortOrder) + lotp.AssertCalled(t, "ExecuteMatchedOrdersTx", longOrder, shortOrder, expectedFillAmount) + + //setting this to test if returned order is same as original except for FilledBaseAssetQuantity + longOrder.FilledBaseAssetQuantity = longOrder.BaseAssetQuantity + shortOrder.FilledBaseAssetQuantity = shortOrder.BaseAssetQuantity + assert.Equal(t, longOrder, changedLongOrder) + assert.Equal(t, shortOrder, changedShortOrder) + assert.Equal(t, true, ordersMatched) + }) + t.Run("When filled is non zero for long and short order, it returns fully filled longOrder and shortOrder and ordersMatched=true", func(t *testing.T) { + _, lotp, _, _, _ := setupDependencies(t) + longOrder := getLongOrder() + longOrder.BaseAssetQuantity = big.NewInt(20) + longOrder.FilledBaseAssetQuantity = big.NewInt(5) + shortOrder := getShortOrder() + longOrder.Price.Add(shortOrder.Price, big.NewInt(1)) + shortOrder.BaseAssetQuantity = big.NewInt(-30) + shortOrder.FilledBaseAssetQuantity = big.NewInt(-15) + + expectedFillAmount := big.NewInt(0).Sub(longOrder.BaseAssetQuantity, longOrder.FilledBaseAssetQuantity) + lotp.On("ExecuteMatchedOrdersTx", longOrder, shortOrder, expectedFillAmount).Return(nil) + changedLongOrder, changedShortOrder, ordersMatched := matchLongAndShortOrder(lotp, longOrder, shortOrder) + + lotp.AssertCalled(t, "ExecuteMatchedOrdersTx", longOrder, shortOrder, expectedFillAmount) + //setting this to test if returned order is same as original except for FilledBaseAssetQuantity + longOrder.FilledBaseAssetQuantity = longOrder.BaseAssetQuantity + shortOrder.FilledBaseAssetQuantity = shortOrder.BaseAssetQuantity + assert.Equal(t, longOrder, changedLongOrder) + assert.Equal(t, shortOrder, changedShortOrder) + assert.Equal(t, true, ordersMatched) + }) + }) + t.Run("when unfilled(amount x) is less for longOrder, it returns fully filled longOrder and adds fillAmount(x) to shortOrder with and ordersMatched=true", func(t *testing.T) { + _, lotp, _, _, _ := setupDependencies(t) + longOrder := getLongOrder() + longOrder.BaseAssetQuantity = big.NewInt(20) + longOrder.FilledBaseAssetQuantity = big.NewInt(15) + shortOrder := getShortOrder() + longOrder.Price.Add(shortOrder.Price, big.NewInt(1)) + shortOrder.BaseAssetQuantity = big.NewInt(-30) + shortOrder.FilledBaseAssetQuantity = big.NewInt(-15) + + expectedFillAmount := big.NewInt(0).Sub(longOrder.BaseAssetQuantity, longOrder.FilledBaseAssetQuantity) + lotp.On("ExecuteMatchedOrdersTx", longOrder, shortOrder, expectedFillAmount).Return(nil) + changedLongOrder, changedShortOrder, ordersMatched := matchLongAndShortOrder(lotp, longOrder, shortOrder) + lotp.AssertCalled(t, "ExecuteMatchedOrdersTx", longOrder, shortOrder, expectedFillAmount) + + expectedShortOrderFilled := big.NewInt(0).Sub(shortOrder.FilledBaseAssetQuantity, expectedFillAmount) + //setting this to test if returned order is same as original except for FilledBaseAssetQuantity + longOrder.FilledBaseAssetQuantity = longOrder.BaseAssetQuantity + shortOrder.FilledBaseAssetQuantity = expectedShortOrderFilled + assert.Equal(t, longOrder, changedLongOrder) + assert.Equal(t, shortOrder, changedShortOrder) + assert.Equal(t, true, ordersMatched) + }) + t.Run("when unfilled(amount x) is less for shortOrder, it returns fully filled shortOrder and adds fillAmount(x) to longOrder and ordersMatched=true", func(t *testing.T) { + _, lotp, _, _, _ := setupDependencies(t) + longOrder := getLongOrder() + longOrder.BaseAssetQuantity = big.NewInt(20) + longOrder.FilledBaseAssetQuantity = big.NewInt(5) + shortOrder := getShortOrder() + longOrder.Price.Add(shortOrder.Price, big.NewInt(1)) + shortOrder.BaseAssetQuantity = big.NewInt(-30) + shortOrder.FilledBaseAssetQuantity = big.NewInt(-25) + + expectedFillAmount := big.NewInt(0).Neg(big.NewInt(0).Sub(shortOrder.BaseAssetQuantity, shortOrder.FilledBaseAssetQuantity)) + lotp.On("ExecuteMatchedOrdersTx", longOrder, shortOrder, expectedFillAmount).Return(nil) + changedLongOrder, changedShortOrder, ordersMatched := matchLongAndShortOrder(lotp, longOrder, shortOrder) + lotp.AssertCalled(t, "ExecuteMatchedOrdersTx", longOrder, shortOrder, expectedFillAmount) + + expectedLongOrderFilled := big.NewInt(0).Add(longOrder.FilledBaseAssetQuantity, expectedFillAmount) + //setting this to test if returned order is same as original except for FilledBaseAssetQuantity + longOrder.FilledBaseAssetQuantity = expectedLongOrderFilled + shortOrder.FilledBaseAssetQuantity = shortOrder.BaseAssetQuantity + assert.Equal(t, longOrder, changedLongOrder) + assert.Equal(t, shortOrder, changedShortOrder) + assert.Equal(t, true, ordersMatched) + }) + }) + }) +} + +func TestAreMatchingOrders(t *testing.T) { + minAllowableMargin := big.NewInt(1e6) + takerFee := big.NewInt(1e6) + upperBound := big.NewInt(22) + + trader := common.HexToAddress("0x22Bb736b64A0b4D4081E103f83bccF864F0404aa") + longOrder_ := Order{ + Market: 1, + PositionType: LONG, + BaseAssetQuantity: hu.Mul1e18(big.NewInt(10)), + Trader: trader, + FilledBaseAssetQuantity: big.NewInt(0), + Salt: big.NewInt(1), + Price: hu.Mul1e6(big.NewInt(100)), + ReduceOnly: false, + LifecycleList: []Lifecycle{Lifecycle{}}, + BlockNumber: big.NewInt(21), + RawOrder: &LimitOrder{ + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(1), + Trader: trader, + BaseAssetQuantity: hu.Mul1e18(big.NewInt(10)), + Price: hu.Mul1e6(big.NewInt(100)), + Salt: big.NewInt(1), + ReduceOnly: false, + }, + PostOnly: false, + }, + OrderType: Limit, + } + + shortTrader := common.HexToAddress("0xc413Fa79AdE66224F560BD7693F8bEc81746Bf92") + shortOrder_ := Order{ + Market: 1, + PositionType: SHORT, + BaseAssetQuantity: hu.Mul1e18(big.NewInt(-10)), + Trader: shortTrader, + FilledBaseAssetQuantity: big.NewInt(0), + Salt: big.NewInt(2), + Price: hu.Mul1e6(big.NewInt(100)), + ReduceOnly: false, + LifecycleList: []Lifecycle{Lifecycle{}}, + BlockNumber: big.NewInt(21), + RawOrder: &LimitOrder{ + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(1), + Trader: trader, + BaseAssetQuantity: hu.Mul1e18(big.NewInt(-10)), + Price: hu.Mul1e6(big.NewInt(100)), + Salt: big.NewInt(2), + ReduceOnly: false, + }, + PostOnly: false, + }, + OrderType: Limit, + } + + t.Run("longOrder's price < shortOrder's price", func(t *testing.T) { + longOrder := deepCopyOrder(&longOrder_) + shortOrder := deepCopyOrder(&shortOrder_) + + longOrder.Price = big.NewInt(80) + marginMap := map[common.Address]*big.Int{ + trader: big.NewInt(0), // limit order doesn't need any available margin + } + actualFillAmount, err := areMatchingOrders(longOrder, shortOrder, marginMap, minAllowableMargin, takerFee, upperBound) + assert.EqualError(t, err, fmt.Errorf("long order price %s is less than short order price %s", longOrder.Price.String(), shortOrder.Price.String()).Error()) + assert.Nil(t, actualFillAmount) + }) + + t.Run("longOrder was placed first", func(t *testing.T) { + longOrder := deepCopyOrder(&longOrder_) + shortOrder := deepCopyOrder(&shortOrder_) + longOrder.BlockNumber = big.NewInt(20) + shortOrder.BlockNumber = big.NewInt(21) + t.Run("longOrder is IOC", func(t *testing.T) { + longOrder.OrderType = IOC + rawOrder := longOrder.RawOrder.(*LimitOrder) + longOrder.RawOrder = &IOCOrder{ + BaseOrder: rawOrder.BaseOrder, + OrderType: 1, + ExpireAt: big.NewInt(0), + } + marginMap := map[common.Address]*big.Int{ + trader: big.NewInt(0), // limit order doesn't need any available margin + } + actualFillAmount, err := areMatchingOrders(longOrder, shortOrder, marginMap, minAllowableMargin, takerFee, upperBound) + assert.EqualError(t, err, fmt.Errorf("resting order semantics mismatch").Error()) + assert.Nil(t, actualFillAmount) + }) + t.Run("short order is post only", func(t *testing.T) { + longOrder := deepCopyOrder(&longOrder_) + longOrder.BlockNumber = big.NewInt(20) + + shortOrder.RawOrder.(*LimitOrder).PostOnly = true + marginMap := map[common.Address]*big.Int{ + trader: big.NewInt(0), // limit order doesn't need any available margin + } + actualFillAmount, err := areMatchingOrders(longOrder, shortOrder, marginMap, minAllowableMargin, takerFee, upperBound) + assert.EqualError(t, err, fmt.Errorf("resting order semantics mismatch").Error()) + assert.Nil(t, actualFillAmount) + }) + }) + + t.Run("shortOrder was placed first", func(t *testing.T) { + longOrder := deepCopyOrder(&longOrder_) + shortOrder := deepCopyOrder(&shortOrder_) + longOrder.BlockNumber = big.NewInt(21) + shortOrder.BlockNumber = big.NewInt(20) + t.Run("shortOrder is IOC", func(t *testing.T) { + shortOrder.OrderType = IOC + rawOrder := shortOrder.RawOrder.(*LimitOrder) + shortOrder.RawOrder = &IOCOrder{ + BaseOrder: rawOrder.BaseOrder, + OrderType: 1, + ExpireAt: big.NewInt(0), + } + marginMap := map[common.Address]*big.Int{ + trader: big.NewInt(0), // limit order doesn't need any available margin + } + actualFillAmount, err := areMatchingOrders(longOrder, shortOrder, marginMap, minAllowableMargin, takerFee, upperBound) + assert.EqualError(t, err, fmt.Errorf("resting order semantics mismatch").Error()) + assert.Nil(t, actualFillAmount) + }) + t.Run("longOrder is post only", func(t *testing.T) { + longOrder := deepCopyOrder(&longOrder_) + longOrder.BlockNumber = big.NewInt(21) + + longOrder.RawOrder.(*LimitOrder).PostOnly = true + marginMap := map[common.Address]*big.Int{ + trader: big.NewInt(0), // limit order doesn't need any available margin + } + actualFillAmount, err := areMatchingOrders(longOrder, shortOrder, marginMap, minAllowableMargin, takerFee, upperBound) + assert.EqualError(t, err, fmt.Errorf("resting order semantics mismatch").Error()) + assert.Nil(t, actualFillAmount) + }) + }) + + t.Run("one of the orders is fully filled", func(t *testing.T) { + longOrder := deepCopyOrder(&longOrder_) + shortOrder := deepCopyOrder(&shortOrder_) + + longOrder.FilledBaseAssetQuantity = longOrder.BaseAssetQuantity + marginMap := map[common.Address]*big.Int{ + trader: big.NewInt(0), // limit order doesn't need any available margin + } + actualFillAmount, err := areMatchingOrders(longOrder, shortOrder, marginMap, minAllowableMargin, takerFee, upperBound) + assert.EqualError(t, err, fmt.Errorf("no fill amount").Error()) + assert.Nil(t, actualFillAmount) + }) + + t.Run("success", func(t *testing.T) { + longOrder := deepCopyOrder(&longOrder_) + shortOrder := deepCopyOrder(&shortOrder_) + + longOrder.FilledBaseAssetQuantity = hu.Mul1e18(big.NewInt(5)) + marginMap := map[common.Address]*big.Int{ + trader: big.NewInt(0), + shortTrader: big.NewInt(0), + } + actualFillAmount, err := areMatchingOrders(longOrder, shortOrder, marginMap, minAllowableMargin, takerFee, upperBound) + assert.Nil(t, err) + assert.Equal(t, hu.Mul1e18(big.NewInt(5)), actualFillAmount) + }) + + t.Run("test ioc/signed orders", func(t *testing.T) { + t.Run("long trader has insufficient margin", func(t *testing.T) { + longOrder := deepCopyOrder(&longOrder_) // longOrder_ has block 21 + longOrder.OrderType = IOC + + shortOrder := deepCopyOrder(&shortOrder_) // shortOrder_ has block 2 + shortOrder.OrderType = Signed + + expectedFillAmount := longOrder.BaseAssetQuantity + marginMap := map[common.Address]*big.Int{ + trader: big.NewInt(0), + shortTrader: big.NewInt(0), + } + actualFillAmount, err := areMatchingOrders(longOrder, shortOrder, marginMap, minAllowableMargin, takerFee, upperBound) + requiredMargin := hu.GetRequiredMargin(longOrder.Price, expectedFillAmount, minAllowableMargin, takerFee) + assert.EqualError(t, err, fmt.Errorf("insufficient margin. trader %s, required: %s, available: %s", trader, requiredMargin, big.NewInt(0)).Error()) + assert.Nil(t, actualFillAmount) + }) + + t.Run("short trader has insufficient margin", func(t *testing.T) { + longOrder := deepCopyOrder(&longOrder_) // longOrder_ has block 21 + longOrder.OrderType = IOC + + shortOrder := deepCopyOrder(&shortOrder_) // shortOrder_ has block 2 + shortOrder.OrderType = Signed + + expectedFillAmount := longOrder.BaseAssetQuantity + longRequiredMargin := hu.GetRequiredMargin(longOrder.Price, expectedFillAmount, minAllowableMargin, takerFee) + shortRequiredMargin := hu.GetRequiredMargin(shortOrder.Price, expectedFillAmount, minAllowableMargin, big.NewInt(0)) + marginMap := map[common.Address]*big.Int{ + trader: longRequiredMargin, + shortTrader: big.NewInt(0), + } + actualFillAmount, err := areMatchingOrders(longOrder, shortOrder, marginMap, minAllowableMargin, takerFee, upperBound) + assert.EqualError(t, err, fmt.Errorf("insufficient margin. trader %s, required: %s, available: %s", shortTrader, shortRequiredMargin, big.NewInt(0)).Error()) + assert.Nil(t, actualFillAmount) + }) + + t.Run("[success] match ioc order with signed order", func(t *testing.T) { + longOrder := deepCopyOrder(&longOrder_) // longOrder_ has block 21 + longOrder.OrderType = IOC + + shortOrder := deepCopyOrder(&shortOrder_) // shortOrder_ has block 2 + shortOrder.OrderType = Signed + + longOrder.FilledBaseAssetQuantity = hu.Mul1e18(big.NewInt(4)) + expectedFillAmount := hu.Mul1e18(big.NewInt(6)) + longRequiredMargin := hu.GetRequiredMargin(longOrder.Price, expectedFillAmount, minAllowableMargin, takerFee) + shortRequiredMargin := hu.GetRequiredMargin(shortOrder.Price, expectedFillAmount, minAllowableMargin, big.NewInt(0)) + marginMap := map[common.Address]*big.Int{ + trader: longRequiredMargin, + shortTrader: shortRequiredMargin, + } + actualFillAmount, err := areMatchingOrders(longOrder, shortOrder, marginMap, minAllowableMargin, takerFee, upperBound) + assert.Nil(t, err) + assert.Equal(t, expectedFillAmount, actualFillAmount) + }) + }) +} + +func getShortOrder() Order { + salt := big.NewInt(time.Now().Unix()) + shortOrder := createLimitOrder(SHORT, "0x22Bb736b64A0b4D4081E103f83bccF864F0404aa", big.NewInt(-10), big.NewInt(20.0), Placed, big.NewInt(2), salt) + return shortOrder +} + +func getLongOrder() Order { + salt := big.NewInt(time.Now().Unix()) + longOrder := createLimitOrder(LONG, "0x22Bb736b64A0b4D4081E103f83bccF864F0404aa", big.NewInt(10), big.NewInt(20.0), Placed, big.NewInt(2), salt) + return longOrder +} + +func buildLongOrder(price, q int64) Order { + salt := big.NewInt(time.Now().Unix()) + longOrder := createLimitOrder(LONG, "0x22Bb736b64A0b4D4081E103f83bccF864F0404aa", big.NewInt(q), big.NewInt(price), Placed, big.NewInt(2), salt) + return longOrder +} + +func buildShortOrder(price, q int64) Order { + salt := big.NewInt(time.Now().Unix()) + order := createLimitOrder(SHORT, "0x22Bb736b64A0b4D4081E103f83bccF864F0404aa", big.NewInt(q), big.NewInt(price), Placed, big.NewInt(2), salt) + return order +} + +func getLiquidablePos(address common.Address, posType PositionType, size int64) LiquidablePosition { + return LiquidablePosition{ + Address: address, + Market: market, + PositionType: posType, + Size: big.NewInt(size), + FilledSize: big.NewInt(0), + } +} + +func setupDependencies(t *testing.T) (*MockLimitOrderDatabase, *MockLimitOrderTxProcessor, *MatchingPipeline, map[Market]*big.Int, *MockConfigService) { + db := NewMockLimitOrderDatabase() + lotp := NewMockLimitOrderTxProcessor() + cs := NewMockConfigService() + pipeline := NewMatchingPipeline(db, lotp, cs) + underlyingPrices := make(map[Market]*big.Int) + underlyingPrices[market] = big.NewInt(20.0) + return db, lotp, pipeline, underlyingPrices, cs +} diff --git a/plugin/evm/orderbook/memory_database.go b/plugin/evm/orderbook/memory_database.go new file mode 100644 index 0000000000..c7e412c079 --- /dev/null +++ b/plugin/evm/orderbook/memory_database.go @@ -0,0 +1,1422 @@ +package orderbook + +import ( + "bytes" + "encoding/gob" + "encoding/json" + "fmt" + "math/big" + "sort" + "sync" + "time" + + "github.com/ava-labs/subnet-evm/metrics" + hu "github.com/ava-labs/subnet-evm/plugin/evm/orderbook/hubbleutils" + "github.com/ava-labs/subnet-evm/utils" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" +) + +type InMemoryDatabase struct { + mu *sync.RWMutex `json:"-"` + Orders map[common.Hash]*Order `json:"order_map"` // ID => order + LongOrders map[Market][]*Order `json:"long_orders"` + ShortOrders map[Market][]*Order `json:"short_orders"` + TraderMap map[common.Address]*Trader `json:"trader_map"` // address => trader info + NextFundingTime uint64 `json:"next_funding_time"` + LastPrice map[Market]*big.Int `json:"last_price"` + CumulativePremiumFraction map[Market]*big.Int `json:"cumulative_last_premium_fraction"` + NextSamplePITime uint64 `json:"next_sample_pi_time"` + SamplePIAttemptedTime uint64 `json:"sample_pi_attempted_time"` + configService IConfigService `json:"-"` +} + +func NewInMemoryDatabase(configService IConfigService) *InMemoryDatabase { + lastPrice := map[Market]*big.Int{} + traderMap := map[common.Address]*Trader{} + + return &InMemoryDatabase{ + Orders: map[common.Hash]*Order{}, + LongOrders: map[Market][]*Order{}, + ShortOrders: map[Market][]*Order{}, + TraderMap: traderMap, + LastPrice: lastPrice, + CumulativePremiumFraction: map[Market]*big.Int{}, + mu: &sync.RWMutex{}, + configService: configService, + } +} + +const ( + RETRY_AFTER_BLOCKS = 10 +) + +type Market = hu.Market + +type Collateral = int + +const ( + HUSD Collateral = iota +) + +type PositionType int + +const ( + LONG PositionType = iota + SHORT +) + +func (p PositionType) String() string { + return [...]string{"long", "short"}[p] +} + +type Status uint8 + +const ( + Placed Status = iota + FulFilled + Cancelled + Execution_Failed +) + +type OrderType = hu.OrderType + +const ( + Limit = hu.Limit + IOC = hu.IOC + Signed = hu.Signed +) + +type Lifecycle struct { + BlockNumber uint64 + Status Status + Info string +} + +type Order struct { + Id common.Hash + Market Market + PositionType PositionType + Trader common.Address + BaseAssetQuantity *big.Int + FilledBaseAssetQuantity *big.Int + Salt *big.Int + Price *big.Int + ReduceOnly bool + LifecycleList []Lifecycle + BlockNumber *big.Int // block number order was placed on + RawOrder ContractOrder `json:"-"` + OrderType OrderType +} + +func (order *Order) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + Market Market `json:"market"` + PositionType string `json:"position_type"` + Trader string `json:"trader"` + BaseAssetQuantity string `json:"base_asset_quantity"` + FilledBaseAssetQuantity string `json:"filled_base_asset_quantity"` + Salt string `json:"salt"` + Price string `json:"price"` + LifecycleList []Lifecycle `json:"lifecycle_list"` + BlockNumber uint64 `json:"block_number"` // block number order was placed on + ReduceOnly bool `json:"reduce_only"` + OrderType string `json:"order_type"` + }{ + Market: order.Market, + PositionType: order.PositionType.String(), + Trader: order.Trader.String(), + BaseAssetQuantity: order.BaseAssetQuantity.String(), + FilledBaseAssetQuantity: order.FilledBaseAssetQuantity.String(), + Salt: order.Salt.String(), + Price: order.Price.String(), + LifecycleList: order.LifecycleList, + BlockNumber: order.BlockNumber.Uint64(), + ReduceOnly: order.ReduceOnly, + OrderType: order.OrderType.String(), + }) +} + +func (order Order) GetUnFilledBaseAssetQuantity() *big.Int { + return big.NewInt(0).Sub(order.BaseAssetQuantity, order.FilledBaseAssetQuantity) +} + +func (order Order) getOrderStatus() Lifecycle { + lifecycle := order.LifecycleList + return lifecycle[len(lifecycle)-1] +} + +func (order Order) getExpireAt() *big.Int { + if order.OrderType == IOC { + return order.RawOrder.(*IOCOrder).ExpireAt + } + if order.OrderType == Signed { + return order.RawOrder.(*hu.SignedOrder).ExpireAt + } + return big.NewInt(0) +} + +func (order Order) isPostOnly() bool { + if order.OrderType == Limit { + if rawOrder, ok := order.RawOrder.(*LimitOrder); ok { + return rawOrder.PostOnly + } + } + if order.OrderType == Signed { + if rawOrder, ok := order.RawOrder.(*hu.SignedOrder); ok { + return rawOrder.PostOnly + } + } + return false +} + +func (order Order) String() string { + t := time.Unix(order.getExpireAt().Int64(), 0) + return fmt.Sprintf("Order: Id: %s, OrderType: %s, Market: %v, PositionType: %v, UserAddress: %v, BaseAssetQuantity: %s, FilledBaseAssetQuantity: %s, Salt: %v, Price: %s, ReduceOnly: %v, PostOnly: %v, expireAt %s, BlockNumber: %s", order.Id, order.OrderType, order.Market, order.PositionType, order.Trader.String(), prettifyScaledBigInt(order.BaseAssetQuantity, 18), prettifyScaledBigInt(order.FilledBaseAssetQuantity, 18), order.Salt, prettifyScaledBigInt(order.Price, 6), order.ReduceOnly, order.isPostOnly(), t.UTC(), order.BlockNumber) +} + +func (order Order) ToOrderMin() OrderMin { + return OrderMin{ + Market: order.Market, + Price: order.Price.String(), + Size: order.GetUnFilledBaseAssetQuantity().String(), + Signer: order.Trader.String(), + OrderId: order.Id.String(), + } +} + +type Position struct { + hu.Position + UnrealisedFunding *big.Int `json:"unrealised_funding"` + LastPremiumFraction *big.Int `json:"last_premium_fraction"` + LiquidationThreshold *big.Int `json:"liquidation_threshold"` +} + +func (p *Position) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + OpenNotional string `json:"open_notional"` + Size string `json:"size"` + UnrealisedFunding string `json:"unrealised_funding"` + LastPremiumFraction string `json:"last_premium_fraction"` + LiquidationThreshold string `json:"liquidation_threshold"` + }{ + OpenNotional: p.OpenNotional.String(), + Size: p.Size.String(), + UnrealisedFunding: p.UnrealisedFunding.String(), + LastPremiumFraction: p.LastPremiumFraction.String(), + LiquidationThreshold: p.LiquidationThreshold.String(), + }) +} + +type Margin struct { + Available *big.Int `json:"available"` + Deposited map[Collateral]*big.Int `json:"deposited"` + Reserved *big.Int `json:"reserved"` + VirtualReserved *big.Int `json:"virtual"` +} + +type Trader struct { + Positions map[Market]*Position `json:"positions"` // position for every market + Margin Margin `json:"margin"` // available margin/balance for every market +} + +type LimitOrderDatabase interface { + LoadFromSnapshot(snapshot Snapshot) error + GetAllOrders() []Order + GetMarketOrders(market Market) []Order + Add(order *Order) + AddSignedOrder(order *Order, requiredMargin *big.Int) + Delete(orderId common.Hash) + UpdateFilledBaseAssetQuantity(quantity *big.Int, orderId common.Hash, blockNumber uint64) + GetLongOrders(market Market, lowerbound *big.Int, blockNumber *big.Int) []Order + GetShortOrders(market Market, upperbound *big.Int, blockNumber *big.Int) []Order + UpdatePosition(trader common.Address, market Market, size *big.Int, openNotional *big.Int, isLiquidation bool, blockNumber uint64) + UpdateMargin(trader common.Address, collateral Collateral, addAmount *big.Int) + UpdateReservedMargin(trader common.Address, addAmount *big.Int) + UpdateUnrealisedFunding(market Market, cumulativePremiumFraction *big.Int) + ResetUnrealisedFunding(market Market, trader common.Address, cumulativePremiumFraction *big.Int) + UpdateNextFundingTime(nextFundingTime uint64) + GetNextFundingTime() uint64 + UpdateNextSamplePITime(nextSamplePITime uint64) + GetNextSamplePITime() uint64 + GetSamplePIAttemptedTime() uint64 + SignalSamplePIAttempted(time uint64) + UpdateLastPrice(market Market, lastPrice *big.Int) + GetLastPrices() map[Market]*big.Int + GetAllTraders() map[common.Address]Trader + GetOrderBookData() InMemoryDatabase + GetOrderBookDataCopy() (*InMemoryDatabase, error) + Accept(acceptedBlockNumber uint64, blockTimestamp uint64) + SetOrderStatus(orderId common.Hash, status Status, info string, blockNumber uint64) error + RevertLastStatus(orderId common.Hash) error + GetNaughtyTraders(hState *hu.HubbleState) ([]LiquidablePosition, map[common.Address][]Order, map[common.Address]*big.Int) + GetAllOpenOrdersForTrader(trader common.Address) []Order + GetOpenOrdersForTraderByType(trader common.Address, orderType OrderType) []Order + UpdateLastPremiumFraction(market Market, trader common.Address, lastPremiumFraction *big.Int, cumlastPremiumFraction *big.Int) + GetOrderById(orderId common.Hash) *Order + GetTraderInfo(trader common.Address) *Trader + GetOrderValidationFields(orderId common.Hash, order *hu.SignedOrder) OrderValidationFields + SampleImpactPrice() (impactBids, impactAsks, midPrices []*big.Int) + RemoveExpiredSignedOrders() + GetMarginAvailableForMakerbook(trader common.Address, prices map[int]*big.Int) *big.Int +} + +type Snapshot struct { + Data *InMemoryDatabase + AcceptedBlockNumber *big.Int // data includes this block number too +} + +func (db *InMemoryDatabase) LoadFromSnapshot(snapshot Snapshot) error { + db.mu.Lock() + defer db.mu.Unlock() + + if snapshot.Data == nil || snapshot.Data.Orders == nil || snapshot.Data.TraderMap == nil || snapshot.Data.LastPrice == nil || + snapshot.Data.CumulativePremiumFraction == nil { + return fmt.Errorf("invalid snapshot; snapshot=%+v", snapshot) + } + + db.Orders = snapshot.Data.Orders + db.TraderMap = snapshot.Data.TraderMap + db.LastPrice = snapshot.Data.LastPrice + db.NextFundingTime = snapshot.Data.NextFundingTime + db.NextSamplePITime = snapshot.Data.NextSamplePITime + db.CumulativePremiumFraction = snapshot.Data.CumulativePremiumFraction + + for _, order := range db.Orders { + db.AddInSortedArray(order) + } + return nil +} + +func (db *InMemoryDatabase) Accept(acceptedBlockNumber, blockTimestamp uint64) { + db.mu.Lock() + defer db.mu.Unlock() + + log.Info("Accept", "acceptedBlockNumber", acceptedBlockNumber, "blockTimestamp", blockTimestamp) + // SUNSET: this will work with 0 markets + count := db.configService.GetActiveMarketsCount() + for m := int64(0); m < count; m++ { + longOrders := db.getLongOrdersWithoutLock(Market(m), nil, nil, false) + shortOrders := db.getShortOrdersWithoutLock(Market(m), nil, nil, false) + + for _, longOrder := range longOrders { + if shouldRemove(acceptedBlockNumber, blockTimestamp, longOrder) == REMOVE { + db.deleteOrderWithoutLock(longOrder.Id) + } + } + + for _, shortOrder := range shortOrders { + if shouldRemove(acceptedBlockNumber, blockTimestamp, shortOrder) == REMOVE { + db.deleteOrderWithoutLock(shortOrder.Id) + } + } + } +} + +type OrderStatus uint8 + +const ( + KEEP OrderStatus = iota + REMOVE +) + +func shouldRemove(acceptedBlockNumber, blockTimestamp uint64, order Order) OrderStatus { + // check if there is any criteria to delete the order + // 1. Order is fulfilled or cancelled + lifecycle := order.getOrderStatus() + if (lifecycle.Status == FulFilled || lifecycle.Status == Cancelled) && lifecycle.BlockNumber <= acceptedBlockNumber { + return REMOVE + } + + if order.OrderType == Limit { + return KEEP + } + + // do not remove expired signed order here. They should be removed from + // RemoveExpiredSignedOrders function only so that the appropriate Trader event is sent + if order.OrderType == Signed { + return KEEP + } + + // remove if order is expired; valid for both IOC and Signed orders + expireAt := order.getExpireAt() + if expireAt.Sign() > 0 && expireAt.Int64() < int64(blockTimestamp) { + return REMOVE + } + return KEEP +} + +func (db *InMemoryDatabase) RemoveExpiredSignedOrders() { + db.mu.Lock() + defer db.mu.Unlock() + + now := time.Now().Unix() + for _, order := range db.Orders { + if order.OrderType == Signed && order.getExpireAt().Int64() <= now { + db.deleteOrderWithoutLock(order.Id) + + // send TraderEvent for the expired order + go func(order_ *Order) { + traderEvent := TraderEvent{ + Trader: order_.Trader, + Removed: false, + EventName: "OrderExpired", + BlockStatus: ConfirmationLevelHead, + OrderId: order_.Id, + OrderType: order_.OrderType.String(), + Timestamp: big.NewInt(now), + } + + traderFeed.Send(traderEvent) + traderEvent.BlockStatus = ConfirmationLevelAccepted + traderFeed.Send(traderEvent) + }(order) + + } + } +} + +func (db *InMemoryDatabase) SetOrderStatus(orderId common.Hash, status Status, info string, blockNumber uint64) error { + db.mu.Lock() + defer db.mu.Unlock() + + if db.Orders[orderId] == nil { + return fmt.Errorf("invalid orderId %s", orderId.Hex()) + } + db.Orders[orderId].LifecycleList = append(db.Orders[orderId].LifecycleList, Lifecycle{blockNumber, status, info}) + return nil +} + +func (db *InMemoryDatabase) RevertLastStatus(orderId common.Hash) error { + db.mu.Lock() + defer db.mu.Unlock() + + if db.Orders[orderId] == nil { + return fmt.Errorf("invalid orderId %s", orderId.Hex()) + } + + lifeCycleList := db.Orders[orderId].LifecycleList + if len(lifeCycleList) > 0 { + db.Orders[orderId].LifecycleList = lifeCycleList[:len(lifeCycleList)-1] + } + return nil +} + +func (db *InMemoryDatabase) GetAllOrders() []Order { + db.mu.RLock() // only read lock required + defer db.mu.RUnlock() + + allOrders := []Order{} + for _, order := range db.Orders { + allOrders = append(allOrders, deepCopyOrder(order)) + } + return allOrders +} + +func (db *InMemoryDatabase) GetMarketOrders(market Market) []Order { + db.mu.RLock() // only read lock required + defer db.mu.RUnlock() + + allOrders := []Order{} + for _, order := range db.LongOrders[market] { + allOrders = append(allOrders, deepCopyOrder(order)) + } + + for _, order := range db.ShortOrders[market] { + allOrders = append(allOrders, deepCopyOrder(order)) + } + + return allOrders +} + +func (db *InMemoryDatabase) Add(order *Order) { + if order.OrderType != Limit && order.OrderType != IOC { + log.Error("In Add - order type is not Limit or IOC", "order", order) + return + } + db.mu.Lock() + defer db.mu.Unlock() + db.addOrderWithoutLock(order) +} + +func (db *InMemoryDatabase) AddSignedOrder(order *Order, requiredMargin *big.Int) { + if order.OrderType != Signed { + log.Error("In AddSignedOrder - order type is not Signed", "order", order) + return + } + log.Info("SignedOrder/OrderAccepted", "order", order) + + db.mu.Lock() + defer db.mu.Unlock() + db.addOrderWithoutLock(order) + db.updateVirtualReservedMargin(order.Trader, requiredMargin) +} + +func (db *InMemoryDatabase) addOrderWithoutLock(order *Order) { + order.LifecycleList = append(order.LifecycleList, Lifecycle{order.BlockNumber.Uint64(), Placed, ""}) + db.AddInSortedArray(order) + db.Orders[order.Id] = order +} + +// caller is expected to acquire db.mu before calling this function +func (db *InMemoryDatabase) AddInSortedArray(order *Order) { + market := order.Market + + var orders []*Order + var position int + if order.PositionType == LONG { + orders = db.LongOrders[market] + position = sort.Search(len(orders), func(i int) bool { + priceDiff := order.Price.Cmp(orders[i].Price) + if priceDiff == 1 { + return true + } else if priceDiff == 0 { + blockDiff := order.BlockNumber.Cmp(orders[i].BlockNumber) + if blockDiff == -1 { // order was placed before i + return true + } else if blockDiff == 0 { // order and i were placed in the same block + if order.OrderType == IOC { + // prioritize fulfilling IOC orders first, because they are short-lived + return true + } + } + } + return false + }) + } else { + orders = db.ShortOrders[market] + position = sort.Search(len(orders), func(i int) bool { + priceDiff := order.Price.Cmp(orders[i].Price) + if priceDiff == -1 { + return true + } else if priceDiff == 0 { + blockDiff := order.BlockNumber.Cmp(orders[i].BlockNumber) + if blockDiff == -1 { // order was placed before i + return true + } else if blockDiff == 0 { // order and i were placed in the same block + if order.OrderType == IOC { + // prioritize fulfilling IOC orders first, because they are short-lived + return true + } + } + } + return false + }) + } + + // Insert the order at the determined position + orders = append(orders, &Order{}) // Add an empty order to the end + copy(orders[position+1:], orders[position:]) // Shift orders to the right + orders[position] = order // Insert new Order at the right position + + if order.PositionType == LONG { + db.LongOrders[market] = orders + } else { + db.ShortOrders[market] = orders + } +} + +func (db *InMemoryDatabase) Delete(orderId common.Hash) { + db.mu.Lock() + defer db.mu.Unlock() + + db.deleteOrderWithoutLock(orderId) +} + +func (db *InMemoryDatabase) deleteOrderWithoutLock(orderId common.Hash) { + order := db.Orders[orderId] + if order == nil { + log.Error("In Delete - orderId does not exist in the db.Orders", "orderId", orderId.Hex()) + deleteOrderIdNotFoundCounter.Inc(1) + return + } + + market := order.Market + if order.PositionType == LONG { + orders := db.LongOrders[market] + idx := getOrderIdx(orders, orderId) + if idx == -1 { + log.Error("In Delete - orderId does not exist in the db.LongOrders", "orderId", orderId.Hex()) + deleteOrderIdNotFoundCounter.Inc(1) + } else { + orders = append(orders[:idx], orders[idx+1:]...) + db.LongOrders[market] = orders + } + } else { + orders := db.ShortOrders[market] + idx := getOrderIdx(orders, orderId) + if idx == -1 { + log.Error("In Delete - orderId does not exist in the db.ShortOrders", "orderId", orderId.Hex()) + deleteOrderIdNotFoundCounter.Inc(1) + } else { + orders = append(orders[:idx], orders[idx+1:]...) + db.ShortOrders[market] = orders + } + } + + delete(db.Orders, orderId) + + if order.OrderType == Signed && !order.ReduceOnly { + minAllowableMargin := db.configService.GetMinAllowableMargin() + requiredMargin := hu.GetRequiredMargin(order.Price, hu.Abs(order.GetUnFilledBaseAssetQuantity()), minAllowableMargin, big.NewInt(0)) + db.updateVirtualReservedMargin(order.Trader, hu.Neg(requiredMargin)) + } +} + +func (db *InMemoryDatabase) UpdateFilledBaseAssetQuantity(quantity *big.Int, orderId common.Hash, blockNumber uint64) { + db.mu.Lock() + defer db.mu.Unlock() + + order := db.Orders[orderId] + if order == nil { + log.Error("In UpdateFilledBaseAssetQuantity - orderId does not exist in the database", "orderId", orderId.Hex()) + metrics.GetOrRegisterCounter("update_filled_base_asset_quantity_order_id_not_found", nil).Inc(1) + return + } + if order.PositionType == LONG { + order.FilledBaseAssetQuantity.Add(order.FilledBaseAssetQuantity, quantity) // filled = filled + quantity + } + if order.PositionType == SHORT { + order.FilledBaseAssetQuantity.Sub(order.FilledBaseAssetQuantity, quantity) // filled = filled - quantity + } + + if order.BaseAssetQuantity.Cmp(order.FilledBaseAssetQuantity) == 0 { + order.LifecycleList = append(order.LifecycleList, Lifecycle{blockNumber, FulFilled, ""}) + } + + if quantity.Cmp(big.NewInt(0)) == -1 && order.getOrderStatus().Status == FulFilled { + // handling reorgs + order.LifecycleList = order.LifecycleList[:len(order.LifecycleList)-1] + } + + // only update margin if the order is not reduce-only + if order.OrderType == Signed && !order.ReduceOnly { + hState := GetHubbleState(db.configService) + minAllowableMargin := hState.MinAllowableMargin + requiredMargin := hu.GetRequiredMargin(order.Price, quantity, minAllowableMargin, big.NewInt(0)) + db.updateVirtualReservedMargin(order.Trader, hu.Neg(requiredMargin)) + + } +} + +func (db *InMemoryDatabase) GetNextFundingTime() uint64 { + db.mu.RLock() + defer db.mu.RUnlock() + + return db.NextFundingTime +} + +func (db *InMemoryDatabase) UpdateNextFundingTime(nextFundingTime uint64) { + db.mu.Lock() + defer db.mu.Unlock() + + db.NextFundingTime = nextFundingTime +} + +func (db *InMemoryDatabase) GetNextSamplePITime() uint64 { + db.mu.RLock() + defer db.mu.RUnlock() + + return db.NextSamplePITime +} + +func (db *InMemoryDatabase) GetSamplePIAttemptedTime() uint64 { + db.mu.RLock() + defer db.mu.RUnlock() + + return db.SamplePIAttemptedTime +} + +func (db *InMemoryDatabase) SignalSamplePIAttempted(time uint64) { + db.mu.Lock() + defer db.mu.Unlock() + db.SamplePIAttemptedTime = time +} + +func (db *InMemoryDatabase) UpdateNextSamplePITime(nextSamplePITime uint64) { + db.mu.Lock() + defer db.mu.Unlock() + + db.NextSamplePITime = nextSamplePITime +} + +func (db *InMemoryDatabase) GetLongOrders(market Market, lowerbound *big.Int, blockNumber *big.Int) []Order { + db.mu.RLock() + defer db.mu.RUnlock() + return db.getLongOrdersWithoutLock(market, lowerbound, blockNumber, true) +} + +func (db *InMemoryDatabase) getLongOrdersWithoutLock(market Market, lowerbound *big.Int, blockNumber *big.Int, shouldClean bool) []Order { + var longOrders []Order + + marketOrders := db.LongOrders[market] + // log.Info("getLongOrdersWithoutLock", "marketOrders", marketOrders, "lowerbound", lowerbound, "blockNumber", blockNumber) + for _, order := range marketOrders { + if lowerbound != nil && order.Price.Cmp(lowerbound) < 0 { + // because the long orders are sorted in descending order of price, there is no point in checking further + break + } + + if shouldClean { + if _order := db.getCleanOrder(order, blockNumber); _order != nil { + longOrders = append(longOrders, *_order) + } + } else { + longOrders = append(longOrders, deepCopyOrder(order)) + } + } + return longOrders +} + +func (db *InMemoryDatabase) GetShortOrders(market Market, upperbound *big.Int, blockNumber *big.Int) []Order { + db.mu.RLock() + defer db.mu.RUnlock() + return db.getShortOrdersWithoutLock(market, upperbound, blockNumber, true) +} + +func (db *InMemoryDatabase) getShortOrdersWithoutLock(market Market, upperbound *big.Int, blockNumber *big.Int, shouldClean bool) []Order { + var shortOrders []Order + + marketOrders := db.ShortOrders[market] + + for _, order := range marketOrders { + if upperbound != nil && order.Price.Cmp(upperbound) > 0 { + // short orders are sorted in ascending order of price + break + } + if shouldClean { + if _order := db.getCleanOrder(order, blockNumber); _order != nil { + shortOrders = append(shortOrders, *_order) + } + } else { + shortOrders = append(shortOrders, deepCopyOrder(order)) + } + } + return shortOrders +} + +func (db *InMemoryDatabase) getCleanOrder(order *Order, blockNumber *big.Int) *Order { + // log.Info("getCleanOrder", "order", order, "blockNumber", blockNumber) + eligibleForExecution := false + orderStatus := order.getOrderStatus() + // log.Info("getCleanOrder", "orderStatus", orderStatus) + switch orderStatus.Status { + case Placed: + eligibleForExecution = true + case Execution_Failed: + // ideally these orders should have been auto-cancelled (by the validator) at the same time that they were fulfilling the criteria to fail + // However, there are several reasons why this might not have happened + // 1. A particular cancellation strategy is not implemented yet for e.g. reduce only orders with order.BaseAssetQuantity > position.size are not being auto-cancelled as of Jun 20, 23. This is a @todo + // 2. There might be a scenarios that the order was not deemed cancellable at the time of checking and was hence used for matching; but then eventually failed execution + // a. a tx before the order in the same block, changed their PnL which caused them to have insufficient margin to execute the order + // b. specially true in multi-collateral, where the price of 1 collateral dipped but recovered again after the order was taken for matching (but failed execution) + // 3. There might be a bug in the cancellation logic in either of EVM or smart contract code + // 4. We might have made margin requirements for order fulfillment more liberal at a later stage + // Hence, in view of the above and to serve as a catch-all we retry failed orders after every 100 blocks + // Note at if an order is failing multiple times and it is also not being caught in the auto-cancel logic, then something/somewhere definitely needs fixing + if blockNumber != nil { + if orderStatus.BlockNumber+RETRY_AFTER_BLOCKS <= blockNumber.Uint64() { + eligibleForExecution = true + } else if blockNumber.Uint64()%10 == 0 { + // to not make the log too noisy + log.Warn("eligible order is in Execution_Failed state", "orderId", order.String(), "retryInBlocks", orderStatus.BlockNumber+RETRY_AFTER_BLOCKS-blockNumber.Uint64()) + } + } + } + + expireAt := order.getExpireAt() + if expireAt.Sign() == 1 && expireAt.Int64() <= time.Now().Unix() { + eligibleForExecution = false + } + // log.Info("getCleanOrder", "expireAt", expireAt, "eligibleForExecution", eligibleForExecution) + + if eligibleForExecution { + if order.ReduceOnly { + return db.getReduceOnlyOrderDisplay(order) + } + _order := deepCopyOrder(order) + return &_order + } + return nil +} + +func (db *InMemoryDatabase) UpdateMargin(trader common.Address, collateral Collateral, addAmount *big.Int) { + db.mu.Lock() + defer db.mu.Unlock() + + if _, ok := db.TraderMap[trader]; !ok { + db.TraderMap[trader] = getBlankTrader() + } + + if _, ok := db.TraderMap[trader].Margin.Deposited[collateral]; !ok { + db.TraderMap[trader].Margin.Deposited[collateral] = big.NewInt(0) + } + + db.TraderMap[trader].Margin.Deposited[collateral].Add(db.TraderMap[trader].Margin.Deposited[collateral], addAmount) +} + +func (db *InMemoryDatabase) UpdateReservedMargin(trader common.Address, addAmount *big.Int) { + db.mu.Lock() + defer db.mu.Unlock() + + if _, ok := db.TraderMap[trader]; !ok { + db.TraderMap[trader] = getBlankTrader() + } + + db.TraderMap[trader].Margin.Reserved.Add(db.TraderMap[trader].Margin.Reserved, addAmount) +} + +func (db *InMemoryDatabase) updateVirtualReservedMargin(trader common.Address, addAmount *big.Int) { + if _, ok := db.TraderMap[trader]; !ok { + db.TraderMap[trader] = getBlankTrader() + } + + db.TraderMap[trader].Margin.VirtualReserved.Add(db.TraderMap[trader].Margin.VirtualReserved, addAmount) +} + +func (db *InMemoryDatabase) UpdatePosition(trader common.Address, market Market, size *big.Int, openNotional *big.Int, isLiquidation bool, blockNumber uint64) { + db.mu.Lock() + defer db.mu.Unlock() + + if _, ok := db.TraderMap[trader]; !ok { + db.TraderMap[trader] = getBlankTrader() + } + + if _, ok := db.TraderMap[trader].Positions[market]; !ok { + db.TraderMap[trader].Positions[market] = &Position{} + } + + if db.CumulativePremiumFraction[market] == nil { + db.CumulativePremiumFraction[market] = big.NewInt(0) + } + + previousSize := db.TraderMap[trader].Positions[market].Size + if previousSize == nil || previousSize.Sign() == 0 { + // this is also set in the AMM contract when a new position is opened, without emitting a FundingPaid event + db.TraderMap[trader].Positions[market].LastPremiumFraction = db.CumulativePremiumFraction[market] + db.TraderMap[trader].Positions[market].UnrealisedFunding = big.NewInt(0) + } + + db.TraderMap[trader].Positions[market].Size = size + db.TraderMap[trader].Positions[market].OpenNotional = openNotional + + if !isLiquidation { + db.TraderMap[trader].Positions[market].LiquidationThreshold = getLiquidationThreshold(db.configService.getMaxLiquidationRatio(market), db.configService.getMinSizeRequirement(market), size) + } + + // adjust the liquidation threshold if > resultant position size (for both isLiquidation = true/false) + threshold := utils.BigIntMinAbs(db.TraderMap[trader].Positions[market].LiquidationThreshold, size) + db.TraderMap[trader].Positions[market].LiquidationThreshold.Mul(threshold, big.NewInt(int64(size.Sign()))) // same sign as size +} + +func (db *InMemoryDatabase) UpdateUnrealisedFunding(market Market, cumulativePremiumFraction *big.Int) { + db.mu.Lock() + defer db.mu.Unlock() + + db.CumulativePremiumFraction[market] = cumulativePremiumFraction + for _, trader := range db.TraderMap { + position := trader.Positions[market] + if position != nil { + position.UnrealisedFunding = calcPendingFunding(cumulativePremiumFraction, position.LastPremiumFraction, position.Size) + } + } +} + +func calcPendingFunding(cumulativePremiumFraction, lastPremiumFraction, size *big.Int) *big.Int { + if size == nil || size.Sign() == 0 { + return big.NewInt(0) + } + + if cumulativePremiumFraction == nil { + cumulativePremiumFraction = big.NewInt(0) + } + + if lastPremiumFraction == nil { + lastPremiumFraction = big.NewInt(0) + } + + // Calculate difference + diff := new(big.Int).Sub(cumulativePremiumFraction, lastPremiumFraction) + + // Multiply by size + result := new(big.Int).Mul(diff, size) + + // Handle negative rounding + if result.Sign() < 0 { + result.Add(result, big.NewInt(1e18-1)) + } + + // Divide by 1e18 + return hu.Div1e18(result) +} + +func (db *InMemoryDatabase) ResetUnrealisedFunding(market Market, trader common.Address, cumulativePremiumFraction *big.Int) { + db.mu.Lock() + defer db.mu.Unlock() + + if db.TraderMap[trader] != nil { + if _, ok := db.TraderMap[trader].Positions[market]; ok { + db.TraderMap[trader].Positions[market].UnrealisedFunding = big.NewInt(0) + db.TraderMap[trader].Positions[market].LastPremiumFraction = cumulativePremiumFraction + } + } +} + +func (db *InMemoryDatabase) UpdateLastPrice(market Market, lastPrice *big.Int) { + db.mu.Lock() + defer db.mu.Unlock() + + db.LastPrice[market] = lastPrice +} + +func (db *InMemoryDatabase) UpdateLastPremiumFraction(market Market, trader common.Address, lastPremiumFraction *big.Int, cumulativePremiumFraction *big.Int) { + db.mu.Lock() + defer db.mu.Unlock() + + if _, ok := db.TraderMap[trader]; !ok { + db.TraderMap[trader] = getBlankTrader() + } + + if _, ok := db.TraderMap[trader].Positions[market]; !ok { + db.TraderMap[trader].Positions[market] = &Position{} + } + + db.TraderMap[trader].Positions[market].LastPremiumFraction = lastPremiumFraction + db.TraderMap[trader].Positions[market].UnrealisedFunding = hu.Div1e18(big.NewInt(0).Mul(big.NewInt(0).Sub(cumulativePremiumFraction, lastPremiumFraction), db.TraderMap[trader].Positions[market].Size)) +} + +func (db *InMemoryDatabase) GetLastPrices() map[Market]*big.Int { + db.mu.RLock() + defer db.mu.RUnlock() + + copyMap := make(map[Market]*big.Int) + for k, v := range db.LastPrice { + copyMap[k] = new(big.Int).Set(v) + } + return copyMap +} + +func (db *InMemoryDatabase) GetAllTraders() map[common.Address]Trader { + db.mu.RLock() + defer db.mu.RUnlock() + + traderMap := map[common.Address]Trader{} + for address, trader := range db.TraderMap { + traderMap[address] = *trader + } + return traderMap +} + +func (db *InMemoryDatabase) GetOpenOrdersForTraderByType(trader common.Address, orderType OrderType) []Order { + db.mu.RLock() + defer db.mu.RUnlock() + + return db.getTraderOrders(trader, orderType) +} + +func (db *InMemoryDatabase) GetAllOpenOrdersForTrader(trader common.Address) []Order { + db.mu.RLock() + defer db.mu.RUnlock() + + return db.getAllTraderOrders(trader) +} + +func (db *InMemoryDatabase) GetOrderById(orderId common.Hash) *Order { + db.mu.RLock() + defer db.mu.RUnlock() + + order := db.Orders[orderId] + if order == nil { + return nil + } + + orderCopy := deepCopyOrder(order) + return &orderCopy +} + +func (db *InMemoryDatabase) GetTraderInfo(trader common.Address) *Trader { + db.mu.RLock() + defer db.mu.RUnlock() + + traderInfo := db.TraderMap[trader] + if traderInfo == nil { + return nil + } + + traderCopy := deepCopyTrader(traderInfo) + return traderCopy +} + +func determinePositionToLiquidate(trader *Trader, addr common.Address, marginFraction *big.Int, markets []Market, minSizes []*big.Int) LiquidablePosition { + liquidable := LiquidablePosition{} + // iterate through the markets and return the first one with an open position + // @todo when we introduce multiple markets, we will have to implement a more sophisticated liquidation strategy + for i, market := range markets { + position := trader.Positions[market] + if position == nil || position.Size.Sign() == 0 { + continue + } + liquidable = LiquidablePosition{ + Address: addr, + Market: market, + Size: new(big.Int).Abs(position.LiquidationThreshold), // position.LiquidationThreshold is a pointer, to want to avoid unintentional mutation if/when we mutate liquidable.Size + MarginFraction: new(big.Int).Set(marginFraction), + FilledSize: big.NewInt(0), + } + // while setting liquidation threshold of a position, we do not ensure whether it is a multiple of minSize. + // we will take care of that here + liquidable.Size.Div(liquidable.Size, minSizes[i]) + liquidable.Size.Mul(liquidable.Size, minSizes[i]) + if position.Size.Sign() == -1 { + liquidable.PositionType = SHORT + liquidable.Size.Neg(liquidable.Size) + } else { + liquidable.PositionType = LONG + } + } + return liquidable +} + +func (db *InMemoryDatabase) GetNaughtyTraders(hState *hu.HubbleState) ([]LiquidablePosition, map[common.Address][]Order, map[common.Address]*big.Int) { + db.mu.RLock() + defer db.mu.RUnlock() + + liquidablePositions := []LiquidablePosition{} + ordersToCancel := map[common.Address][]Order{} + marginMap := map[common.Address]*big.Int{} + count := 0 + + // will be updated lazily only if liquidablePositions are found + minSizes := []*big.Int{} + + for addr, trader := range db.TraderMap { + userState := &hu.UserState{ + Positions: translatePositions(trader.Positions), + Margins: getMargins(trader, len(hState.Assets)), + PendingFunding: getTotalFunding(trader, hState.ActiveMarkets), + ReservedMargin: new(big.Int).Set(trader.Margin.Reserved), + // this is the only leveldb read, others above are in-memory reads + ReduceOnlyAmounts: db.configService.GetReduceOnlyAmounts(addr), + } + marginFraction := hu.GetMarginFraction(hState, userState) + db.TraderMap[addr].Margin.Available = hu.GetAvailableMargin(hState, userState) + marginMap[addr] = new(big.Int).Set(db.TraderMap[addr].Margin.Available) + if marginFraction.Cmp(hState.MaintenanceMargin) == -1 { + log.Info("below maintenanceMargin", "trader", addr.String(), "marginFraction", prettifyScaledBigInt(marginFraction, 6)) + if len(minSizes) == 0 { + for _, market := range hState.ActiveMarkets { + minSizes = append(minSizes, db.configService.getMinSizeRequirement(market)) + } + } + liquidablePositions = append(liquidablePositions, determinePositionToLiquidate(trader, addr, marginFraction, hState.ActiveMarkets, minSizes)) + continue // we do not check for their open orders yet. Maybe liquidating them first will make available margin positive + } + + shouldLookForOrdersToCancel := false + marketsToCancelReduceOnlyOrdersIn := make(map[int]bool) + for _, marketId := range hState.ActiveMarkets { + posSize := big.NewInt(0) + if userState.Positions[marketId] != nil && userState.Positions[marketId].Size != nil { + posSize = userState.Positions[marketId].Size + } + totalReduceAmount := userState.ReduceOnlyAmounts[marketId] + hasStaleReduceOnlyOrders := (posSize.Sign() == 0 && totalReduceAmount.Sign() != 0) || + (posSize.Sign() > 0 && (totalReduceAmount.Sign() > 0 || (totalReduceAmount.Sign() < 0 && hu.Neg(totalReduceAmount).Cmp(posSize) > 0))) || + (posSize.Sign() < 0 && (totalReduceAmount.Sign() < 0 || (totalReduceAmount.Sign() > 0 && totalReduceAmount.Cmp(hu.Neg(posSize)) > 0))) + if hasStaleReduceOnlyOrders { + marketsToCancelReduceOnlyOrdersIn[marketId] = true + shouldLookForOrdersToCancel = true + } + } + + // now check for regular un-fillable orders + availableMargin := new(big.Int).Set(db.TraderMap[addr].Margin.Available) + if availableMargin.Sign() < 0 { + // negative available margin, and has reserved margin + if trader.Margin.Reserved.Sign() > 0 { + shouldLookForOrdersToCancel = true + } else { + // this has the affect that we will not look for non-reduce only orders to cancel + availableMargin = big.NewInt(0) + } + } + + if !shouldLookForOrdersToCancel { + continue + } + + foundCancellableOrders := false + foundCancellableOrders = db.determineOrdersToCancel(addr, trader, availableMargin, marketsToCancelReduceOnlyOrdersIn, hState.OraclePrices, ordersToCancel, hState.MinAllowableMargin) + if foundCancellableOrders { + log.Info("found orders to cancel for", "trader", addr.String()) + } else { + count++ + } + } + if count > 0 { + log.Info("#traders that have shouldLookForOrdersToCancel=true but no orders to cancel", "count", count) + } + // lower margin fraction positions should be liquidated first + sortLiquidableSliceByMarginFraction(liquidablePositions) + return liquidablePositions, ordersToCancel, marginMap +} + +// assumes db.mu.RLock has been held by the caller +func (db *InMemoryDatabase) determineOrdersToCancel(addr common.Address, trader *Trader, availableMargin *big.Int, marketsToCancelReduceOnlyOrdersIn map[int]bool, oraclePrices map[Market]*big.Int, ordersToCancel map[common.Address][]Order, minAllowableMargin *big.Int) bool { + traderOrders := db.getTraderOrders(addr, Limit) + if len(traderOrders) == 0 { + return false + } + + sort.Slice(traderOrders, func(i, j int) bool { + // higher diff comes first + iDiff := big.NewInt(0).Abs(big.NewInt(0).Sub(traderOrders[i].Price, oraclePrices[traderOrders[i].Market])) + jDiff := big.NewInt(0).Abs(big.NewInt(0).Sub(traderOrders[j].Price, oraclePrices[traderOrders[j].Market])) + return iDiff.Cmp(jDiff) > 0 + }) + + _availableMargin := new(big.Int).Set(availableMargin) + // cancel orders until available margin is positive + ordersToCancel[addr] = []Order{} + foundCancellableOrders := false + for _, order := range traderOrders { + if order.OrderType != Limit { + // ioc and signed orders dont need to be cancelled + continue + } + if order.ReduceOnly { + if marketsToCancelReduceOnlyOrdersIn[order.Market] { + ordersToCancel[addr] = append(ordersToCancel[addr], order) + marketsToCancelReduceOnlyOrdersIn[order.Market] = false // keeping it simple by cancelling just 1 stale order per market per run + foundCancellableOrders = true + } + } else if _availableMargin.Sign() < 0 { + ordersToCancel[addr] = append(ordersToCancel[addr], order) + foundCancellableOrders = true + orderNotional := big.NewInt(0).Abs(hu.Div1e18(hu.Mul(order.GetUnFilledBaseAssetQuantity(), order.Price))) // | size * current price | + marginReleased := hu.Div1e6(hu.Mul(orderNotional, db.configService.GetMinAllowableMargin())) + _availableMargin.Add(_availableMargin, marginReleased) + } + } + return foundCancellableOrders +} + +func (db *InMemoryDatabase) getTraderOrders(trader common.Address, orderType OrderType) []Order { + traderOrders := []Order{} + for _, order := range db.Orders { + if order.Trader == trader && order.OrderType == orderType { + traderOrders = append(traderOrders, deepCopyOrder(order)) + } + } + return traderOrders +} + +func (db *InMemoryDatabase) getAllTraderOrders(trader common.Address) []Order { + traderOrders := []Order{} + for _, order := range db.Orders { + if order.Trader == trader { + traderOrders = append(traderOrders, deepCopyOrder(order)) + } + } + return traderOrders +} + +func (db *InMemoryDatabase) getReduceOnlyOrderDisplay(order *Order) *Order { + trader := order.Trader + if db.TraderMap[trader] == nil { + return nil + } + positions := db.TraderMap[trader].Positions + if position, ok := positions[order.Market]; ok { + // position.Size, order.BaseAssetQuantity need to be of opposite sign and abs(position.Size) >= abs(order.BaseAssetQuantity) + if position.Size.Sign() == 0 || position.Size.Sign() == order.BaseAssetQuantity.Sign() { + return nil + } + if position.Size.CmpAbs(order.GetUnFilledBaseAssetQuantity()) >= 0 { + // position is bigger than unfilled order size + orderCopy := deepCopyOrder(order) + return &orderCopy + } else { + // position is smaller than unfilled order + // increase the filled quantity so that unfilled amount is equal to position size + orderCopy := deepCopyOrder(order) + orderCopy.FilledBaseAssetQuantity = big.NewInt(0).Add(orderCopy.BaseAssetQuantity, position.Size) // both have opposite sign, therefore we add + return &orderCopy + } + } else { + return nil + } +} + +func (db *InMemoryDatabase) GetOrderBookData() InMemoryDatabase { + db.mu.RLock() + defer db.mu.RUnlock() + + return *db +} + +func (db *InMemoryDatabase) GetOrderBookDataCopy() (*InMemoryDatabase, error) { + db.mu.RLock() + defer db.mu.RUnlock() + + var buf bytes.Buffer + err := gob.NewEncoder(&buf).Encode(db) + if err != nil { + return nil, fmt.Errorf("error encoding database: %v", err) + } + + buf2 := bytes.NewBuffer(buf.Bytes()) + var memoryDBCopy *InMemoryDatabase + err = gob.NewDecoder(buf2).Decode(&memoryDBCopy) + if err != nil { + return nil, fmt.Errorf("error decoding database: %v", err) + } + + memoryDBCopy.mu = &sync.RWMutex{} + memoryDBCopy.configService = db.configService + return memoryDBCopy, nil +} + +func getLiquidationThreshold(maxLiquidationRatio *big.Int, minSizeRequirement *big.Int, size *big.Int) *big.Int { + absSize := big.NewInt(0).Abs(size) + maxLiquidationSize := hu.Div1e6(big.NewInt(0).Mul(absSize, maxLiquidationRatio)) + liquidationThreshold := utils.BigIntMax(maxLiquidationSize, minSizeRequirement) + return big.NewInt(0).Mul(liquidationThreshold, big.NewInt(int64(size.Sign()))) // same sign as size +} + +func getBlankTrader() *Trader { + return &Trader{ + Positions: map[Market]*Position{}, + Margin: Margin{ + Available: big.NewInt(0), + Deposited: map[Collateral]*big.Int{ + 0: big.NewInt(0), + }, + Reserved: big.NewInt(0), + VirtualReserved: big.NewInt(0), + }, + } +} + +func getAvailableMargin(trader *Trader, hState *hu.HubbleState) *big.Int { + return hu.GetAvailableMargin( + hState, + &hu.UserState{ + Positions: translatePositions(trader.Positions), + Margins: getMargins(trader, len(hState.Assets)), + PendingFunding: getTotalFunding(trader, hState.ActiveMarkets), + ReservedMargin: trader.Margin.Reserved, + }, + ) +} + +// deepCopyOrder deep copies the LimitOrder struct +func deepCopyOrder(order *Order) Order { + lifecycleList := &order.LifecycleList + return Order{ + Id: order.Id, + Market: order.Market, + PositionType: order.PositionType, + Trader: order.Trader, + BaseAssetQuantity: big.NewInt(0).Set(order.BaseAssetQuantity), + FilledBaseAssetQuantity: big.NewInt(0).Set(order.FilledBaseAssetQuantity), + Salt: big.NewInt(0).Set(order.Salt), + Price: big.NewInt(0).Set(order.Price), + ReduceOnly: order.ReduceOnly, + LifecycleList: *lifecycleList, + BlockNumber: big.NewInt(0).Set(order.BlockNumber), + RawOrder: order.RawOrder, + OrderType: order.OrderType, + } +} + +func deepCopyTrader(order *Trader) *Trader { + positions := map[Market]*Position{} + for market, position := range order.Positions { + positions[market] = &Position{ + Position: hu.Position{ + OpenNotional: big.NewInt(0).Set(position.OpenNotional), + Size: big.NewInt(0).Set(position.Size), + }, + UnrealisedFunding: big.NewInt(0).Set(position.UnrealisedFunding), + LastPremiumFraction: big.NewInt(0).Set(position.LastPremiumFraction), + LiquidationThreshold: big.NewInt(0).Set(position.LiquidationThreshold), + } + } + + margin := Margin{ + Reserved: big.NewInt(0).Set(order.Margin.Reserved), + Deposited: map[Collateral]*big.Int{}, + } + for collateral, amount := range order.Margin.Deposited { + margin.Deposited[collateral] = big.NewInt(0).Set(amount) + } + return &Trader{ + Positions: positions, + Margin: margin, + } +} + +func getOrderIdx(orders []*Order, orderId common.Hash) int { + for i, order := range orders { + if order.Id == orderId { + return i + } + } + return -1 +} + +type OrderValidationFields struct { + Exists bool + PosSize *big.Int + AsksHead *big.Int + BidsHead *big.Int + ShouldTriggerMatching bool +} + +func (db *InMemoryDatabase) GetOrderValidationFields(orderId common.Hash, order *hu.SignedOrder) OrderValidationFields { + db.mu.RLock() + defer db.mu.RUnlock() + + if db.Orders[orderId] != nil { + return OrderValidationFields{Exists: true} + } + + // trader data + trader := order.Trader + marketId := int(order.AmmIndex.Int64()) + posSize := big.NewInt(0) + if db.TraderMap[trader] != nil && db.TraderMap[trader].Positions[marketId] != nil && db.TraderMap[trader].Positions[marketId].Size != nil { + posSize = db.TraderMap[trader].Positions[marketId].Size + } + + // market data + // allow some grace to market orders to be filled and accept post-only orders that might fill them + // iterate until we find a short order that is not an IOC order. + isLongOrder := order.BaseAssetQuantity.Sign() > 0 + shouldTriggerMatching := false + asksHead := big.NewInt(0) + if isLongOrder && len(db.ShortOrders[marketId]) > 0 { + for _, _order := range db.ShortOrders[marketId] { + if _order.OrderType != IOC { + asksHead = _order.Price + break + } else if _order.Price.Cmp(order.Price) <= 0 { + shouldTriggerMatching = true + } + } + } + + bidsHead := big.NewInt(0) + if !isLongOrder && len(db.LongOrders[marketId]) > 0 { + for _, _order := range db.LongOrders[marketId] { + if _order.OrderType != IOC { + bidsHead = _order.Price + break + } else if _order.Price.Cmp(order.Price) >= 0 { + shouldTriggerMatching = true + } + } + } + + return OrderValidationFields{ + Exists: false, + PosSize: posSize, + AsksHead: asksHead, + BidsHead: bidsHead, + ShouldTriggerMatching: shouldTriggerMatching, + } +} + +func (db *InMemoryDatabase) GetMarginAvailableForMakerbook(trader common.Address, prices map[int]*big.Int) *big.Int { + db.mu.RLock() + defer db.mu.RUnlock() + + _trader := db.TraderMap[trader] + if _trader == nil { + return big.NewInt(0) + } + + hState := GetHubbleState(db.configService) + hState.OraclePrices = prices + userState := &hu.UserState{ + Positions: translatePositions(_trader.Positions), + Margins: getMargins(_trader, len(hState.Assets)), + PendingFunding: getTotalFunding(_trader, hState.ActiveMarkets), + ReservedMargin: new(big.Int).Set(_trader.Margin.Reserved), + } + if _trader.Margin.VirtualReserved == nil { + _trader.Margin.VirtualReserved = big.NewInt(0) + } + return hu.Sub(hu.GetAvailableMargin(hState, userState), _trader.Margin.VirtualReserved) +} + +func (db *InMemoryDatabase) SampleImpactPrice() (impactBids, impactAsks, midPrices []*big.Int) { + db.mu.RLock() + defer db.mu.RUnlock() + + // SUNSET: code will not reach here when markets are settled + count := db.configService.GetActiveMarketsCount() + impactBids = make([]*big.Int, count) + impactAsks = make([]*big.Int, count) + midPrices = make([]*big.Int, count) + + for m := int64(0); m < count; m++ { + // @todo make the optimisation to fetch orders only until impactMarginNotional + longOrders := db.getLongOrdersWithoutLock(Market(m), nil, nil, true) + shortOrders := db.getShortOrdersWithoutLock(Market(m), nil, nil, true) + ammAddress := db.configService.GetMarketAddressFromMarketID(m) + impactMarginNotional := db.configService.GetImpactMarginNotional(ammAddress) + calcMidPrice := true + if len(longOrders) != 0 { + impactBids[m] = calculateImpactPrice(longOrders, impactMarginNotional) + } else { + impactBids[m] = big.NewInt(0) + calcMidPrice = false + } + + if len(shortOrders) != 0 { + impactAsks[m] = calculateImpactPrice(shortOrders, impactMarginNotional) + } else { + impactAsks[m] = big.NewInt(0) + calcMidPrice = false + } + + if calcMidPrice { + midPrices[m] = hu.Div(hu.Add(longOrders[0].Price, shortOrders[0].Price), new(big.Int).SetInt64(2)) + } else { + midPrices[m] = big.NewInt(0) + } + } + return impactBids, impactAsks, midPrices +} + +func calculateImpactPrice(orders []Order, _impactMarginNotional *big.Int) *big.Int { + if _impactMarginNotional.Sign() == 0 { + return big.NewInt(0) + } + impactMarginNotional := new(big.Int).Mul(_impactMarginNotional, big.NewInt(1e12)) // 18 decimals + accNotional := big.NewInt(0) // 18 decimals + accBaseQ := big.NewInt(0) // 18 decimals + tick := big.NewInt(0) // 6 decimals + found := false + for _, order := range orders { + amount := hu.Abs(hu.Sub(order.BaseAssetQuantity, order.FilledBaseAssetQuantity)) // 18 decimals + if amount.Sign() == 0 { + continue + } + tick = order.Price + accumulator := new(big.Int).Add(accNotional, hu.Div1e6(big.NewInt(0).Mul(amount, tick))) + if accumulator.Cmp(impactMarginNotional) >= 0 { + found = true // that we have enough liquidity to fill the impactMarginNotional + break + } + accNotional = accumulator + accBaseQ.Add(accBaseQ, amount) + } + if !found { + return big.NewInt(0) + } + baseQAtTick := new(big.Int).Div(hu.Mul1e6(new(big.Int).Sub(impactMarginNotional, accNotional)), tick) + return new(big.Int).Div(hu.Mul1e6(impactMarginNotional), new(big.Int).Add(baseQAtTick, accBaseQ)) // return value is in 6 decimals +} diff --git a/plugin/evm/orderbook/memory_database_test.go b/plugin/evm/orderbook/memory_database_test.go new file mode 100644 index 0000000000..f6a024ab7f --- /dev/null +++ b/plugin/evm/orderbook/memory_database_test.go @@ -0,0 +1,1153 @@ +package orderbook + +import ( + "math/big" + "math/rand" + "testing" + "time" + + "github.com/ava-labs/subnet-evm/metrics" + hu "github.com/ava-labs/subnet-evm/plugin/evm/orderbook/hubbleutils" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/assert" +) + +var positionType = SHORT +var userAddress = "0x22Bb736b64A0b4D4081E103f83bccF864F0404aa" +var trader = common.HexToAddress(userAddress) +var price = big.NewInt(20) +var status Status = Placed +var blockNumber = big.NewInt(2) + +var market = Market(0) +var assets = []hu.Collateral{{Price: big.NewInt(1e6), Weight: big.NewInt(1e6), Decimals: 6}} + +func TestgetDatabase(t *testing.T) { + inMemoryDatabase := getDatabase() + assert.NotNil(t, inMemoryDatabase) +} + +func TestAddSequence(t *testing.T) { + baseAssetQuantity := big.NewInt(10) + db := getDatabase() + + t.Run("Long orders", func(t *testing.T) { + order1 := createLimitOrder(LONG, userAddress, baseAssetQuantity, big.NewInt(20), status, big.NewInt(2), big.NewInt(1)) + db.Add(&order1) + + assert.Equal(t, 1, len(db.Orders)) + assert.Equal(t, 1, len(db.LongOrders[market])) + assert.Equal(t, db.LongOrders[market][0].Id, order1.Id) + + order2 := createLimitOrder(LONG, userAddress, baseAssetQuantity, big.NewInt(21), status, big.NewInt(2), big.NewInt(2)) + db.Add(&order2) + + assert.Equal(t, 2, len(db.Orders)) + assert.Equal(t, 2, len(db.LongOrders[market])) + assert.Equal(t, db.LongOrders[market][0].Id, order2.Id) + assert.Equal(t, db.LongOrders[market][1].Id, order1.Id) + + order3 := createLimitOrder(LONG, userAddress, baseAssetQuantity, big.NewInt(19), status, big.NewInt(2), big.NewInt(3)) + db.Add(&order3) + + assert.Equal(t, 3, len(db.Orders)) + assert.Equal(t, 3, len(db.LongOrders[market])) + assert.Equal(t, db.LongOrders[market][0].Id, order2.Id) + assert.Equal(t, db.LongOrders[market][1].Id, order1.Id) + assert.Equal(t, db.LongOrders[market][2].Id, order3.Id) + + // block number + order4 := createLimitOrder(LONG, userAddress, baseAssetQuantity, big.NewInt(20), status, big.NewInt(3), big.NewInt(4)) + db.Add(&order4) + + assert.Equal(t, 4, len(db.Orders)) + assert.Equal(t, 4, len(db.LongOrders[market])) + assert.Equal(t, db.LongOrders[market][0].Id, order2.Id) + assert.Equal(t, db.LongOrders[market][1].Id, order1.Id) + assert.Equal(t, db.LongOrders[market][2].Id, order4.Id) + assert.Equal(t, db.LongOrders[market][3].Id, order3.Id) + + // ioc order + order5 := createIOCOrder(LONG, userAddress, baseAssetQuantity, big.NewInt(20), status, big.NewInt(2), big.NewInt(5), big.NewInt(2)) + db.Add(&order5) + + assert.Equal(t, 5, len(db.Orders)) + assert.Equal(t, 5, len(db.LongOrders[market])) + assert.Equal(t, db.LongOrders[market][0].Id, order2.Id) + assert.Equal(t, db.LongOrders[market][1].Id, order5.Id) + assert.Equal(t, db.LongOrders[market][2].Id, order1.Id) + assert.Equal(t, db.LongOrders[market][3].Id, order4.Id) + assert.Equal(t, db.LongOrders[market][4].Id, order3.Id) + }) + + t.Run("Short orders", func(t *testing.T) { + baseAssetQuantity = big.NewInt(-10) + order1 := createLimitOrder(SHORT, userAddress, baseAssetQuantity, big.NewInt(20), status, big.NewInt(2), big.NewInt(6)) + db.Add(&order1) + + assert.Equal(t, 6, len(db.Orders)) + assert.Equal(t, 1, len(db.ShortOrders[market])) + assert.Equal(t, db.ShortOrders[market][0].Id, order1.Id) + + order2 := createLimitOrder(SHORT, userAddress, baseAssetQuantity, big.NewInt(19), status, big.NewInt(2), big.NewInt(7)) + db.Add(&order2) + + assert.Equal(t, 7, len(db.Orders)) + assert.Equal(t, 2, len(db.ShortOrders[market])) + assert.Equal(t, db.ShortOrders[market][0].Id, order2.Id) + assert.Equal(t, db.ShortOrders[market][1].Id, order1.Id) + + order3 := createLimitOrder(SHORT, userAddress, baseAssetQuantity, big.NewInt(21), status, big.NewInt(2), big.NewInt(8)) + db.Add(&order3) + + assert.Equal(t, 8, len(db.Orders)) + assert.Equal(t, 3, len(db.ShortOrders[market])) + assert.Equal(t, db.ShortOrders[market][0].Id, order2.Id) + assert.Equal(t, db.ShortOrders[market][1].Id, order1.Id) + assert.Equal(t, db.ShortOrders[market][2].Id, order3.Id) + + // block number + order4 := createLimitOrder(SHORT, userAddress, baseAssetQuantity, big.NewInt(20), status, big.NewInt(3), big.NewInt(9)) + db.Add(&order4) + + assert.Equal(t, 9, len(db.Orders)) + assert.Equal(t, 4, len(db.ShortOrders[market])) + assert.Equal(t, db.ShortOrders[market][0].Id, order2.Id) + assert.Equal(t, db.ShortOrders[market][1].Id, order1.Id) + assert.Equal(t, db.ShortOrders[market][2].Id, order4.Id) + assert.Equal(t, db.ShortOrders[market][3].Id, order3.Id) + + // ioc order + order5 := createIOCOrder(SHORT, userAddress, baseAssetQuantity, big.NewInt(20), status, big.NewInt(2), big.NewInt(10), big.NewInt(2)) + db.Add(&order5) + + assert.Equal(t, 10, len(db.Orders)) + assert.Equal(t, 5, len(db.ShortOrders[market])) + assert.Equal(t, db.ShortOrders[market][0].Id, order2.Id) + assert.Equal(t, db.ShortOrders[market][1].Id, order5.Id) + assert.Equal(t, db.ShortOrders[market][2].Id, order1.Id) + assert.Equal(t, db.ShortOrders[market][3].Id, order4.Id) + assert.Equal(t, db.ShortOrders[market][4].Id, order3.Id) + }) +} + +func TestAdd(t *testing.T) { + baseAssetQuantity := big.NewInt(-10) + inMemoryDatabase := getDatabase() + salt := big.NewInt(time.Now().Unix()) + limitOrder := createLimitOrder(positionType, userAddress, baseAssetQuantity, price, status, blockNumber, salt) + inMemoryDatabase.Add(&limitOrder) + returnedOrder := inMemoryDatabase.Orders[limitOrder.Id] + assert.Equal(t, limitOrder.PositionType, returnedOrder.PositionType) + assert.Equal(t, limitOrder.Trader, returnedOrder.Trader) + assert.Equal(t, limitOrder.BaseAssetQuantity, returnedOrder.BaseAssetQuantity) + assert.Equal(t, limitOrder.Price, returnedOrder.Price) + assert.Equal(t, limitOrder.getOrderStatus().Status, returnedOrder.getOrderStatus().Status) + assert.Equal(t, limitOrder.BlockNumber, returnedOrder.BlockNumber) +} + +func TestGetAllOrders(t *testing.T) { + baseAssetQuantity := big.NewInt(-10) + inMemoryDatabase := getDatabase() + totalOrders := uint64(5) + for i := uint64(0); i < totalOrders; i++ { + salt := big.NewInt(0).Add(big.NewInt(int64(i)), big.NewInt(time.Now().Unix())) + limitOrder := createLimitOrder(positionType, userAddress, baseAssetQuantity, price, status, blockNumber, salt) + inMemoryDatabase.Add(&limitOrder) + } + returnedOrders := inMemoryDatabase.GetAllOrders() + assert.Equal(t, totalOrders, uint64(len(returnedOrders))) + for _, returnedOrder := range returnedOrders { + assert.Equal(t, positionType, returnedOrder.PositionType) + assert.Equal(t, userAddress, returnedOrder.Trader.String()) + assert.Equal(t, baseAssetQuantity, returnedOrder.BaseAssetQuantity) + assert.Equal(t, price, returnedOrder.Price) + assert.Equal(t, status, returnedOrder.getOrderStatus().Status) + assert.Equal(t, blockNumber, returnedOrder.BlockNumber) + } +} + +func TestGetShortOrders(t *testing.T) { + baseAssetQuantity := hu.Mul1e18(big.NewInt(-3)) + inMemoryDatabase := getDatabase() + totalLongOrders := uint64(2) + longOrderPrice := big.NewInt(0).Add(price, big.NewInt(1)) + longOrderBaseAssetQuantity := hu.Mul1e18(big.NewInt(10)) + for i := uint64(0); i < totalLongOrders; i++ { + salt := big.NewInt(0).Add(big.NewInt(int64(i)), big.NewInt(time.Now().Unix())) + limitOrder := createLimitOrder(LONG, userAddress, longOrderBaseAssetQuantity, longOrderPrice, status, blockNumber, salt) + inMemoryDatabase.Add(&limitOrder) + } + //Short order with price 10 and blockNumber 2 + price1 := big.NewInt(10) + blockNumber1 := big.NewInt(2) + salt1 := big.NewInt(time.Now().Unix()) + shortOrder1 := createLimitOrder(SHORT, userAddress, baseAssetQuantity, price1, status, blockNumber1, salt1) + inMemoryDatabase.Add(&shortOrder1) + + //Short order with price 9 and blockNumber 2 + price2 := big.NewInt(9) + blockNumber2 := big.NewInt(2) + salt2 := big.NewInt(0).Add(salt1, big.NewInt(1)) + shortOrder2 := createLimitOrder(SHORT, userAddress, baseAssetQuantity, price2, status, blockNumber2, salt2) + inMemoryDatabase.Add(&shortOrder2) + + //Short order with price 9.01 and blockNumber 3 + price3 := big.NewInt(9) + blockNumber3 := big.NewInt(3) + salt3 := big.NewInt(0).Add(salt2, big.NewInt(1)) + shortOrder3 := createLimitOrder(SHORT, userAddress, baseAssetQuantity, price3, status, blockNumber3, salt3) + inMemoryDatabase.Add(&shortOrder3) + + //Reduce only short order with price 9 and blockNumber 4 + price4 := big.NewInt(9) + blockNumber4 := big.NewInt(4) + salt4 := big.NewInt(0).Add(salt3, big.NewInt(1)) + shortOrder4 := createLimitOrder(SHORT, userAddress, baseAssetQuantity, price4, status, blockNumber4, salt4) + shortOrder4.ReduceOnly = true + inMemoryDatabase.Add(&shortOrder4) + + returnedShortOrders := inMemoryDatabase.GetShortOrders(market, nil, nil) + assert.Equal(t, 3, len(returnedShortOrders)) + + for _, returnedOrder := range returnedShortOrders { + assert.Equal(t, SHORT, returnedOrder.PositionType) + assert.Equal(t, userAddress, returnedOrder.Trader.String()) + assert.Equal(t, baseAssetQuantity, returnedOrder.BaseAssetQuantity) + assert.Equal(t, status, returnedOrder.getOrderStatus().Status) + } + + //Test returnedShortOrders are sorted by price lowest to highest first and then block number from lowest to highest + assert.Equal(t, price2, returnedShortOrders[0].Price) + assert.Equal(t, blockNumber2, returnedShortOrders[0].BlockNumber) + assert.Equal(t, price3, returnedShortOrders[1].Price) + assert.Equal(t, blockNumber3, returnedShortOrders[1].BlockNumber) + assert.Equal(t, price1, returnedShortOrders[2].Price) + assert.Equal(t, blockNumber1, returnedShortOrders[2].BlockNumber) + + // now test with one reduceOnly order when there's a long position + size := big.NewInt(0).Mul(big.NewInt(2), hu.ONE_E_18) + inMemoryDatabase.UpdatePosition(trader, market, size, big.NewInt(0).Mul(big.NewInt(100), hu.ONE_E_6), false, 0) + + returnedShortOrders = inMemoryDatabase.GetShortOrders(market, nil, nil) + assert.Equal(t, 4, len(returnedShortOrders)) + + // at least one of the orders should be reduce only + reduceOnlyOrder := Order{} + for _, order := range returnedShortOrders { + if order.ReduceOnly { + reduceOnlyOrder = order + } + } + assert.Equal(t, reduceOnlyOrder.Salt, salt4) + assert.Equal(t, reduceOnlyOrder.BaseAssetQuantity, baseAssetQuantity) + assert.Equal(t, reduceOnlyOrder.FilledBaseAssetQuantity, big.NewInt(0).Neg(hu.ONE_E_18)) +} + +func TestGetShortOrdersIOC(t *testing.T) { + inMemoryDatabase := getDatabase() + + // order with expiry of 2 seconds + iocOrder1 := createIOCOrder(SHORT, userAddress, big.NewInt(-10), big.NewInt(10), status, big.NewInt(2), big.NewInt(100), big.NewInt(2)) + // order with expiry of -2 seconds, should be expired already + iocOrder2 := createIOCOrder(SHORT, userAddress, big.NewInt(-10), big.NewInt(10), status, big.NewInt(2), big.NewInt(101), big.NewInt(-2)) + inMemoryDatabase.Add(&iocOrder1) + inMemoryDatabase.Add(&iocOrder2) + + shortOrders := inMemoryDatabase.GetShortOrders(0, nil, nil) + assert.Equal(t, 1, len(shortOrders)) + assert.Equal(t, iocOrder1.Id, shortOrders[0].Id) +} + +func TestGetLongOrders(t *testing.T) { + baseAssetQuantity := big.NewInt(-10) + inMemoryDatabase := getDatabase() + for i := uint64(0); i < 3; i++ { + salt := big.NewInt(0).Add(big.NewInt(time.Now().Unix()), big.NewInt(int64(i))) + limitOrder := createLimitOrder(SHORT, userAddress, baseAssetQuantity, price, status, blockNumber, salt) + inMemoryDatabase.Add(&limitOrder) + } + + //Long order with price 9 and blockNumber 2 + longOrderBaseAssetQuantity := big.NewInt(10) + price1 := big.NewInt(9) + blockNumber1 := big.NewInt(2) + salt1 := big.NewInt(time.Now().Unix()) + longOrder1 := createLimitOrder(LONG, userAddress, longOrderBaseAssetQuantity, price1, status, blockNumber1, salt1) + inMemoryDatabase.Add(&longOrder1) + + //long order with price 9 and blockNumber 3 + price2 := big.NewInt(9) + blockNumber2 := big.NewInt(3) + salt2 := big.NewInt(0).Add(salt1, big.NewInt(1)) + longOrder2 := createLimitOrder(LONG, userAddress, longOrderBaseAssetQuantity, price2, status, blockNumber2, salt2) + inMemoryDatabase.Add(&longOrder2) + + //long order with price 10 and blockNumber 3 + price3 := big.NewInt(10) + blockNumber3 := big.NewInt(3) + salt3 := big.NewInt(0).Add(salt2, big.NewInt(1)) + longOrder3 := createLimitOrder(LONG, userAddress, longOrderBaseAssetQuantity, price3, status, blockNumber3, salt3) + inMemoryDatabase.Add(&longOrder3) + + returnedLongOrders := inMemoryDatabase.GetLongOrders(market, nil, nil) + assert.Equal(t, 3, len(returnedLongOrders)) + + //Test returnedLongOrders are sorted by price highest to lowest first and then block number from lowest to highest + assert.Equal(t, price3, returnedLongOrders[0].Price) + assert.Equal(t, blockNumber3, returnedLongOrders[0].BlockNumber) + assert.Equal(t, price1, returnedLongOrders[1].Price) + assert.Equal(t, blockNumber1, returnedLongOrders[1].BlockNumber) + assert.Equal(t, price2, returnedLongOrders[2].Price) + assert.Equal(t, blockNumber2, returnedLongOrders[2].BlockNumber) + + for _, returnedOrder := range returnedLongOrders { + assert.Equal(t, LONG, returnedOrder.PositionType) + assert.Equal(t, userAddress, returnedOrder.Trader.String()) + assert.Equal(t, longOrderBaseAssetQuantity, returnedOrder.BaseAssetQuantity) + assert.Equal(t, status, returnedOrder.getOrderStatus().Status) + } +} + +func TestDeleteOrders(t *testing.T) { + db := getDatabase() + + order1 := createLimitOrder(SHORT, userAddress, big.NewInt(-10), big.NewInt(20), status, big.NewInt(2), big.NewInt(1)) + order2 := createLimitOrder(SHORT, userAddress, big.NewInt(-10), big.NewInt(19), status, big.NewInt(2), big.NewInt(2)) + order3 := createLimitOrder(SHORT, userAddress, big.NewInt(-10), big.NewInt(21), status, big.NewInt(2), big.NewInt(3)) + order4 := createLimitOrder(LONG, userAddress, big.NewInt(10), big.NewInt(20), status, big.NewInt(2), big.NewInt(4)) + order5 := createLimitOrder(LONG, userAddress, big.NewInt(10), big.NewInt(19), status, big.NewInt(2), big.NewInt(5)) + order6 := createLimitOrder(LONG, userAddress, big.NewInt(10), big.NewInt(21), status, big.NewInt(2), big.NewInt(6)) + + db.Add(&order1) + db.Add(&order2) + db.Add(&order3) + db.Add(&order4) + db.Add(&order5) + db.Add(&order6) + + assert.Equal(t, 6, len(db.Orders)) + assert.Equal(t, 3, len(db.ShortOrders[market])) + assert.Equal(t, 3, len(db.LongOrders[market])) + + db.Delete(order1.Id) + assert.Equal(t, 5, len(db.Orders)) + assert.Equal(t, 2, len(db.ShortOrders[market])) + assert.Equal(t, 3, len(db.LongOrders[market])) + assert.Equal(t, -1, getOrderIdx(db.ShortOrders[market], order1.Id)) + assert.Nil(t, db.Orders[order1.Id]) + + db.Delete(order5.Id) + assert.Equal(t, 4, len(db.Orders)) + assert.Equal(t, 2, len(db.ShortOrders[market])) + assert.Equal(t, 2, len(db.LongOrders[market])) + assert.Equal(t, -1, getOrderIdx(db.LongOrders[market], order5.Id)) + assert.Nil(t, db.Orders[order5.Id]) + + db.Delete(order3.Id) + assert.Equal(t, 3, len(db.Orders)) + assert.Equal(t, 1, len(db.ShortOrders[market])) + assert.Equal(t, 2, len(db.LongOrders[market])) + assert.Equal(t, -1, getOrderIdx(db.ShortOrders[market], order3.Id)) + assert.Nil(t, db.Orders[order3.Id]) + + db.Delete(order2.Id) + assert.Equal(t, 2, len(db.Orders)) + assert.Equal(t, 0, len(db.ShortOrders[market])) + assert.Equal(t, 2, len(db.LongOrders[market])) + assert.Equal(t, -1, getOrderIdx(db.ShortOrders[market], order2.Id)) + assert.Nil(t, db.Orders[order2.Id]) +} + +func TestGetCancellableOrders(t *testing.T) { + // also tests getTotalNotionalPositionAndUnrealizedPnl + inMemoryDatabase := getDatabase() + getReservedMargin := func(order Order) *big.Int { + notional := big.NewInt(0).Abs(big.NewInt(0).Div(big.NewInt(0).Mul(order.BaseAssetQuantity, order.Price), hu.ONE_E_18)) + return hu.Div1e6(big.NewInt(0).Mul(notional, inMemoryDatabase.configService.GetMinAllowableMargin())) + } + + blockNumber1 := big.NewInt(2) + baseAssetQuantity := hu.Mul1e18(big.NewInt(-3)) + + salt1 := big.NewInt(101) + price1 := hu.Mul1e6(big.NewInt(10)) + shortOrder1 := createLimitOrder(SHORT, userAddress, baseAssetQuantity, price1, status, blockNumber1, salt1) + + salt2 := big.NewInt(102) + price2 := hu.Mul1e6(big.NewInt(9)) + shortOrder2 := createLimitOrder(SHORT, userAddress, baseAssetQuantity, price2, status, blockNumber1, salt2) + + salt3 := big.NewInt(103) + price3 := hu.Mul1e6(big.NewInt(8)) + shortOrder3 := createLimitOrder(SHORT, userAddress, baseAssetQuantity, price3, status, blockNumber1, salt3) + + depositMargin := hu.Mul1e6(big.NewInt(40)) + inMemoryDatabase.UpdateMargin(trader, HUSD, depositMargin) + + // 3 different short orders with price = 10, 9, 8 + inMemoryDatabase.Add(&shortOrder1) + inMemoryDatabase.UpdateReservedMargin(trader, getReservedMargin(shortOrder1)) + inMemoryDatabase.Add(&shortOrder2) + inMemoryDatabase.UpdateReservedMargin(trader, getReservedMargin(shortOrder2)) + inMemoryDatabase.Add(&shortOrder3) + inMemoryDatabase.UpdateReservedMargin(trader, getReservedMargin(shortOrder3)) + + // 1 fulfilled order at price = 10, size = 9 + size := big.NewInt(0).Mul(big.NewInt(-9), hu.ONE_E_18) + fulfilPrice := hu.Mul1e6(big.NewInt(10)) + inMemoryDatabase.UpdatePosition(trader, market, size, hu.Div1e18(new(big.Int).Mul(new(big.Int).Abs(size), fulfilPrice)), false, 0) + inMemoryDatabase.UpdateLastPrice(market, fulfilPrice) + + // price has moved from 10 to 11 now + priceMap := map[Market]*big.Int{ + market: hu.Mul1e6(big.NewInt(11)), + } + // Setup completed, assertions start here + _trader := inMemoryDatabase.TraderMap[trader] + assert.Equal(t, big.NewInt(0), getTotalFunding(_trader, []Market{market})) + assert.Equal(t, depositMargin, getNormalisedMargin(_trader, assets)) + + // oracle price based notional = 9 * 11 = 99, pnl = -9, mf = (40-9)/99 = 0.31 + notionalPosition, unrealizePnL := getTotalNotionalPositionAndUnrealizedPnl(_trader, depositMargin, hu.Min_Allowable_Margin, priceMap, inMemoryDatabase.GetLastPrices(), []Market{market}) + assert.Equal(t, hu.Mul1e6(big.NewInt(99)), notionalPosition) + assert.Equal(t, hu.Mul1e6(big.NewInt(-9)), unrealizePnL) + + hState := &hu.HubbleState{ + Assets: assets, + OraclePrices: priceMap, + MidPrices: inMemoryDatabase.GetLastPrices(), + ActiveMarkets: []Market{market}, + MinAllowableMargin: inMemoryDatabase.configService.GetMinAllowableMargin(), + MaintenanceMargin: inMemoryDatabase.configService.GetMaintenanceMargin(), + UpgradeVersion: hu.V2, + } + marginFraction := calcMarginFraction(_trader, hState) + assert.Equal(t, new(big.Int).Div(hu.Mul1e6(hu.Add(depositMargin, unrealizePnL)), notionalPosition), marginFraction) + + availableMargin := getAvailableMargin(_trader, hState) + // availableMargin = 40 - 9 - (99 + (10+9+8) * 3)/5 = -5 + assert.Equal(t, hu.Mul1e6(big.NewInt(-5)), availableMargin) + _, ordersToCancel, _ := inMemoryDatabase.GetNaughtyTraders(hState) + + // t.Log("####", "ordersToCancel", ordersToCancel) + assert.Equal(t, 1, len(ordersToCancel)) // only one trader + // orders will be cancelled in the order of price, hence orderId3, 2, 1 + // orderId3 will free up 8*3/5 = 4.8 + // orderId2 will free up 9*3/5 = 5.4 + assert.Equal(t, 2, len(ordersToCancel[trader])) // 2 orders + assert.Equal(t, ordersToCancel[trader][0].Id, shortOrder3.Id) + assert.Equal(t, ordersToCancel[trader][1].Id, shortOrder2.Id) +} + +func TestUpdateFulfilledBaseAssetQuantityLimitOrder(t *testing.T) { + baseAssetQuantity := big.NewInt(-10) + t.Run("when order id does not exist", func(t *testing.T) { + inMemoryDatabase := getDatabase() + filledQuantity := big.NewInt(1) + randomOrderID := common.BigToHash(big.NewInt(1)) + counter := metrics.GetOrRegisterCounter("update_filled_base_asset_quantity_order_id_not_found", nil) + assert.Equal(t, counter.Count(), int64(0)) + + inMemoryDatabase.UpdateFilledBaseAssetQuantity(filledQuantity, randomOrderID, 69) + counter = metrics.GetOrRegisterCounter("update_filled_base_asset_quantity_order_id_not_found", nil) + assert.Equal(t, counter.Count(), int64(1)) + }) + t.Run("when filled quantity is not equal to baseAssetQuantity", func(t *testing.T) { + t.Run("When order type is short order", func(t *testing.T) { + inMemoryDatabase := getDatabase() + salt := big.NewInt(time.Now().Unix()) + limitOrder := createLimitOrder(positionType, userAddress, baseAssetQuantity, price, status, blockNumber, salt) + inMemoryDatabase.Add(&limitOrder) + + filledQuantity := big.NewInt(2) + + inMemoryDatabase.UpdateFilledBaseAssetQuantity(filledQuantity, limitOrder.Id, 69) + updatedLimitOrder := inMemoryDatabase.Orders[limitOrder.Id] + + assert.Equal(t, updatedLimitOrder.FilledBaseAssetQuantity, big.NewInt(0).Neg(filledQuantity)) + assert.Equal(t, updatedLimitOrder.FilledBaseAssetQuantity, filledQuantity.Mul(filledQuantity, big.NewInt(-1))) + }) + t.Run("When order type is long order", func(t *testing.T) { + inMemoryDatabase := getDatabase() + positionType = LONG + baseAssetQuantity = big.NewInt(10) + salt := big.NewInt(time.Now().Unix()) + limitOrder := createLimitOrder(positionType, userAddress, baseAssetQuantity, price, status, blockNumber, salt) + inMemoryDatabase.Add(&limitOrder) + + filledQuantity := big.NewInt(2) + inMemoryDatabase.UpdateFilledBaseAssetQuantity(filledQuantity, limitOrder.Id, 69) + updatedLimitOrder := inMemoryDatabase.Orders[limitOrder.Id] + + assert.Equal(t, updatedLimitOrder.FilledBaseAssetQuantity, filledQuantity) + }) + }) + t.Run("when filled quantity is equal to baseAssetQuantity", func(t *testing.T) { + t.Run("When order type is short order", func(t *testing.T) { + inMemoryDatabase := getDatabase() + salt := big.NewInt(time.Now().Unix()) + limitOrder := createLimitOrder(positionType, userAddress, baseAssetQuantity, price, status, blockNumber, salt) + inMemoryDatabase.Add(&limitOrder) + + filledQuantity := big.NewInt(0).Abs(limitOrder.BaseAssetQuantity) + inMemoryDatabase.UpdateFilledBaseAssetQuantity(filledQuantity, limitOrder.Id, 69) + assert.Equal(t, int64(0), limitOrder.GetUnFilledBaseAssetQuantity().Int64()) + + allOrders := inMemoryDatabase.GetAllOrders() + assert.Equal(t, 1, len(allOrders)) + inMemoryDatabase.Accept(70, 70) + allOrders = inMemoryDatabase.GetAllOrders() + assert.Equal(t, 0, len(allOrders)) + }) + t.Run("When order type is long order", func(t *testing.T) { + inMemoryDatabase := getDatabase() + positionType = LONG + baseAssetQuantity = big.NewInt(10) + salt := big.NewInt(time.Now().Unix()) + limitOrder := createLimitOrder(positionType, userAddress, baseAssetQuantity, price, status, blockNumber, salt) + inMemoryDatabase.Add(&limitOrder) + + filledQuantity := big.NewInt(0).Abs(limitOrder.BaseAssetQuantity) + inMemoryDatabase.UpdateFilledBaseAssetQuantity(filledQuantity, limitOrder.Id, 420) + + assert.Equal(t, int64(0), limitOrder.GetUnFilledBaseAssetQuantity().Int64()) + + allOrders := inMemoryDatabase.GetAllOrders() + assert.Equal(t, 1, len(allOrders)) + inMemoryDatabase.Accept(420, 420) + allOrders = inMemoryDatabase.GetAllOrders() + assert.Equal(t, 0, len(allOrders)) + }) + }) +} + +func TestUpdatePosition(t *testing.T) { + t.Run("When no positions exists for trader, it updates trader map with new positions", func(t *testing.T) { + inMemoryDatabase := getDatabase() + address := common.HexToAddress("0x22Bb736b64A0b4D4081E103f83bccF864F0404aa") + var market Market = 1 + size := big.NewInt(20.00) + openNotional := big.NewInt(200.00) + inMemoryDatabase.UpdatePosition(address, market, size, openNotional, false, 0) + position := inMemoryDatabase.TraderMap[address].Positions[market] + assert.Equal(t, size, position.Size) + assert.Equal(t, openNotional, position.OpenNotional) + }) + t.Run("When positions exists for trader, it overwrites old positions with new data", func(t *testing.T) { + inMemoryDatabase := getDatabase() + address := common.HexToAddress("0x22Bb736b64A0b4D4081E103f83bccF864F0404aa") + var market Market = 1 + size := big.NewInt(20.00) + openNotional := big.NewInt(200.00) + inMemoryDatabase.UpdatePosition(address, market, size, openNotional, false, 0) + + newSize := big.NewInt(25.00) + newOpenNotional := big.NewInt(250.00) + inMemoryDatabase.UpdatePosition(address, market, newSize, newOpenNotional, false, 0) + position := inMemoryDatabase.TraderMap[address].Positions[market] + assert.Equal(t, newSize, position.Size) + assert.Equal(t, newOpenNotional, position.OpenNotional) + }) +} + +func TestUpdateMargin(t *testing.T) { + t.Run("when adding margin for first time it updates margin in tradermap", func(t *testing.T) { + inMemoryDatabase := getDatabase() + address := common.HexToAddress("0x22Bb736b64A0b4D4081E103f83bccF864F0404aa") + var collateral Collateral = 1 + amount := big.NewInt(20.00) + inMemoryDatabase.UpdateMargin(address, collateral, amount) + margin := inMemoryDatabase.TraderMap[address].Margin.Deposited[collateral] + assert.Equal(t, amount, margin) + }) + t.Run("When more margin is added, it updates margin in tradermap", func(t *testing.T) { + inMemoryDatabase := getDatabase() + address := common.HexToAddress("0x22Bb736b64A0b4D4081E103f83bccF864F0404aa") + var collateral Collateral = 1 + amount := big.NewInt(20.00) + inMemoryDatabase.UpdateMargin(address, collateral, amount) + + removedMargin := big.NewInt(15.00) + inMemoryDatabase.UpdateMargin(address, collateral, removedMargin) + margin := inMemoryDatabase.TraderMap[address].Margin.Deposited[collateral] + assert.Equal(t, big.NewInt(0).Add(amount, removedMargin), margin) + }) + t.Run("When margin is removed, it updates margin in tradermap", func(t *testing.T) { + inMemoryDatabase := getDatabase() + address := common.HexToAddress("0x22Bb736b64A0b4D4081E103f83bccF864F0404aa") + var collateral Collateral = 1 + amount := big.NewInt(20.00) + inMemoryDatabase.UpdateMargin(address, collateral, amount) + + removedMargin := big.NewInt(-15.00) + inMemoryDatabase.UpdateMargin(address, collateral, removedMargin) + margin := inMemoryDatabase.TraderMap[address].Margin.Deposited[collateral] + assert.Equal(t, big.NewInt(0).Add(amount, removedMargin), margin) + }) +} + +func TestAccept(t *testing.T) { + t.Run("Order is fulfilled, should be deleted when block is accepted", func(t *testing.T) { + inMemoryDatabase := getDatabase() + orderId1 := addLimitOrder(inMemoryDatabase) + orderId2 := addLimitOrder(inMemoryDatabase) + + err := inMemoryDatabase.SetOrderStatus(orderId1, FulFilled, "", 51) + assert.Nil(t, err) + assert.Equal(t, inMemoryDatabase.Orders[orderId1].getOrderStatus().Status, FulFilled) + + inMemoryDatabase.Accept(51, 51) + + // fulfilled order is deleted + _, ok := inMemoryDatabase.Orders[orderId1] + assert.False(t, ok) + // unfulfilled order still exists + _, ok = inMemoryDatabase.Orders[orderId2] + assert.True(t, ok) + }) + + t.Run("Order is fulfilled, should be deleted when a future block is accepted", func(t *testing.T) { + inMemoryDatabase := getDatabase() + orderId := addLimitOrder(inMemoryDatabase) + err := inMemoryDatabase.SetOrderStatus(orderId, FulFilled, "", 51) + assert.Nil(t, err) + assert.Equal(t, inMemoryDatabase.Orders[orderId].getOrderStatus().Status, FulFilled) + + inMemoryDatabase.Accept(52, 52) + + _, ok := inMemoryDatabase.Orders[orderId] + assert.False(t, ok) + }) + + t.Run("Order is fulfilled, should not be deleted when a past block is accepted", func(t *testing.T) { + inMemoryDatabase := getDatabase() + orderId := addLimitOrder(inMemoryDatabase) + err := inMemoryDatabase.SetOrderStatus(orderId, FulFilled, "", 51) + assert.Nil(t, err) + assert.Equal(t, inMemoryDatabase.Orders[orderId].getOrderStatus().Status, FulFilled) + + inMemoryDatabase.Accept(50, 50) + + _, ok := inMemoryDatabase.Orders[orderId] + assert.True(t, ok) + }) + + t.Run("Order is placed, should not be deleted when a block is accepted", func(t *testing.T) { + inMemoryDatabase := getDatabase() + orderId := addLimitOrder(inMemoryDatabase) + inMemoryDatabase.Accept(50, 50) + + _, ok := inMemoryDatabase.Orders[orderId] + assert.True(t, ok) + }) +} + +func TestRevertLastStatus(t *testing.T) { + t.Run("revert status for order that doesn't exist - expect error", func(t *testing.T) { + inMemoryDatabase := getDatabase() + orderId := common.BytesToHash([]byte("order id")) + err := inMemoryDatabase.RevertLastStatus(orderId) + + assert.Error(t, err) + }) + + t.Run("revert status for placed order", func(t *testing.T) { + inMemoryDatabase := getDatabase() + orderId := addLimitOrder(inMemoryDatabase) + + err := inMemoryDatabase.RevertLastStatus(orderId) + assert.Nil(t, err) + + assert.Equal(t, len(inMemoryDatabase.Orders[orderId].LifecycleList), 0) + }) + + t.Run("revert status for fulfilled order", func(t *testing.T) { + inMemoryDatabase := getDatabase() + orderId := addLimitOrder(inMemoryDatabase) + err := inMemoryDatabase.SetOrderStatus(orderId, FulFilled, "", 3) + assert.Nil(t, err) + + err = inMemoryDatabase.RevertLastStatus(orderId) + assert.Nil(t, err) + + assert.Equal(t, len(inMemoryDatabase.Orders[orderId].LifecycleList), 1) + assert.Equal(t, inMemoryDatabase.Orders[orderId].LifecycleList[0].BlockNumber, uint64(2)) + }) + + t.Run("revert status for accepted + fulfilled order - expect error", func(t *testing.T) { + inMemoryDatabase := getDatabase() + orderId := addLimitOrder(inMemoryDatabase) + err := inMemoryDatabase.SetOrderStatus(orderId, FulFilled, "", 3) + assert.Nil(t, err) + + inMemoryDatabase.Accept(3, 3) + err = inMemoryDatabase.RevertLastStatus(orderId) + assert.Error(t, err) + }) +} + +func TestUpdateUnrealizedFunding(t *testing.T) { + t.Run("When trader has no positions, it does not update anything", func(t *testing.T) { + inMemoryDatabase := getDatabase() + address := common.HexToAddress("0x22Bb736b64A0b4D4081E103f83bccF864F0404aa") + var market Market = 1 + cumulativePremiumFraction := big.NewInt(2) + trader := inMemoryDatabase.TraderMap[address] + inMemoryDatabase.UpdateUnrealisedFunding(market, cumulativePremiumFraction) + updatedTrader := inMemoryDatabase.TraderMap[address] + assert.Equal(t, trader, updatedTrader) + }) + t.Run("When trader has positions", func(t *testing.T) { + t.Run("when unrealized funding is zero, it updates unrealized funding in trader's positions", func(t *testing.T) { + inMemoryDatabase := getDatabase() + addresses := [2]common.Address{common.HexToAddress("0x22Bb736b64A0b4D4081E103f83bccF864F0404aa"), common.HexToAddress("0x710bf5F942331874dcBC7783319123679033b63b")} + var market Market = 1 + openNotional := big.NewInt(200.00) + cumulativePremiumFraction := big.NewInt(0) + for i, address := range addresses { + iterator := i + 1 + size := big.NewInt(int64(20 * iterator)) + inMemoryDatabase.UpdatePosition(address, market, size, openNotional, false, 0) + inMemoryDatabase.ResetUnrealisedFunding(market, address, cumulativePremiumFraction) + } + newCumulativePremiumFraction := big.NewInt(5) + inMemoryDatabase.UpdateUnrealisedFunding(market, newCumulativePremiumFraction) + for _, address := range addresses { + assert.Equal(t, uint64(0), inMemoryDatabase.TraderMap[address].Positions[market].UnrealisedFunding.Uint64()) + } + }) + t.Run("when unrealized funding is not zero, it adds new funding to old unrealized funding in trader's positions", func(t *testing.T) { + inMemoryDatabase := getDatabase() + address := common.HexToAddress("0x22Bb736b64A0b4D4081E103f83bccF864F0404aa") + var market Market = 1 + openNotional := big.NewInt(200.00) + size := big.NewInt(20.00) + inMemoryDatabase.UpdatePosition(address, market, size, openNotional, false, 0) + cumulativePremiumFraction := big.NewInt(2) + inMemoryDatabase.ResetUnrealisedFunding(market, address, cumulativePremiumFraction) + + newCumulativePremiumFraction := big.NewInt(-1) + inMemoryDatabase.UpdateUnrealisedFunding(market, newCumulativePremiumFraction) + newUnrealizedFunding := inMemoryDatabase.TraderMap[address].Positions[market].UnrealisedFunding + expectedUnrealizedFunding := calcPendingFunding(newCumulativePremiumFraction, cumulativePremiumFraction, size) + assert.Equal(t, expectedUnrealizedFunding, newUnrealizedFunding) + }) + }) +} + +func TestResetUnrealisedFunding(t *testing.T) { + t.Run("When trader has no positions, it does not update anything", func(t *testing.T) { + inMemoryDatabase := getDatabase() + address := common.HexToAddress("0x22Bb736b64A0b4D4081E103f83bccF864F0404aa") + var market Market = 1 + trader := inMemoryDatabase.TraderMap[address] + cumulativePremiumFraction := big.NewInt(5) + inMemoryDatabase.ResetUnrealisedFunding(market, address, cumulativePremiumFraction) + updatedTrader := inMemoryDatabase.TraderMap[address] + assert.Equal(t, trader, updatedTrader) + }) + t.Run("When trader has positions, it resets unrealized funding to zero", func(t *testing.T) { + inMemoryDatabase := getDatabase() + address := common.HexToAddress("0x22Bb736b64A0b4D4081E103f83bccF864F0404aa") + var market Market = 1 + openNotional := big.NewInt(200) + size := big.NewInt(20) + inMemoryDatabase.UpdatePosition(address, market, size, openNotional, false, 0) + cumulativePremiumFraction := big.NewInt(1) + inMemoryDatabase.ResetUnrealisedFunding(market, address, cumulativePremiumFraction) + unrealizedFundingFee := inMemoryDatabase.TraderMap[address].Positions[market].UnrealisedFunding + assert.Equal(t, big.NewInt(0), unrealizedFundingFee) + }) +} + +func TestUpdateNextFundingTime(t *testing.T) { + inMemoryDatabase := getDatabase() + nextFundingTime := uint64(time.Now().Unix()) + inMemoryDatabase.UpdateNextFundingTime(nextFundingTime) + assert.Equal(t, nextFundingTime, inMemoryDatabase.NextFundingTime) +} + +func TestGetNextFundingTime(t *testing.T) { + t.Run("when funding time is not set", func(t *testing.T) { + inMemoryDatabase := getDatabase() + assert.Equal(t, uint64(0), inMemoryDatabase.GetNextFundingTime()) + }) + t.Run("when funding time is set", func(t *testing.T) { + inMemoryDatabase := getDatabase() + nextFundingTime := uint64(time.Now().Unix()) + inMemoryDatabase.UpdateNextFundingTime(nextFundingTime) + assert.Equal(t, nextFundingTime, inMemoryDatabase.GetNextFundingTime()) + }) +} + +func TestUpdateLastPrice(t *testing.T) { + inMemoryDatabase := getDatabase() + var market Market = 1 + lastPrice := big.NewInt(20) + inMemoryDatabase.UpdateLastPrice(market, lastPrice) + assert.Equal(t, lastPrice, inMemoryDatabase.LastPrice[market]) +} +func TestGetLastPrice(t *testing.T) { + inMemoryDatabase := getDatabase() + var market Market = 1 + lastPrice := big.NewInt(20) + inMemoryDatabase.UpdateLastPrice(market, lastPrice) + assert.Equal(t, lastPrice, inMemoryDatabase.GetLastPrices()[market]) +} + +func TestUpdateReservedMargin(t *testing.T) { + address := common.HexToAddress("0x22Bb736b64A0b4D4081E103f83bccF864F0404aa") + amount := big.NewInt(20 * 1e6) + inMemoryDatabase := getDatabase() + inMemoryDatabase.UpdateReservedMargin(address, amount) + assert.Equal(t, amount, inMemoryDatabase.TraderMap[address].Margin.Reserved) + + // subtract some amount + amount = big.NewInt(-5 * 1e6) + inMemoryDatabase.UpdateReservedMargin(address, amount) + assert.Equal(t, big.NewInt(15*1e6), inMemoryDatabase.TraderMap[address].Margin.Reserved) +} + +func createLimitOrder(positionType PositionType, userAddress string, baseAssetQuantity *big.Int, price *big.Int, status Status, blockNumber *big.Int, salt *big.Int) Order { + lo := Order{ + Market: market, + PositionType: positionType, + Trader: common.HexToAddress(userAddress), + FilledBaseAssetQuantity: big.NewInt(0), + BaseAssetQuantity: baseAssetQuantity, + Price: price, + Salt: salt, + BlockNumber: blockNumber, + ReduceOnly: false, + } + lo.Id = getIdFromOrder(lo) + return lo +} + +func createIOCOrder(positionType PositionType, userAddress string, baseAssetQuantity *big.Int, price *big.Int, status Status, blockNumber *big.Int, salt *big.Int, expireDuration *big.Int) Order { + now := big.NewInt(time.Now().Unix()) + expireAt := big.NewInt(0).Add(now, expireDuration) + ioc := Order{ + OrderType: IOC, + Market: market, + PositionType: positionType, + Trader: common.HexToAddress(userAddress), + FilledBaseAssetQuantity: big.NewInt(0), + BaseAssetQuantity: baseAssetQuantity, + Price: price, + Salt: salt, + BlockNumber: blockNumber, + ReduceOnly: false, + RawOrder: &IOCOrder{ + OrderType: uint8(IOC), + ExpireAt: expireAt, + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: common.HexToAddress(userAddress), + BaseAssetQuantity: baseAssetQuantity, + Price: price, + Salt: salt, + ReduceOnly: false, + }, + }} + + // it's incorrect but should not affect the test results + ioc.Id = getIdFromOrder(ioc) + return ioc +} + +func TestGetUnfilledBaseAssetQuantity(t *testing.T) { + t.Run("When limit FilledBaseAssetQuantity is zero, it returns BaseAssetQuantity", func(t *testing.T) { + baseAssetQuantityLongOrder := big.NewInt(10) + salt1 := big.NewInt(time.Now().Unix()) + longOrder := createLimitOrder(LONG, "0x22Bb736b64A0b4D4081E103f83bccF864F0404aa", baseAssetQuantityLongOrder, big.NewInt(21), Placed, big.NewInt(2), salt1) + longOrder.FilledBaseAssetQuantity = big.NewInt(0) + //baseAssetQuantityLongOrder - filledBaseAssetQuantity + expectedUnFilledForLongOrder := big.NewInt(10) + assert.Equal(t, expectedUnFilledForLongOrder, longOrder.GetUnFilledBaseAssetQuantity()) + + baseAssetQuantityShortOrder := big.NewInt(-10) + salt2 := big.NewInt(0).Add(salt1, big.NewInt(1)) + shortOrder := createLimitOrder(SHORT, "0x22Bb736b64A0b4D4081E103f83bccF864F0404aa", baseAssetQuantityShortOrder, big.NewInt(21), Placed, big.NewInt(2), salt2) + shortOrder.FilledBaseAssetQuantity = big.NewInt(0) + //baseAssetQuantityLongOrder - filledBaseAssetQuantity + expectedUnFilledForShortOrder := big.NewInt(-10) + assert.Equal(t, expectedUnFilledForShortOrder, shortOrder.GetUnFilledBaseAssetQuantity()) + }) + t.Run("When limit FilledBaseAssetQuantity is not zero, it returns BaseAssetQuantity - FilledBaseAssetQuantity", func(t *testing.T) { + baseAssetQuantityLongOrder := big.NewInt(10) + salt1 := big.NewInt(time.Now().Unix()) + longOrder := createLimitOrder(LONG, "0x22Bb736b64A0b4D4081E103f83bccF864F0404aa", baseAssetQuantityLongOrder, big.NewInt(21), Placed, big.NewInt(2), salt1) + longOrder.FilledBaseAssetQuantity = big.NewInt(5) + //baseAssetQuantityLongOrder - filledBaseAssetQuantity + expectedUnFilledForLongOrder := big.NewInt(5) + assert.Equal(t, expectedUnFilledForLongOrder, longOrder.GetUnFilledBaseAssetQuantity()) + + baseAssetQuantityShortOrder := big.NewInt(-10) + salt2 := big.NewInt(0).Add(salt1, big.NewInt(1)) + shortOrder := createLimitOrder(SHORT, "0x22Bb736b64A0b4D4081E103f83bccF864F0404aa", baseAssetQuantityShortOrder, big.NewInt(21), Placed, big.NewInt(2), salt2) + shortOrder.FilledBaseAssetQuantity = big.NewInt(-5) + //baseAssetQuantityLongOrder - filledBaseAssetQuantity + expectedUnFilledForShortOrder := big.NewInt(-5) + assert.Equal(t, expectedUnFilledForShortOrder, shortOrder.GetUnFilledBaseAssetQuantity()) + }) +} + +func addLimitOrder(db *InMemoryDatabase) common.Hash { + salt := big.NewInt(time.Now().Unix() + int64(rand.Intn(200))) + limitOrder := createLimitOrder(positionType, userAddress, big.NewInt(50), price, status, blockNumber, salt) + db.Add(&limitOrder) + return limitOrder.Id +} + +func TestSampleImpactPrice(t *testing.T) { + db := getDatabase() + t.Run("insufficient liquidity on both sides", func(t *testing.T) { + t.Run("no orders on both sides", func(t *testing.T) { + impactBids, impactAsks, midPrices := db.SampleImpactPrice() + assert.Equal(t, big.NewInt(0), impactBids[0]) + assert.Equal(t, big.NewInt(0), impactAsks[0]) + assert.Equal(t, big.NewInt(0), midPrices[0]) + }) + t.Run("no orders on SHORT, insufficient liquidity on LONG", func(t *testing.T) { + order1 := createLimitOrder(LONG, "0x22Bb736b64A0b4D4081E103f83bccF864F0404aa", big.NewInt(1), big.NewInt(400), Placed, big.NewInt(2), big.NewInt(2)) + db.Add(&order1) + impactBids, impactAsks, midPrices := db.SampleImpactPrice() + + assert.Equal(t, big.NewInt(0), impactBids[0]) + assert.Equal(t, big.NewInt(0), impactAsks[0]) + assert.Equal(t, big.NewInt(0), midPrices[0]) + + db.Delete(order1.Id) + }) + t.Run("no orders on LONG, insufficient liquidity on SHORT", func(t *testing.T) { + order1 := createLimitOrder(SHORT, "0x22Bb736b64A0b4D4081E103f83bccF864F0404aa", big.NewInt(-1), big.NewInt(400), Placed, big.NewInt(2), big.NewInt(2)) + db.Add(&order1) + impactBids, impactAsks, midPrices := db.SampleImpactPrice() + + assert.Equal(t, big.NewInt(0), impactBids[0]) + assert.Equal(t, big.NewInt(0), impactAsks[0]) + assert.Equal(t, big.NewInt(0), midPrices[0]) + + db.Delete(order1.Id) + }) + + t.Run("insufficient liquidity on both sides", func(t *testing.T) { + order1 := createLimitOrder(LONG, "0x22Bb736b64A0b4D4081E103f83bccF864F0404aa", big.NewInt(1), big.NewInt(499), Placed, big.NewInt(2), big.NewInt(2)) + db.Add(&order1) + order2 := createLimitOrder(SHORT, "0x22Bb736b64A0b4D4081E103f83bccF864F0404aa", big.NewInt(-1), big.NewInt(400), Placed, big.NewInt(2), big.NewInt(2)) + db.Add(&order2) + + impactBids, impactAsks, midPrices := db.SampleImpactPrice() + + assert.Equal(t, big.NewInt(0), impactBids[0]) + assert.Equal(t, big.NewInt(0), impactAsks[0]) + assert.Equal(t, big.NewInt(449), midPrices[0]) + + db.Delete(order1.Id) + db.Delete(order2.Id) + }) + }) + + t.Run("insufficient liquidity on 1 side", func(t *testing.T) { + db := getDatabase() + t.Run("sufficient liquidity on SHORT", func(t *testing.T) { + t.Run("no orders on long", func(t *testing.T) { + order2 := createLimitOrder(SHORT, "0x22Bb736b64A0b4D4081E103f83bccF864F0404aa", big.NewInt(-1e18), hu.Mul1e6(big.NewInt(500)), Placed, big.NewInt(2), big.NewInt(2)) + db.Add(&order2) + impactBids, impactAsks, midPrices := db.SampleImpactPrice() + + assert.Equal(t, big.NewInt(0), impactBids[0]) + assert.Equal(t, hu.Mul1e6(big.NewInt(500)), impactAsks[0]) + assert.Equal(t, big.NewInt(0), midPrices[0]) + + db.Delete(order2.Id) + }) + + t.Run("insufficient liquidity on LONG", func(t *testing.T) { + order1 := createLimitOrder(LONG, "0x22Bb736b64A0b4D4081E103f83bccF864F0404aa", big.NewInt(1e18), hu.Mul1e6(big.NewInt(490)), Placed, big.NewInt(2), big.NewInt(2)) + db.Add(&order1) + order2 := createLimitOrder(SHORT, "0x22Bb736b64A0b4D4081E103f83bccF864F0404aa", big.NewInt(-1e18), hu.Mul1e6(big.NewInt(500)), Placed, big.NewInt(2), big.NewInt(2)) + db.Add(&order2) + impactBids, impactAsks, midPrices := db.SampleImpactPrice() + + assert.Equal(t, big.NewInt(0), impactBids[0]) + assert.Equal(t, hu.Mul1e6(big.NewInt(500)), impactAsks[0]) + assert.Equal(t, hu.Mul1e6(big.NewInt(495)), midPrices[0]) + + db.Delete(order1.Id) + db.Delete(order2.Id) + }) + }) + + t.Run("sufficient liquidity on LONG", func(t *testing.T) { + t.Run("no orders on short", func(t *testing.T) { + order2 := createLimitOrder(LONG, "0x22Bb736b64A0b4D4081E103f83bccF864F0404aa", big.NewInt(1e18), hu.Mul1e6(big.NewInt(500)), Placed, big.NewInt(2), big.NewInt(2)) + db.Add(&order2) + impactBids, impactAsks, midPrices := db.SampleImpactPrice() + + assert.Equal(t, hu.Mul1e6(big.NewInt(500)), impactBids[0]) + assert.Equal(t, big.NewInt(0), impactAsks[0]) + assert.Equal(t, big.NewInt(0), midPrices[0]) + + db.Delete(order2.Id) + }) + + t.Run("insufficient liquidity on short", func(t *testing.T) { + order1 := createLimitOrder(LONG, "0x22Bb736b64A0b4D4081E103f83bccF864F0404aa", big.NewInt(1e18), hu.Mul1e6(big.NewInt(500)), Placed, big.NewInt(2), big.NewInt(2)) + db.Add(&order1) + order2 := createLimitOrder(SHORT, "0x22Bb736b64A0b4D4081E103f83bccF864F0404aa", big.NewInt(-1e18), hu.Mul1e6(big.NewInt(490)), Placed, big.NewInt(2), big.NewInt(2)) + db.Add(&order2) + impactBids, impactAsks, midPrices := db.SampleImpactPrice() + + assert.Equal(t, hu.Mul1e6(big.NewInt(500)), impactBids[0]) + assert.Equal(t, big.NewInt(0), impactAsks[0]) + assert.Equal(t, hu.Mul1e6(big.NewInt(495)), midPrices[0]) + + db.Delete(order1.Id) + db.Delete(order2.Id) + }) + }) + }) + + t.Run("sufficient liquidity on both sides", func(t *testing.T) { + t.Run("just 1 order", func(t *testing.T) { + order1 := createLimitOrder(LONG, "0x22Bb736b64A0b4D4081E103f83bccF864F0404aa", big.NewInt(1e18), hu.Mul1e6(big.NewInt(500)), Placed, big.NewInt(2), big.NewInt(2)) + db.Add(&order1) + order2 := createLimitOrder(SHORT, "0x22Bb736b64A0b4D4081E103f83bccF864F0404aa", big.NewInt(-1e18), hu.Mul1e6(big.NewInt(510)), Placed, big.NewInt(2), big.NewInt(2)) + db.Add(&order2) + impactBids, impactAsks, midPrices := db.SampleImpactPrice() + + assert.Equal(t, hu.Mul1e6(big.NewInt(500)), impactBids[0]) + assert.Equal(t, hu.Mul1e6(big.NewInt(510)), impactAsks[0]) + assert.Equal(t, hu.Mul1e6(big.NewInt(505)), midPrices[0]) + + db.Delete(order1.Id) + db.Delete(order2.Id) + }) + + t.Run("multiple orders", func(t *testing.T) { + // 600*.5 + 520*.1 + 500*.4 = 552 + // accNotional = 600*.5 + 520*.1 + 500*.296 = 500 + // impact price = 500/(.5 + .1 + .296) = 558.035714 + order1 := createLimitOrder(LONG, "0x22Bb736b64A0b4D4081E103f83bccF864F0404aa", big.NewInt(5e17), hu.Mul1e6(big.NewInt(600)), Placed, big.NewInt(2), big.NewInt(2)) + db.Add(&order1) + order2 := createLimitOrder(LONG, "0x22Bb736b64A0b4D4081E103f83bccF864F0404aa", big.NewInt(1e17), hu.Mul1e6(big.NewInt(520)), Placed, big.NewInt(2), big.NewInt(2)) + db.Add(&order2) + order3 := createLimitOrder(LONG, "0x22Bb736b64A0b4D4081E103f83bccF864F0404aa", big.NewInt(4e17), hu.Mul1e6(big.NewInt(500)), Placed, big.NewInt(2), big.NewInt(2)) + db.Add(&order3) + + // 700*.5 + 750*.1 + 800*.4 = 745 + // accNotional = 700*.5 + 750*.1 = 425 + // impact price = 500/(.5 + .1 + .09375) = 720.720720 + order4 := createLimitOrder(SHORT, "0x22Bb736b64A0b4D4081E103f83bccF864F0404aa", big.NewInt(5e17), hu.Mul1e6(big.NewInt(700)), Placed, big.NewInt(2), big.NewInt(2)) + db.Add(&order4) + order5 := createLimitOrder(SHORT, "0x22Bb736b64A0b4D4081E103f83bccF864F0404aa", big.NewInt(1e17), hu.Mul1e6(big.NewInt(750)), Placed, big.NewInt(2), big.NewInt(2)) + db.Add(&order5) + order6 := createLimitOrder(SHORT, "0x22Bb736b64A0b4D4081E103f83bccF864F0404aa", big.NewInt(4e17), hu.Mul1e6(big.NewInt(800)), Placed, big.NewInt(2), big.NewInt(2)) + db.Add(&order6) + + impactBids, impactAsks, midPrices := db.SampleImpactPrice() + + assert.Equal(t, big.NewInt(558035714), impactBids[0]) + assert.Equal(t, big.NewInt(720720720), impactAsks[0]) + assert.Equal(t, hu.Mul1e6(big.NewInt(650)), midPrices[0]) + + db.Delete(order1.Id) + db.Delete(order2.Id) + db.Delete(order3.Id) + db.Delete(order4.Id) + db.Delete(order5.Id) + db.Delete(order6.Id) + }) + }) +} + +func TestGetOrderValidationFields(t *testing.T) { + db := getDatabase() + + t.Run("bidsHead is unaffected by IOC orders", func(t *testing.T) { + signedOrder := &hu.SignedOrder{ + LimitOrder: LimitOrder{ + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8"), + BaseAssetQuantity: big.NewInt(-5000000000000000000), + Price: big.NewInt(1000000000), + Salt: big.NewInt(1688994806105), + ReduceOnly: false, + }, + PostOnly: true, + }, + OrderType: 2, + ExpireAt: big.NewInt(1688994854), + } + orderId, _ := signedOrder.Hash() + + // no orders, ask and bids head should be 0 + fields := db.GetOrderValidationFields(orderId, signedOrder) + assert.Equal(t, big.NewInt(0), fields.BidsHead) + assert.Equal(t, big.NewInt(0), fields.AsksHead) + + // send a bid at $100 + order1 := createLimitOrder(LONG, "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", big.NewInt(1), big.NewInt(100), Placed, big.NewInt(2), big.NewInt(1688994806105)) + db.Add(&order1) + fields = db.GetOrderValidationFields(orderId, signedOrder) + assert.Equal(t, big.NewInt(100), fields.BidsHead) + assert.Equal(t, big.NewInt(0), fields.AsksHead) + + // send a market market bid at $101 + // assert that bidsHead remains at $101 so signed orders at (100, 101) can be accepted and matched + order2 := createIOCOrder(LONG, "0x22Bb736b64A0b4D4081E103f83bccF864F0404aa", big.NewInt(1e18), big.NewInt(101), Placed, big.NewInt(2), big.NewInt(2), big.NewInt(10)) + db.Add(&order2) + fields = db.GetOrderValidationFields(orderId, signedOrder) + assert.Equal(t, big.NewInt(100), fields.BidsHead) + assert.Equal(t, big.NewInt(0), fields.AsksHead) + + db.Delete(order1.Id) + db.Delete(order2.Id) + }) + + t.Run("asksHead is unaffected by IOC orders", func(t *testing.T) { + signedOrder := &hu.SignedOrder{ + LimitOrder: LimitOrder{ + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8"), + BaseAssetQuantity: big.NewInt(5000000000000000000), + Price: big.NewInt(1000000000), + Salt: big.NewInt(1688994806105), + ReduceOnly: false, + }, + PostOnly: true, + }, + OrderType: 2, + ExpireAt: big.NewInt(1688994854), + } + orderId, _ := signedOrder.Hash() + + // no orders, ask and bids head should be 0 + fields := db.GetOrderValidationFields(orderId, signedOrder) + assert.Equal(t, big.NewInt(0), fields.BidsHead) + assert.Equal(t, big.NewInt(0), fields.AsksHead) + + // send a bid at $100 + order1 := createLimitOrder(SHORT, "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", big.NewInt(-1), big.NewInt(100), Placed, big.NewInt(2), big.NewInt(1688994806105)) + db.Add(&order1) + fields = db.GetOrderValidationFields(orderId, signedOrder) + assert.Equal(t, big.NewInt(0), fields.BidsHead) + assert.Equal(t, big.NewInt(100), fields.AsksHead) + + // send a market market bid at $101 + // assert that bidsHead remains at $101 so signed orders at (100, 101) can be accepted and matched + order2 := createIOCOrder(SHORT, "0x22Bb736b64A0b4D4081E103f83bccF864F0404aa", big.NewInt(-1), big.NewInt(99), Placed, big.NewInt(2), big.NewInt(2), big.NewInt(10)) + db.Add(&order2) + fields = db.GetOrderValidationFields(orderId, signedOrder) + assert.Equal(t, big.NewInt(0), fields.BidsHead) + assert.Equal(t, big.NewInt(100), fields.AsksHead) + + db.Delete(order1.Id) + db.Delete(order2.Id) + }) +} diff --git a/plugin/evm/orderbook/metrics.go b/plugin/evm/orderbook/metrics.go new file mode 100644 index 0000000000..8713d7ed15 --- /dev/null +++ b/plugin/evm/orderbook/metrics.go @@ -0,0 +1,50 @@ +package orderbook + +import ( + "github.com/ava-labs/subnet-evm/metrics" +) + +var ( + transactionsPerBlockHistogram = metrics.NewRegisteredHistogram("transactions/rate", nil, metrics.ResettingSample(metrics.NewExpDecaySample(1028, 0.015))) + + gasUsedPerBlockHistogram = metrics.NewRegisteredHistogram("gas_used_per_block", nil, metrics.ResettingSample(metrics.NewExpDecaySample(1028, 0.015))) + blockGasCostPerBlockHistogram = metrics.NewRegisteredHistogram("block_gas_cost", nil, metrics.ResettingSample(metrics.NewExpDecaySample(1028, 0.015))) + + ordersPlacedPerBlock = metrics.NewRegisteredHistogram("orders_placed_per_block", nil, metrics.ResettingSample(metrics.NewExpDecaySample(1028, 0.015))) + ordersCancelledPerBlock = metrics.NewRegisteredHistogram("orders_cancelled_per_block", nil, metrics.ResettingSample(metrics.NewExpDecaySample(1028, 0.015))) + + // only valid for OrderBook transactions send by this validator + orderBookTransactionsSuccessTotalCounter = metrics.NewRegisteredCounter("orderbooktxs/total/success", nil) + orderBookTransactionsFailureTotalCounter = metrics.NewRegisteredCounter("orderbooktxs/total/failure", nil) + + // panics are recovered but monitored + AllPanicsCounter = metrics.NewRegisteredCounter("all_panics", nil) + RunMatchingPipelinePanicsCounter = metrics.NewRegisteredCounter("matching_pipeline_panics", nil) + RunSanitaryPipelinePanicsCounter = metrics.NewRegisteredCounter("sanitary_pipeline_panics", nil) + HandleHubbleFeedLogsPanicsCounter = metrics.NewRegisteredCounter("handle_hubble_feed_logs_panics", nil) + HandleChainAcceptedLogsPanicsCounter = metrics.NewRegisteredCounter("handle_chain_accepted_logs_panics", nil) + SaveSnapshotPanicsCounter = metrics.NewRegisteredCounter("save_snapshot_panics", nil) + HandleChainAcceptedEventPanicsCounter = metrics.NewRegisteredCounter("handle_chain_accepted_event_panics", nil) + HandleMatchingPipelineTimerPanicsCounter = metrics.NewRegisteredCounter("handle_matching_pipeline_timer_panics", nil) + RPCPanicsCounter = metrics.NewRegisteredCounter("rpc_panic", nil) + AwaitSignedOrdersGossipPanicsCounter = metrics.NewRegisteredCounter("await_signed_orders_gossip_panics", nil) + MakerbookFileWriteChannelPanicsCounter = metrics.NewRegisteredCounter("makerbook_file_write_channel_panics", nil) + + BuildBlockFailedWithLowBlockGasCounter = metrics.NewRegisteredCounter("build_block_failed_low_block_gas", nil) + + // lag between head and accepted block + headBlockLagHistogram = metrics.NewRegisteredHistogram("head_block_lag", nil, metrics.ResettingSample(metrics.NewExpDecaySample(1028, 0.015))) + + // order id not found while deleting + deleteOrderIdNotFoundCounter = metrics.NewRegisteredCounter("delete_order_id_not_found", nil) + + // unquenched liquidations + unquenchedLiquidationsCounter = metrics.NewRegisteredCounter("unquenched_liquidations", nil) + placeSignedOrderCounter = metrics.NewRegisteredCounter("place_signed_order", nil) + + // makerbook write failures + makerBookWriteFailuresCounter = metrics.NewRegisteredCounter("makerbook_write_failures", nil) + + // snapshot write failures + SnapshotWriteFailuresCounter = metrics.NewRegisteredCounter("snapshot_write_failures", nil) +) diff --git a/plugin/evm/orderbook/mocks.go b/plugin/evm/orderbook/mocks.go new file mode 100644 index 0000000000..0d4a153d18 --- /dev/null +++ b/plugin/evm/orderbook/mocks.go @@ -0,0 +1,361 @@ +package orderbook + +import ( + "math/big" + + "github.com/ava-labs/subnet-evm/core/types" + hu "github.com/ava-labs/subnet-evm/plugin/evm/orderbook/hubbleutils" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/mock" +) + +type MockLimitOrderDatabase struct { + mock.Mock +} + +func NewMockLimitOrderDatabase() *MockLimitOrderDatabase { + return &MockLimitOrderDatabase{} +} + +func (db *MockLimitOrderDatabase) SetOrderStatus(orderId common.Hash, status Status, info string, blockNumber uint64) error { + return nil +} + +func (db *MockLimitOrderDatabase) RevertLastStatus(orderId common.Hash) error { + return nil +} + +func (db *MockLimitOrderDatabase) Accept(blockNumber uint64, blockTimestamp uint64) { +} + +func (db *MockLimitOrderDatabase) GetAllOrders() []Order { + args := db.Called() + return args.Get(0).([]Order) +} + +func (db *MockLimitOrderDatabase) GetMarketOrders(market Market) []Order { + args := db.Called() + return args.Get(0).([]Order) +} + +func (db *MockLimitOrderDatabase) Add(order *Order) { +} + +func (db *MockLimitOrderDatabase) AddSignedOrder(order *Order, requiredMargin *big.Int) { +} + +func (db *MockLimitOrderDatabase) UpdateFilledBaseAssetQuantity(quantity *big.Int, orderId common.Hash, blockNumber uint64) { +} + +func (db *MockLimitOrderDatabase) Delete(id common.Hash) { +} + +func (db *MockLimitOrderDatabase) GetLongOrders(market Market, lowerbound *big.Int, blockNumber *big.Int) []Order { + args := db.Called() + return args.Get(0).([]Order) +} + +func (db *MockLimitOrderDatabase) GetShortOrders(market Market, upperbound *big.Int, blockNumber *big.Int) []Order { + args := db.Called() + return args.Get(0).([]Order) +} + +func (db *MockLimitOrderDatabase) UpdatePosition(trader common.Address, market Market, size *big.Int, openNotional *big.Int, isLiquidation bool, blockNumber uint64) { +} + +func (db *MockLimitOrderDatabase) UpdateMargin(trader common.Address, collateral Collateral, addAmount *big.Int) { +} + +func (db *MockLimitOrderDatabase) UpdateReservedMargin(trader common.Address, addAmount *big.Int) { +} + +func (db *MockLimitOrderDatabase) UpdateUnrealisedFunding(market Market, fundingRate *big.Int) { +} + +func (db *MockLimitOrderDatabase) ResetUnrealisedFunding(market Market, trader common.Address, cumulativePremiumFraction *big.Int) { +} + +func (db *MockLimitOrderDatabase) UpdateNextFundingTime(uint64) { +} + +func (db *MockLimitOrderDatabase) GetNextFundingTime() uint64 { + return 0 +} + +func (db *MockLimitOrderDatabase) GetAllTraders() map[common.Address]Trader { + args := db.Called() + return args.Get(0).(map[common.Address]Trader) +} + +func (db *MockLimitOrderDatabase) UpdateLastPrice(market Market, lastPrice *big.Int) {} + +func (db *MockLimitOrderDatabase) GetInProgressBlocks() []*types.Block { + return []*types.Block{} +} + +func (db *MockLimitOrderDatabase) UpdateInProgressState(block *types.Block, quantityMap map[string]*big.Int) { +} + +func (db *MockLimitOrderDatabase) RemoveInProgressState(block *types.Block, quantityMap map[string]*big.Int) { +} + +func (db *MockLimitOrderDatabase) GetLastPrice(market Market) *big.Int { + args := db.Called() + return args.Get(0).(*big.Int) +} + +func (db *MockLimitOrderDatabase) UpdateNextSamplePITime(nextSamplePITime uint64) {} + +func (db *MockLimitOrderDatabase) GetNextSamplePITime() uint64 { + return 0 +} + +func (db *MockLimitOrderDatabase) GetOrdersToCancel(oraclePrice map[Market]*big.Int) map[common.Address][]common.Hash { + args := db.Called() + return args.Get(0).(map[common.Address][]common.Hash) +} + +func (db *MockLimitOrderDatabase) GetLastPrices() map[Market]*big.Int { + return map[Market]*big.Int{} +} + +func (db *MockLimitOrderDatabase) GetNaughtyTraders(hState *hu.HubbleState) ([]LiquidablePosition, map[common.Address][]Order, map[common.Address]*big.Int) { + return []LiquidablePosition{}, map[common.Address][]Order{}, map[common.Address]*big.Int{} +} + +func (db *MockLimitOrderDatabase) GetOrderBookData() InMemoryDatabase { + return InMemoryDatabase{} +} + +func (db *MockLimitOrderDatabase) GetOrderBookDataCopy() (*InMemoryDatabase, error) { + return &InMemoryDatabase{}, nil +} + +func (db *MockLimitOrderDatabase) LoadFromSnapshot(snapshot Snapshot) error { + return nil +} + +func (db *MockLimitOrderDatabase) GetAllOpenOrdersForTrader(trader common.Address) []Order { + return nil +} + +func (db *MockLimitOrderDatabase) GetOpenOrdersForTraderByType(trader common.Address, orderType OrderType) []Order { + return nil +} + +func (db *MockLimitOrderDatabase) UpdateLastPremiumFraction(market Market, trader common.Address, lastPremiumFraction *big.Int, cumulativePremiumFraction *big.Int) { +} + +func (db *MockLimitOrderDatabase) GetOrderById(id common.Hash) *Order { + return nil +} + +func (db *MockLimitOrderDatabase) GetTraderInfo(trader common.Address) *Trader { + return &Trader{} +} + +func (db *MockLimitOrderDatabase) GetSamplePIAttemptedTime() uint64 { + return 0 +} + +func (db *MockLimitOrderDatabase) SignalSamplePIAttempted(time uint64) {} + +func (db *MockLimitOrderDatabase) GetOrderValidationFields(orderId common.Hash, order *hu.SignedOrder) OrderValidationFields { + return OrderValidationFields{} +} + +func (db *MockLimitOrderDatabase) SampleImpactPrice() (impactBids, impactAsks, midPrices []*big.Int) { + return []*big.Int{}, []*big.Int{}, []*big.Int{} +} + +func (db *MockLimitOrderDatabase) RemoveExpiredSignedOrders() {} + +func (db *MockLimitOrderDatabase) GetMarginAvailableForMakerbook(trader common.Address, prices map[int]*big.Int) *big.Int { + return big.NewInt(0) +} + +type MockLimitOrderTxProcessor struct { + mock.Mock +} + +func NewMockLimitOrderTxProcessor() *MockLimitOrderTxProcessor { + return &MockLimitOrderTxProcessor{} +} + +func (lotp *MockLimitOrderTxProcessor) ExecuteMatchedOrdersTx(incomingOrder Order, matchedOrder Order, fillAmount *big.Int) error { + args := lotp.Called(incomingOrder, matchedOrder, fillAmount) + return args.Error(0) +} + +func (lotp *MockLimitOrderTxProcessor) PurgeOrderBookTxs() { + lotp.Called() +} + +func (lotp *MockLimitOrderTxProcessor) SetOrderBookTxsBlockNumber(blockNumber uint64) { + lotp.Called() +} + +func (lotp *MockLimitOrderTxProcessor) GetOrderBookTxsCount() uint64 { + args := lotp.Called() + return uint64(args.Int(0)) +} + +func (lotp *MockLimitOrderTxProcessor) GetOrderBookTxs() map[common.Address]types.Transactions { + return nil +} + +func (lotp *MockLimitOrderTxProcessor) ExecuteFundingPaymentTx() error { + return nil +} + +func (lotp *MockLimitOrderTxProcessor) ExecuteSamplePITx() error { + return nil +} + +func (lotp *MockLimitOrderTxProcessor) ExecuteLiquidation(trader common.Address, matchedOrder Order, fillAmount *big.Int) error { + args := lotp.Called(trader, matchedOrder, fillAmount) + return args.Error(0) +} + +func (lotp *MockLimitOrderTxProcessor) ExecuteLimitOrderCancel(orderIds []LimitOrder) error { + args := lotp.Called(orderIds) + return args.Error(0) +} + +func (lotp *MockLimitOrderTxProcessor) HandleOrderBookEvent(event *types.Log) { +} + +func (lotp *MockLimitOrderTxProcessor) HandleMarginAccountEvent(event *types.Log) { +} + +func (lotp *MockLimitOrderTxProcessor) HandleClearingHouseEvent(event *types.Log) { +} + +func (lotp *MockLimitOrderTxProcessor) GetUnderlyingPrice() (map[Market]*big.Int, error) { + return nil, nil +} + +func (lotp *MockLimitOrderTxProcessor) UpdateMetrics(block *types.Block) { + lotp.Called() +} + +type MockConfigService struct { + mock.Mock +} + +func (mcs *MockConfigService) GetAcceptableBounds(market Market) (*big.Int, *big.Int) { + args := mcs.Called() + return args.Get(0).(*big.Int), args.Get(1).(*big.Int) +} + +func (mcs *MockConfigService) GetAcceptableBoundsForLiquidation(market Market) (*big.Int, *big.Int) { + args := mcs.Called(market) + return args.Get(0).(*big.Int), args.Get(1).(*big.Int) +} + +func (mcs *MockConfigService) getLiquidationSpreadThreshold(market Market) *big.Int { + return big.NewInt(1e4) +} + +func (mcs *MockConfigService) getMaxLiquidationRatio(market Market) *big.Int { + args := mcs.Called() + return args.Get(0).(*big.Int) +} + +func (mcs *MockConfigService) GetMinAllowableMargin() *big.Int { + args := mcs.Called() + return args.Get(0).(*big.Int) +} + +func (mcs *MockConfigService) GetMaintenanceMargin() *big.Int { + args := mcs.Called() + return args.Get(0).(*big.Int) +} + +func (mcs *MockConfigService) getMinSizeRequirement(market Market) *big.Int { + return big.NewInt(1) +} + +func (cs *MockConfigService) GetActiveMarketsCount() int64 { + return int64(1) +} + +func (cs *MockConfigService) GetUnderlyingPrices() []*big.Int { + return []*big.Int{} +} + +func (cs *MockConfigService) GetMidPrices() []*big.Int { + return []*big.Int{} +} + +func (cs *MockConfigService) GetSettlementPrices() []*big.Int { + return []*big.Int{} +} + +func (cs *MockConfigService) GetMarketsIncludingSettled() []common.Address { + return []common.Address{} +} + +func (cs *MockConfigService) GetLastPremiumFraction(market Market, trader *common.Address) *big.Int { + return big.NewInt(0) +} + +func (cs *MockConfigService) GetLastPremiumFractionAtBlock(market Market, trader *common.Address, blockNumber uint64) *big.Int { + return big.NewInt(0) +} + +func (cs *MockConfigService) GetCumulativePremiumFraction(market Market) *big.Int { + return big.NewInt(0) +} + +func (cs *MockConfigService) GetCumulativePremiumFractionAtBlock(market Market, blockNumber uint64) *big.Int { + return big.NewInt(0) +} + +func (cs *MockConfigService) GetCollaterals() []hu.Collateral { + return []hu.Collateral{{Price: big.NewInt(1e6), Weight: big.NewInt(1e6), Decimals: 6}} +} + +func (cs *MockConfigService) GetTakerFee() *big.Int { + return big.NewInt(0) +} + +func (cs *MockConfigService) HasReferrer(trader common.Address) bool { + return true +} + +func (cs *MockConfigService) GetPriceMultiplier(market Market) *big.Int { + return big.NewInt(1e6) +} + +func (cs *MockConfigService) GetSignedOrderStatus(orderHash common.Hash) int64 { + return 0 +} + +func (cs *MockConfigService) IsTradingAuthority(trader, signer common.Address) bool { + return false +} + +func NewMockConfigService() *MockConfigService { + return &MockConfigService{} +} + +func (cs *MockConfigService) GetSignedOrderbookContract() common.Address { + return common.Address{} +} + +func (cs *MockConfigService) GetMarketAddressFromMarketID(marketId int64) common.Address { + return common.Address{} +} + +func (cs *MockConfigService) GetImpactMarginNotional(ammAddress common.Address) *big.Int { + return big.NewInt(500e6) +} + +func (cs *MockConfigService) GetReduceOnlyAmounts(trader common.Address) []*big.Int { + return []*big.Int{big.NewInt(0)} +} + +func (cs *MockConfigService) IsSettledAll() bool { + return false +} diff --git a/plugin/evm/orderbook/order_types.go b/plugin/evm/orderbook/order_types.go new file mode 100644 index 0000000000..6e05da6392 --- /dev/null +++ b/plugin/evm/orderbook/order_types.go @@ -0,0 +1,15 @@ +package orderbook + +import ( + hu "github.com/ava-labs/subnet-evm/plugin/evm/orderbook/hubbleutils" +) + +type ContractOrder interface { + EncodeToABIWithoutType() ([]byte, error) + EncodeToABI() ([]byte, error) + DecodeFromRawOrder(rawOrder interface{}) + Map() map[string]interface{} +} + +type LimitOrder = hu.LimitOrder +type IOCOrder = hu.IOCOrder diff --git a/plugin/evm/orderbook/order_types_test.go b/plugin/evm/orderbook/order_types_test.go new file mode 100644 index 0000000000..9d1d3cc094 --- /dev/null +++ b/plugin/evm/orderbook/order_types_test.go @@ -0,0 +1,230 @@ +package orderbook + +import ( + "encoding/hex" + "fmt" + "math/big" + "strings" + + "testing" + + hu "github.com/ava-labs/subnet-evm/plugin/evm/orderbook/hubbleutils" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/assert" +) + +func TestDecodeLimitOrder(t *testing.T) { + t.Run("long order", func(t *testing.T) { + testDecodeTypeAndEncodedOrder( + t, + strings.TrimPrefix("0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000000000000000000000000000004563918244f4000000000000000000000000000000000000000000000000000000000000000f42400000000000000000000000000000000000000000000000000000018a82b01e9d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "0x"), + strings.TrimPrefix("0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000000000000000000000000000004563918244f4000000000000000000000000000000000000000000000000000000000000000f42400000000000000000000000000000000000000000000000000000018a82b01e9d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "0x"), + Limit, + LimitOrder{ + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8"), + BaseAssetQuantity: big.NewInt(5000000000000000000), + Price: big.NewInt(1000000), + Salt: big.NewInt(1694409694877), + ReduceOnly: false, + }, + PostOnly: false, + }, + ) + }) + + t.Run("long order reduce only", func(t *testing.T) { + testDecodeTypeAndEncodedOrder( + t, + strings.TrimPrefix("0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000000000000000000000000000004563918244f4000000000000000000000000000000000000000000000000000000000000000f42400000000000000000000000000000000000000000000000000000018a82b4121c00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000", "0x"), + strings.TrimPrefix("0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000000000000000000000000000004563918244f4000000000000000000000000000000000000000000000000000000000000000f42400000000000000000000000000000000000000000000000000000018a82b4121c00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000", "0x"), + Limit, + LimitOrder{ + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8"), + BaseAssetQuantity: big.NewInt(5000000000000000000), + Price: big.NewInt(1000000), + Salt: big.NewInt(1694409953820), + ReduceOnly: true, + }, + PostOnly: false, + }, + ) + }) + + t.Run("short order", func(t *testing.T) { + order := LimitOrder{ + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8"), + BaseAssetQuantity: big.NewInt(-5000000000000000000), + Price: big.NewInt(1000000), + Salt: big.NewInt(1694410024592), + ReduceOnly: false, + }, + PostOnly: false, + } + orderHash, err := order.Hash() + assert.Nil(t, err) + assert.Equal(t, "0x0d87f0d9a37bc19fc3557db4085088cbecc5d6f3ff63c05f6db33684b8145108", orderHash.Hex()) + testDecodeTypeAndEncodedOrder( + t, + strings.TrimPrefix("0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8ffffffffffffffffffffffffffffffffffffffffffffffffba9c6e7dbb0c000000000000000000000000000000000000000000000000000000000000000f42400000000000000000000000000000000000000000000000000000018a82b5269000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "0x"), + strings.TrimPrefix("0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8ffffffffffffffffffffffffffffffffffffffffffffffffba9c6e7dbb0c000000000000000000000000000000000000000000000000000000000000000f42400000000000000000000000000000000000000000000000000000018a82b5269000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "0x"), + Limit, + order, + ) + }) + + t.Run("short order reduce only", func(t *testing.T) { + testDecodeTypeAndEncodedOrder( + t, + strings.TrimPrefix("0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8ffffffffffffffffffffffffffffffffffffffffffffffffba9c6e7dbb0c000000000000000000000000000000000000000000000000000000000000000f42400000000000000000000000000000000000000000000000000000018a82b7597700000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000", "0x"), + strings.TrimPrefix("0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8ffffffffffffffffffffffffffffffffffffffffffffffffba9c6e7dbb0c000000000000000000000000000000000000000000000000000000000000000f42400000000000000000000000000000000000000000000000000000018a82b7597700000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000", "0x"), + Limit, + LimitOrder{ + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8"), + BaseAssetQuantity: big.NewInt(-5000000000000000000), + Price: big.NewInt(1000000), + Salt: big.NewInt(1694410168695), + ReduceOnly: true, + }, + PostOnly: false, + }, + ) + }) + t.Run("short order reduce only with post order", func(t *testing.T) { + testDecodeTypeAndEncodedOrder( + t, + strings.TrimPrefix("0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8ffffffffffffffffffffffffffffffffffffffffffffffffba9c6e7dbb0c000000000000000000000000000000000000000000000000000000000000000f42400000000000000000000000000000000000000000000000000000018a82b8382e00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001", "0x"), + strings.TrimPrefix("0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8ffffffffffffffffffffffffffffffffffffffffffffffffba9c6e7dbb0c000000000000000000000000000000000000000000000000000000000000000f42400000000000000000000000000000000000000000000000000000018a82b8382e00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001", "0x"), + Limit, + LimitOrder{ + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8"), + BaseAssetQuantity: big.NewInt(-5000000000000000000), + Price: big.NewInt(1000000), + Salt: big.NewInt(1694410225710), + ReduceOnly: true, + }, + PostOnly: true, + }, + ) + }) +} + +func testDecodeTypeAndEncodedOrder(t *testing.T, typedEncodedOrder string, encodedOrder string, orderType OrderType, expectedOutput interface{}) { + testData, err := hex.DecodeString(typedEncodedOrder) + assert.Nil(t, err) + + decodeStep, err := hu.DecodeTypeAndEncodedOrder(testData) + assert.Nil(t, err) + + assert.Equal(t, orderType, decodeStep.OrderType) + assert.Equal(t, encodedOrder, hex.EncodeToString(decodeStep.EncodedOrder)) + testDecodeLimitOrder(t, encodedOrder, expectedOutput) +} + +func testDecodeLimitOrder(t *testing.T, encodedOrder string, expectedOutput interface{}) { + testData, err := hex.DecodeString(encodedOrder) + assert.Nil(t, err) + + result, err := hu.DecodeLimitOrder(testData) + fmt.Println(result) + assert.NoError(t, err) + assert.NotNil(t, result) + assertLimitOrderEquality(t, expectedOutput.(LimitOrder).BaseOrder, result.BaseOrder) + assert.Equal(t, expectedOutput.(LimitOrder).PostOnly, result.PostOnly) +} + +func TestDecodeIOCOrder(t *testing.T) { + t.Run("long order", func(t *testing.T) { + order := &IOCOrder{ + OrderType: 1, + ExpireAt: big.NewInt(1688994854), + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8"), + BaseAssetQuantity: big.NewInt(5000000000000000000), + Price: big.NewInt(1000000000), + Salt: big.NewInt(1688994806105), + ReduceOnly: false, + }, + } + h, err := order.Hash() + assert.Nil(t, err) + assert.Equal(t, "0xc989b9a5bf196036dbbae61f56179f31172cc04aa91238bc1b7c828bebf0fe5e", h.Hex()) + + typeEncodedOrder := strings.TrimPrefix("0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000064ac0426000000000000000000000000000000000000000000000000000000000000000000000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000000000000000000000000000004563918244f40000000000000000000000000000000000000000000000000000000000003b9aca00000000000000000000000000000000000000000000000000000001893fef79590000000000000000000000000000000000000000000000000000000000000000", "0x") + encodedOrder := strings.TrimPrefix("0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000064ac0426000000000000000000000000000000000000000000000000000000000000000000000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000000000000000000000000000004563918244f40000000000000000000000000000000000000000000000000000000000003b9aca00000000000000000000000000000000000000000000000000000001893fef79590000000000000000000000000000000000000000000000000000000000000000", "0x") + b, err := order.EncodeToABI() + assert.Nil(t, err) + assert.Equal(t, typeEncodedOrder, hex.EncodeToString(b)) + testDecodeTypeAndEncodedIOCOrder(t, typeEncodedOrder, encodedOrder, IOC, order) + }) + + t.Run("short order", func(t *testing.T) { + order := &IOCOrder{ + OrderType: 1, + ExpireAt: big.NewInt(1688994854), + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8"), + BaseAssetQuantity: big.NewInt(-5000000000000000000), + Price: big.NewInt(1000000000), + Salt: big.NewInt(1688994806105), + ReduceOnly: false, + }, + } + h, err := order.Hash() + assert.Nil(t, err) + assert.Equal(t, "0x4f92bf62284e2080d3d3cf7c15dcddf1c1a496902c1742de78737d3d9a870661", h.Hex()) + + typeEncodedOrder := strings.TrimPrefix("0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000064ac0426000000000000000000000000000000000000000000000000000000000000000000000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8ffffffffffffffffffffffffffffffffffffffffffffffffba9c6e7dbb0c0000000000000000000000000000000000000000000000000000000000003b9aca00000000000000000000000000000000000000000000000000000001893fef79590000000000000000000000000000000000000000000000000000000000000000", "0x") + encodedOrder := strings.TrimPrefix("0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000064ac0426000000000000000000000000000000000000000000000000000000000000000000000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8ffffffffffffffffffffffffffffffffffffffffffffffffba9c6e7dbb0c0000000000000000000000000000000000000000000000000000000000003b9aca00000000000000000000000000000000000000000000000000000001893fef79590000000000000000000000000000000000000000000000000000000000000000", "0x") + b, err := order.EncodeToABI() + assert.Nil(t, err) + assert.Equal(t, typeEncodedOrder, hex.EncodeToString(b)) + testDecodeTypeAndEncodedIOCOrder(t, typeEncodedOrder, encodedOrder, IOC, order) + }) +} + +func testDecodeTypeAndEncodedIOCOrder(t *testing.T, typedEncodedOrder string, encodedOrder string, orderType OrderType, expectedOutput *IOCOrder) { + testData, err := hex.DecodeString(typedEncodedOrder) + assert.Nil(t, err) + + decodeStep, err := hu.DecodeTypeAndEncodedOrder(testData) + assert.Nil(t, err) + + assert.Equal(t, orderType, decodeStep.OrderType) + assert.Equal(t, encodedOrder, hex.EncodeToString(decodeStep.EncodedOrder)) + testDecodeIOCOrder(t, decodeStep.EncodedOrder, expectedOutput) +} + +func testDecodeIOCOrder(t *testing.T, encodedOrder []byte, expectedOutput *IOCOrder) { + result, err := hu.DecodeIOCOrder(encodedOrder) + assert.NoError(t, err) + fmt.Println(result) + assert.NotNil(t, result) + assertIOCOrderEquality(t, expectedOutput, result) +} + +func assertIOCOrderEquality(t *testing.T, expected, actual *IOCOrder) { + assert.Equal(t, expected.OrderType, actual.OrderType) + assert.Equal(t, expected.ExpireAt.Int64(), actual.ExpireAt.Int64()) + assertLimitOrderEquality(t, expected.BaseOrder, actual.BaseOrder) +} + +func assertLimitOrderEquality(t *testing.T, expected, actual hu.BaseOrder) { + assert.Equal(t, expected.AmmIndex.Int64(), actual.AmmIndex.Int64()) + assert.Equal(t, expected.Trader, actual.Trader) + assert.Equal(t, expected.BaseAssetQuantity, actual.BaseAssetQuantity) + assert.Equal(t, expected.Price, actual.Price) + assert.Equal(t, expected.Salt, actual.Salt) + assert.Equal(t, expected.ReduceOnly, actual.ReduceOnly) +} diff --git a/plugin/evm/orderbook/service.go b/plugin/evm/orderbook/service.go new file mode 100644 index 0000000000..4da6c95a4b --- /dev/null +++ b/plugin/evm/orderbook/service.go @@ -0,0 +1,400 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package orderbook + +import ( + "context" + "fmt" + "math/big" + "runtime" + "runtime/debug" + "strconv" + "strings" + "time" + + "github.com/ava-labs/subnet-evm/core" + "github.com/ava-labs/subnet-evm/eth" + "github.com/ava-labs/subnet-evm/metrics" + hu "github.com/ava-labs/subnet-evm/plugin/evm/orderbook/hubbleutils" + "github.com/ava-labs/subnet-evm/rpc" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/log" +) + +type OrderBookAPI struct { + db LimitOrderDatabase + backend *eth.EthAPIBackend + configService IConfigService +} + +func NewOrderBookAPI(database LimitOrderDatabase, backend *eth.EthAPIBackend, configService IConfigService) *OrderBookAPI { + return &OrderBookAPI{ + db: database, + backend: backend, + configService: configService, + } +} + +type OrderBookResponse struct { + Orders []OrderMin +} + +type OpenOrdersResponse struct { + Orders []OrderForOpenOrders +} + +type OrderMin struct { + Market + Price string + Size string + Signer string + OrderId string +} + +type OrderForOpenOrders struct { + Market + Price string + Size string + FilledSize string + Timestamp uint64 + Salt string + OrderId string + ReduceOnly bool + PostOnly bool + OrderType string +} + +type GetDebugDataResponse struct { + MarginFraction map[common.Address]*big.Int + AvailableMargin map[common.Address]*big.Int + PendingFunding map[common.Address]*big.Int + Margin map[common.Address]*big.Int + UtilisedMargin map[common.Address]*big.Int + ReservedMargin map[common.Address]*big.Int + NotionalPosition map[common.Address]*big.Int + UnrealizePnL map[common.Address]*big.Int + LastPrice map[Market]*big.Int + OraclePrice map[Market]*big.Int + MidPrice map[Market]*big.Int +} + +func (api *OrderBookAPI) GetDebugData(ctx context.Context, trader string) GetDebugDataResponse { + traderHash := common.HexToAddress(trader) + response := GetDebugDataResponse{ + MarginFraction: map[common.Address]*big.Int{}, + AvailableMargin: map[common.Address]*big.Int{}, + PendingFunding: map[common.Address]*big.Int{}, + Margin: map[common.Address]*big.Int{}, + NotionalPosition: map[common.Address]*big.Int{}, + UnrealizePnL: map[common.Address]*big.Int{}, + UtilisedMargin: map[common.Address]*big.Int{}, + ReservedMargin: map[common.Address]*big.Int{}, + LastPrice: map[Market]*big.Int{}, + OraclePrice: map[Market]*big.Int{}, + } + + traderMap := api.db.GetAllTraders() + if trader != "" { + traderMap = map[common.Address]Trader{ + traderHash: traderMap[traderHash], + } + } + + prices := api.configService.GetUnderlyingPrices() + mPrices := api.configService.GetMidPrices() + + oraclePrices := map[Market]*big.Int{} + midPrices := map[Market]*big.Int{} + count := api.configService.GetActiveMarketsCount() + markets := make([]Market, count) + for i := int64(0); i < count; i++ { + markets[i] = Market(i) + oraclePrices[Market(i)] = prices[Market(i)] + midPrices[Market(i)] = mPrices[Market(i)] + } + assets := api.configService.GetCollaterals() + for addr, trader := range traderMap { + pendingFunding := getTotalFunding(&trader, markets) + margin := new(big.Int).Sub(getNormalisedMargin(&trader, assets), pendingFunding) + notionalPosition, unrealizePnL := getTotalNotionalPositionAndUnrealizedPnl(&trader, margin, hu.Min_Allowable_Margin, oraclePrices, midPrices, markets) + hState := &hu.HubbleState{ + Assets: assets, + OraclePrices: oraclePrices, + MidPrices: midPrices, + ActiveMarkets: markets, + MinAllowableMargin: api.configService.GetMinAllowableMargin(), + MaintenanceMargin: api.configService.GetMaintenanceMargin(), + } + marginFraction := calcMarginFraction(&trader, hState) + availableMargin := getAvailableMargin(&trader, hState) + utilisedMargin := hu.Div1e6(new(big.Int).Mul(notionalPosition, hState.MinAllowableMargin)) + + response.MarginFraction[addr] = marginFraction + response.AvailableMargin[addr] = availableMargin + response.PendingFunding[addr] = pendingFunding + response.Margin[addr] = getNormalisedMargin(&trader, assets) + response.UtilisedMargin[addr] = utilisedMargin + response.NotionalPosition[addr] = notionalPosition + response.UnrealizePnL[addr] = unrealizePnL + response.ReservedMargin[addr] = trader.Margin.Reserved + } + + response.LastPrice = api.db.GetLastPrices() + response.OraclePrice = oraclePrices + response.MidPrice = midPrices + return response +} + +func (api *OrderBookAPI) GetDetailedOrderBookData(ctx context.Context) InMemoryDatabase { + return api.db.GetOrderBookData() +} + +func (api *OrderBookAPI) GetOrderBook(ctx context.Context, marketStr string) (*OrderBookResponse, error) { + market, err := parseMarket(marketStr) + if err != nil { + return nil, err + } + var orders []Order + if market == nil { + orders = api.db.GetAllOrders() + } else { + orders = api.db.GetMarketOrders(Market(*market)) + } + + responseOrders := []OrderMin{} + for _, order := range orders { + responseOrders = append(responseOrders, order.ToOrderMin()) + } + return &OrderBookResponse{Orders: responseOrders}, nil +} + +func parseMarket(marketStr string) (*int, error) { + var market *int + if len(marketStr) > 0 { + _market, err := strconv.Atoi(marketStr) + if err != nil { + return nil, fmt.Errorf("invalid market") + } + market = &_market + } + return market, nil +} + +func (api *OrderBookAPI) GetOpenOrders(ctx context.Context, trader string, marketStr string) (*OpenOrdersResponse, error) { + market, err := parseMarket(marketStr) + if err != nil { + return nil, err + } + traderOrders := []OrderForOpenOrders{} + traderHash := common.HexToAddress(trader) + orders := api.db.GetOpenOrdersForTraderByType(traderHash, Limit) + for _, order := range orders { + if strings.EqualFold(order.Trader.String(), trader) && (market == nil || order.Market == Market(*market)) { + traderOrders = append(traderOrders, OrderForOpenOrders{ + Market: order.Market, + Price: order.Price.String(), + Size: order.BaseAssetQuantity.String(), + FilledSize: order.FilledBaseAssetQuantity.String(), + Salt: order.Salt.String(), + OrderId: order.Id.String(), + ReduceOnly: order.ReduceOnly, + PostOnly: order.isPostOnly(), + OrderType: order.OrderType.String(), + }) + } + } + return &OpenOrdersResponse{Orders: traderOrders}, nil +} + +// NewOrderBookState send a notification each time a new (header) block is appended to the chain. +func (api *OrderBookAPI) NewOrderBookState(ctx context.Context) (*rpc.Subscription, error) { + notifier, supported := rpc.NotifierFromContext(ctx) + if !supported { + return &rpc.Subscription{}, rpc.ErrNotificationsUnsupported + } + + rpcSub := notifier.CreateSubscription() + + go executeFuncAndRecoverPanic(func() { + var ( + headers = make(chan core.ChainHeadEvent) + headersSub event.Subscription + ) + + headersSub = api.backend.SubscribeChainHeadEvent(headers) + defer headersSub.Unsubscribe() + + for { + select { + case <-headers: + orderBookData := api.GetDetailedOrderBookData(ctx) + log.Info("New order book state", "orderBookData", orderBookData) + notifier.Notify(rpcSub.ID, &orderBookData) + case <-rpcSub.Err(): + headersSub.Unsubscribe() + return + case <-notifier.Closed(): + headersSub.Unsubscribe() + return + } + } + }, "panic in NewOrderBookState", RPCPanicsCounter) + + return rpcSub, nil +} + +func (api *OrderBookAPI) GetDepthForMarket(ctx context.Context, market int) *MarketDepth { + return getDepthForMarket(api.db, Market(market)) +} + +// used by UI +func (api *OrderBookAPI) StreamDepthUpdateForMarket(ctx context.Context, market int) (*rpc.Subscription, error) { + notifier, _ := rpc.NotifierFromContext(ctx) + rpcSub := notifier.CreateSubscription() + + ticker := time.NewTicker(1 * time.Second) + + var oldMarketDepth = &MarketDepth{} + + go executeFuncAndRecoverPanic(func() { + for { + select { + case <-ticker.C: + newMarketDepth := getDepthForMarket(api.db, Market(market)) + depthUpdate := getUpdateInDepth(newMarketDepth, oldMarketDepth) + notifier.Notify(rpcSub.ID, depthUpdate) + oldMarketDepth = newMarketDepth + case <-notifier.Closed(): + ticker.Stop() + return + } + } + }, "panic in StreamDepthUpdateForMarket", RPCPanicsCounter) + + return rpcSub, nil +} + +// used by UI +// @todo: this is a duplicate of StreamDepthUpdateForMarket with a param for update frequency. Need to remove the original function later and keep this one. +func (api *OrderBookAPI) StreamDepthUpdateForMarketAndFreq(ctx context.Context, market int, updateFreq string) (*rpc.Subscription, error) { + notifier, _ := rpc.NotifierFromContext(ctx) + rpcSub := notifier.CreateSubscription() + if updateFreq == "" { + updateFreq = "1s" + } + + duration, err := time.ParseDuration(updateFreq) + if err != nil { + return nil, fmt.Errorf("invalid update frequency %s", updateFreq) + } + ticker := time.NewTicker(duration) + + var oldMarketDepth = &MarketDepth{} + + go executeFuncAndRecoverPanic(func() { + for { + select { + case <-ticker.C: + newMarketDepth := getDepthForMarket(api.db, Market(market)) + depthUpdate := getUpdateInDepth(newMarketDepth, oldMarketDepth) + notifier.Notify(rpcSub.ID, depthUpdate) + oldMarketDepth = newMarketDepth + case <-notifier.Closed(): + ticker.Stop() + return + } + } + }, "panic in StreamDepthUpdateForMarketAndFreq", RPCPanicsCounter) + + return rpcSub, nil +} + +func getUpdateInDepth(newMarketDepth *MarketDepth, oldMarketDepth *MarketDepth) *MarketDepth { + var diff = &MarketDepth{ + Market: newMarketDepth.Market, + Longs: map[string]string{}, + Shorts: map[string]string{}, + } + for price, depth := range newMarketDepth.Longs { + oldDepth := oldMarketDepth.Longs[price] + if oldDepth != depth { + diff.Longs[price] = depth + } + } + for price := range oldMarketDepth.Longs { + if newMarketDepth.Longs[price] == "" { + diff.Longs[price] = big.NewInt(0).String() + } + } + for price, depth := range newMarketDepth.Shorts { + oldDepth := oldMarketDepth.Shorts[price] + if oldDepth != depth { + diff.Shorts[price] = depth + } + } + for price := range oldMarketDepth.Shorts { + if newMarketDepth.Shorts[price] == "" { + diff.Shorts[price] = big.NewInt(0).String() + } + } + return diff +} + +func getDepthForMarket(db LimitOrderDatabase, market Market) *MarketDepth { + // currentBlock number only needs to be passed in for the retry logic for failed orders. + // There are some orders in the book that could have been marked failed, + // but because of our retry logic they might be retried every 100 blocks + // So, one could argue that is this not a super accurate representation of the order book + // BUT for the argument sake, we could also say that these retry orders can be treated as "fresh" orders + longOrders := db.GetLongOrders(market, nil /* lowerbound */, nil /* currentBlock */) + shortOrders := db.GetShortOrders(market, nil /* upperbound */, nil /* currentBlock */) + return &MarketDepth{ + Market: market, + Longs: aggregateOrdersByPrice(longOrders), + Shorts: aggregateOrdersByPrice(shortOrders), + } +} + +func aggregateOrdersByPrice(orders []Order) map[string]string { + aggregatedOrders := map[string]string{} + for _, order := range orders { + aggregatedBaseAssetQuantity, ok := aggregatedOrders[order.Price.String()] + if ok { + quantity, _ := big.NewInt(0).SetString(aggregatedBaseAssetQuantity, 10) + aggregatedOrders[order.Price.String()] = quantity.Add(quantity, order.GetUnFilledBaseAssetQuantity()).String() + } else { + aggregatedOrders[order.Price.String()] = order.GetUnFilledBaseAssetQuantity().String() + } + } + return aggregatedOrders +} + +type MarketDepth struct { + Market Market `json:"market"` + Longs map[string]string `json:"longs"` + Shorts map[string]string `json:"shorts"` +} + +func executeFuncAndRecoverPanic(fn func(), panicMessage string, panicCounter metrics.Counter) { + defer func() { + if panicInfo := recover(); panicInfo != nil { + var errorMessage string + switch panicInfo := panicInfo.(type) { + case string: + errorMessage = fmt.Sprintf("recovered (string) panic: %s", panicInfo) + case runtime.Error: + errorMessage = fmt.Sprintf("recovered (runtime.Error) panic: %s", panicInfo.Error()) + case error: + errorMessage = fmt.Sprintf("recovered (error) panic: %s", panicInfo.Error()) + default: + errorMessage = fmt.Sprintf("recovered (default) panic: %v", panicInfo) + } + + log.Error(panicMessage, "errorMessage", errorMessage, "stack_trace", string(debug.Stack())) + panicCounter.Inc(1) + } + }() + fn() +} diff --git a/plugin/evm/orderbook/service_test.go b/plugin/evm/orderbook/service_test.go new file mode 100644 index 0000000000..871ee2a0b5 --- /dev/null +++ b/plugin/evm/orderbook/service_test.go @@ -0,0 +1,91 @@ +package orderbook + +import ( + "context" + "fmt" + "math/big" + "testing" + + "github.com/ava-labs/subnet-evm/eth" + "github.com/stretchr/testify/assert" +) + +func TestAggregatedOrderBook(t *testing.T) { + t.Run("it aggregates long and short orders by price and returns aggregated data in json format with blockNumber", func(t *testing.T) { + db := getDatabase() + service := NewOrderBookAPI(db, ð.EthAPIBackend{}, db.configService) + + longOrder1 := getLongOrder() + db.Add(&longOrder1) + + longOrder2 := getLongOrder() + longOrder2.Salt.Add(longOrder2.Salt, big.NewInt(100)) + longOrder2.Price.Mul(longOrder2.Price, big.NewInt(2)) + longOrder2.Id = getIdFromOrder(longOrder2) + db.Add(&longOrder2) + + shortOrder1 := getShortOrder() + shortOrder1.Salt.Add(shortOrder1.Salt, big.NewInt(200)) + shortOrder1.Id = getIdFromOrder(shortOrder1) + db.Add(&shortOrder1) + + shortOrder2 := getShortOrder() + shortOrder2.Salt.Add(shortOrder1.Salt, big.NewInt(300)) + shortOrder2.Price.Mul(shortOrder2.Price, big.NewInt(2)) + shortOrder2.Id = getIdFromOrder(shortOrder2) + db.Add(&shortOrder2) + + ctx := context.TODO() + response := service.GetDepthForMarket(ctx, int(Market(0))) + expectedAggregatedOrderBookState := MarketDepth{ + Market: Market(0), + Longs: map[string]string{ + longOrder1.Price.String(): longOrder1.BaseAssetQuantity.String(), + longOrder2.Price.String(): longOrder2.BaseAssetQuantity.String(), + }, + Shorts: map[string]string{ + shortOrder1.Price.String(): shortOrder1.BaseAssetQuantity.String(), + shortOrder2.Price.String(): shortOrder2.BaseAssetQuantity.String(), + }, + } + fmt.Println(response) + assert.Equal(t, expectedAggregatedOrderBookState, *response) + + orderbook, _ := service.GetOrderBook(ctx, "0") + assert.Equal(t, 4, len(orderbook.Orders)) + }) + t.Run("when event is the first event after subscribe", func(t *testing.T) { + t.Run("when orderbook has no orders", func(t *testing.T) { + }) + t.Run("when orderbook has either long or short orders with same price", func(t *testing.T) { + t.Run("when order is longOrder", func(t *testing.T) { + }) + t.Run("when order is shortOrder", func(t *testing.T) { + }) + t.Run("when order is one long and one short order", func(t *testing.T) { + }) + t.Run("when orderbook has more than one order of long or short", func(t *testing.T) { + t.Run("when orderbook has more than one long orders", func(t *testing.T) { + }) + t.Run("when orderbook has more than one short orders", func(t *testing.T) { + }) + t.Run("when orderbook has more than one long and short orders", func(t *testing.T) { + }) + }) + }) + t.Run("when orderbook has orders of same type with different price", func(t *testing.T) { + t.Run("when orderbook has long orders of different price", func(t *testing.T) { + }) + t.Run("when orderbook has short orders of different price", func(t *testing.T) { + }) + t.Run("when orderbook has long and short orders of different price", func(t *testing.T) { + }) + }) + }) + t.Run("for all events received after first event after subscribe", func(t *testing.T) { + t.Run("when there are no changes in orderbook like cancel/matching", func(t *testing.T) { + }) + t.Run("if there are changes in orderbook like cancel/matching", func(t *testing.T) { + }) + }) +} diff --git a/plugin/evm/orderbook/state.go b/plugin/evm/orderbook/state.go new file mode 100644 index 0000000000..1f0c86120b --- /dev/null +++ b/plugin/evm/orderbook/state.go @@ -0,0 +1,26 @@ +package orderbook + +import ( + hu "github.com/ava-labs/subnet-evm/plugin/evm/orderbook/hubbleutils" +) + +func GetHubbleState(configService IConfigService) *hu.HubbleState { + count := configService.GetActiveMarketsCount() + markets := make([]Market, count) + for i := int64(0); i < count; i++ { + markets[i] = Market(i) + } + hState := &hu.HubbleState{ + Assets: configService.GetCollaterals(), + OraclePrices: hu.ArrayToMap(configService.GetUnderlyingPrices()), + MidPrices: hu.ArrayToMap(configService.GetMidPrices()), + SettlementPrices: hu.ArrayToMap(configService.GetSettlementPrices()), + ActiveMarkets: markets, + MinAllowableMargin: configService.GetMinAllowableMargin(), + MaintenanceMargin: configService.GetMaintenanceMargin(), + TakerFee: configService.GetTakerFee(), + UpgradeVersion: hu.V2, + } + + return hState +} diff --git a/plugin/evm/orderbook/temp_matching.go b/plugin/evm/orderbook/temp_matching.go new file mode 100644 index 0000000000..10bbcca33c --- /dev/null +++ b/plugin/evm/orderbook/temp_matching.go @@ -0,0 +1,281 @@ +package orderbook + +import ( + "encoding/json" + "math/big" + + "github.com/ava-labs/subnet-evm/accounts/abi" + "github.com/ava-labs/subnet-evm/core/state" + "github.com/ava-labs/subnet-evm/core/types" + "github.com/ava-labs/subnet-evm/metrics" + "github.com/ava-labs/subnet-evm/plugin/evm/orderbook/abis" + "github.com/ava-labs/subnet-evm/precompile/contracts/bibliophile" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" +) + +var ( + getMatchingTxsErrorCounter = metrics.NewRegisteredCounter("GetMatchingTxs_errors", nil) + getMatchingTxsWarningCounter = metrics.NewRegisteredCounter("GetMatchingTxs_warnings", nil) +) + +type TempMatcher struct { + db LimitOrderDatabase + tempDB LimitOrderDatabase + lotp LimitOrderTxProcessor + orderBookABI abi.ABI + limitOrderBookABI abi.ABI + iocOrderBookABI abi.ABI +} + +func NewTempMatcher(db LimitOrderDatabase, lotp LimitOrderTxProcessor) *TempMatcher { + orderBookABI, err := abi.FromSolidityJson(string(abis.OrderBookAbi)) + if err != nil { + panic(err) + } + + limitOrderBookABI, err := abi.FromSolidityJson(string(abis.LimitOrderBookAbi)) + if err != nil { + panic(err) + } + + iocOrderBookABI, err := abi.FromSolidityJson(string(abis.IOCOrderBookAbi)) + if err != nil { + panic(err) + } + + return &TempMatcher{ + db: db, + lotp: lotp, + orderBookABI: orderBookABI, + limitOrderBookABI: limitOrderBookABI, + iocOrderBookABI: iocOrderBookABI, + } +} + +func (matcher *TempMatcher) GetMatchingTxs(tx *types.Transaction, stateDB *state.StateDB, blockNumber *big.Int) map[common.Address]types.Transactions { + var isError bool + defer func() { + if isError { + getMatchingTxsErrorCounter.Inc(1) + } + }() + + to := tx.To() + + if to == nil || len(tx.Data()) < 4 { + return nil + } + + method := tx.Data()[:4] + methodData := tx.Data()[4:] + + var err error + var markets []Market + if matcher.tempDB == nil { + matcher.tempDB, err = matcher.db.GetOrderBookDataCopy() + if err != nil { + log.Error("GetMatchingTxs: error in fetching tempDB", "err", err) + isError = true + return nil + } + } + switch *to { + case LimitOrderBookContractAddress: + abiMethod, err := matcher.limitOrderBookABI.MethodById(method) + if err != nil { + log.Error("GetMatchingTxs: error in fetching abiMethod", "err", err) + isError = true + return nil + } + + // check for placeOrders and cancelOrders txs + switch abiMethod.Name { + case "placeOrders": + orders, err := getLimitOrdersFromMethodData(abiMethod, methodData, blockNumber) + if err != nil { + log.Error("GetMatchingTxs: error in fetching orders from placeOrders tx data", "err", err) + isError = true + return nil + } + marketsMap := make(map[Market]struct{}) + for _, order := range orders { + // the transaction in the args is supposed to be already committed in the db, so the status should be placed + status := bibliophile.GetOrderStatus(stateDB, order.Id) + if status != 1 { // placed + log.Warn("GetMatchingTxs: invalid limit order status", "status", status, "order", order.Id.String()) + getMatchingTxsWarningCounter.Inc(1) + continue + } + + matcher.tempDB.Add(order) + marketsMap[order.Market] = struct{}{} + } + + markets = make([]Market, 0, len(marketsMap)) + for market := range marketsMap { + markets = append(markets, market) + } + + case "cancelOrders": + orders, err := getLimitOrdersFromMethodData(abiMethod, methodData, blockNumber) + if err != nil { + log.Error("GetMatchingTxs: error in fetching orders from cancelOrders tx data", "err", err) + isError = true + return nil + } + for _, order := range orders { + if err := matcher.tempDB.SetOrderStatus(order.Id, Cancelled, "", blockNumber.Uint64()); err != nil { + log.Error("GetMatchingTxs: error in SetOrderStatus", "orderId", order.Id.String(), "err", err) + return nil + } + } + // no need to run matching + return nil + default: + return nil + } + + case IOCOrderBookContractAddress: + abiMethod, err := matcher.iocOrderBookABI.MethodById(method) + if err != nil { + log.Error("Error in fetching abiMethod", "err", err) + isError = true + return nil + } + + switch abiMethod.Name { + case "placeOrders": + orders, err := getIOCOrdersFromMethodData(abiMethod, methodData, blockNumber) + if err != nil { + log.Error("Error in fetching orders", "err", err) + isError = true + return nil + } + marketsMap := make(map[Market]struct{}) + for _, order := range orders { + // the transaction in the args is supposed to be already committed in the db, so the status should be placed + status := bibliophile.IOCGetOrderStatus(stateDB, order.Id) + if status != 1 { // placed + log.Warn("GetMatchingTxs: invalid ioc order status", "status", status, "order", order.Id.String()) + getMatchingTxsWarningCounter.Inc(1) + continue + } + + matcher.tempDB.Add(order) + marketsMap[order.Market] = struct{}{} + } + + markets = make([]Market, 0, len(marketsMap)) + for market := range marketsMap { + markets = append(markets, market) + } + default: + return nil + } + default: + // tx is not related to orderbook + return nil + } + + configService := NewConfigServiceFromStateDB(stateDB) + tempMatchingPipeline := NewTemporaryMatchingPipeline(matcher.tempDB, matcher.lotp, configService) + + return tempMatchingPipeline.GetOrderMatchingTransactions(blockNumber, markets) +} + +func (matcher *TempMatcher) ResetMemoryDB() { + matcher.tempDB = nil +} + +func getLimitOrdersFromMethodData(abiMethod *abi.Method, methodData []byte, blockNumber *big.Int) ([]*Order, error) { + unpackedData, err := abiMethod.Inputs.Unpack(methodData) + if err != nil { + log.Error("Error in unpacking data", "err", err) + return nil, err + } + + limitOrders := []*LimitOrder{} + ordersInterface := unpackedData[0] + + marshalledOrders, _ := json.Marshal(ordersInterface) + err = json.Unmarshal(marshalledOrders, &limitOrders) + if err != nil { + log.Error("Error in unmarshalling orders", "err", err) + return nil, err + } + + orders := []*Order{} + for _, limitOrder := range limitOrders { + orderId, err := limitOrder.Hash() + if err != nil { + log.Error("Error in hashing order", "err", err) + // @todo: send to metrics + return nil, err + } + + order := &Order{ + Id: orderId, + Market: Market(limitOrder.AmmIndex.Int64()), + PositionType: getPositionTypeBasedOnBaseAssetQuantity(limitOrder.BaseAssetQuantity), + Trader: limitOrder.Trader, + BaseAssetQuantity: limitOrder.BaseAssetQuantity, + FilledBaseAssetQuantity: big.NewInt(0), + Price: limitOrder.Price, + RawOrder: limitOrder, + Salt: limitOrder.Salt, + ReduceOnly: limitOrder.ReduceOnly, + BlockNumber: blockNumber, + OrderType: Limit, + } + orders = append(orders, order) + } + + return orders, nil +} + +func getIOCOrdersFromMethodData(abiMethod *abi.Method, methodData []byte, blockNumber *big.Int) ([]*Order, error) { + unpackedData, err := abiMethod.Inputs.Unpack(methodData) + if err != nil { + log.Error("Error in unpacking data", "err", err) + return nil, err + } + + iocOrders := []*IOCOrder{} + ordersInterface := unpackedData[0] + + marshalledOrders, _ := json.Marshal(ordersInterface) + err = json.Unmarshal(marshalledOrders, &iocOrders) + if err != nil { + log.Error("Error in unmarshalling orders", "err", err) + return nil, err + } + + orders := []*Order{} + for _, iocOrder := range iocOrders { + orderId, err := iocOrder.Hash() + if err != nil { + log.Error("Error in hashing order", "err", err) + // @todo: send to metrics + return nil, err + } + + order := &Order{ + Id: orderId, + Market: Market(iocOrder.AmmIndex.Int64()), + PositionType: getPositionTypeBasedOnBaseAssetQuantity(iocOrder.BaseAssetQuantity), + Trader: iocOrder.Trader, + BaseAssetQuantity: iocOrder.BaseAssetQuantity, + FilledBaseAssetQuantity: big.NewInt(0), + Price: iocOrder.Price, + RawOrder: iocOrder, + Salt: iocOrder.Salt, + ReduceOnly: iocOrder.ReduceOnly, + BlockNumber: blockNumber, + OrderType: Limit, + } + orders = append(orders, order) + } + + return orders, nil +} diff --git a/plugin/evm/orderbook/testing_apis.go b/plugin/evm/orderbook/testing_apis.go new file mode 100644 index 0000000000..7963ff7cd3 --- /dev/null +++ b/plugin/evm/orderbook/testing_apis.go @@ -0,0 +1,80 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package orderbook + +import ( + "bytes" + "context" + "encoding/gob" + "fmt" + "math/big" + + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/subnet-evm/eth" + "github.com/ava-labs/subnet-evm/precompile/contracts/bibliophile" + "github.com/ava-labs/subnet-evm/rpc" + "github.com/ethereum/go-ethereum/common" +) + +type TestingAPI struct { + db LimitOrderDatabase + backend *eth.EthAPIBackend + configService IConfigService + hubbleDB database.Database +} + +func NewTestingAPI(database LimitOrderDatabase, backend *eth.EthAPIBackend, configService IConfigService, hubbleDB database.Database) *TestingAPI { + return &TestingAPI{ + db: database, + backend: backend, + configService: configService, + hubbleDB: hubbleDB, + } +} + +func (api *TestingAPI) GetClearingHouseVars(ctx context.Context, trader common.Address) bibliophile.VariablesReadFromClearingHouseSlots { + stateDB, _, _ := api.backend.StateAndHeaderByNumber(ctx, rpc.BlockNumber(getCurrentBlockNumber(api.backend))) + return bibliophile.GetClearingHouseVariables(stateDB, trader) +} + +func (api *TestingAPI) GetMarginAccountVars(ctx context.Context, collateralIdx *big.Int, traderAddress string) bibliophile.VariablesReadFromMarginAccountSlots { + stateDB, _, _ := api.backend.StateAndHeaderByNumber(ctx, rpc.BlockNumber(getCurrentBlockNumber(api.backend))) + return bibliophile.GetMarginAccountVariables(stateDB, collateralIdx, common.HexToAddress(traderAddress)) +} + +func (api *TestingAPI) GetAMMVars(ctx context.Context, ammAddress string, ammIndex int, traderAddress string) bibliophile.VariablesReadFromAMMSlots { + stateDB, _, _ := api.backend.StateAndHeaderByNumber(ctx, rpc.BlockNumber(getCurrentBlockNumber(api.backend))) + return bibliophile.GetAMMVariables(stateDB, common.HexToAddress(ammAddress), int64(ammIndex), common.HexToAddress(traderAddress)) +} + +func (api *TestingAPI) GetIOCOrdersVars(ctx context.Context, orderHash common.Hash) bibliophile.VariablesReadFromIOCOrdersSlots { + stateDB, _, _ := api.backend.StateAndHeaderByNumber(ctx, rpc.BlockNumber(getCurrentBlockNumber(api.backend))) + return bibliophile.GetIOCOrdersVariables(stateDB, orderHash) +} + +func (api *TestingAPI) GetOrderBookVars(ctx context.Context, traderAddress string, senderAddress string, orderHash common.Hash) bibliophile.VariablesReadFromOrderbookSlots { + stateDB, _, _ := api.backend.StateAndHeaderByNumber(ctx, rpc.BlockNumber(getCurrentBlockNumber(api.backend))) + return bibliophile.GetOrderBookVariables(stateDB, traderAddress, senderAddress, orderHash) +} + +func (api *TestingAPI) GetSnapshot(ctx context.Context) (Snapshot, error) { + var snapshot Snapshot + memoryDBSnapshotKey := "memoryDBSnapshot" + memorySnapshotBytes, err := api.hubbleDB.Get([]byte(memoryDBSnapshotKey)) + if err != nil { + return snapshot, fmt.Errorf("Error in fetching snapshot from hubbleDB; err=%v", err) + } + + buf := bytes.NewBuffer(memorySnapshotBytes) + err = gob.NewDecoder(buf).Decode(&snapshot) + if err != nil { + return snapshot, fmt.Errorf("Error in snapshot parsing; err=%v", err) + } + + return snapshot, nil +} + +func getCurrentBlockNumber(backend *eth.EthAPIBackend) uint64 { + return backend.LastAcceptedBlock().NumberU64() +} diff --git a/plugin/evm/orderbook/trading_apis.go b/plugin/evm/orderbook/trading_apis.go new file mode 100644 index 0000000000..62863284c6 --- /dev/null +++ b/plugin/evm/orderbook/trading_apis.go @@ -0,0 +1,493 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package orderbook + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "math/big" + "strings" + "sync" + "time" + + "github.com/ava-labs/subnet-evm/eth" + hu "github.com/ava-labs/subnet-evm/plugin/evm/orderbook/hubbleutils" + "github.com/ava-labs/subnet-evm/rpc" + "github.com/ava-labs/subnet-evm/utils" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/log" +) + +var traderFeed event.Feed +var marketFeed event.Feed +var MakerbookDatabaseFile string + +type TradingAPI struct { + db LimitOrderDatabase + backend *eth.EthAPIBackend + configService IConfigService + makerbookFileWriteChan chan Order + shutdownChan <-chan struct{} + shutdownWg *sync.WaitGroup +} + +func NewTradingAPI(database LimitOrderDatabase, backend *eth.EthAPIBackend, configService IConfigService, shutdownChan <-chan struct{}, shutdownWg *sync.WaitGroup) *TradingAPI { + tradingAPI := &TradingAPI{ + db: database, + backend: backend, + configService: configService, + makerbookFileWriteChan: make(chan Order, 100), + shutdownChan: shutdownChan, + shutdownWg: shutdownWg, + } + + shutdownWg.Add(1) + go func() { + defer shutdownWg.Done() + for { + select { + case order := <-tradingAPI.makerbookFileWriteChan: + writeOrderToFile(order) + case <-tradingAPI.shutdownChan: + return + } + } + }() + + return tradingAPI +} + +type TradingOrderBookDepthResponse struct { + LastUpdateID int `json:"lastUpdateId"` + E int64 `json:"E"` + T int64 `json:"T"` + Symbol int64 `json:"symbol"` + Bids [][]string `json:"bids"` + Asks [][]string `json:"asks"` +} + +type TradingOrderBookDepthUpdateResponse struct { + T int64 `json:"T"` + Symbol int64 `json:"s"` + Bids [][]string `json:"b"` + Asks [][]string `json:"a"` +} + +// found at https://binance-docs.github.io/apidocs/futures/en/#query-order-user_data +// commented field values are from the binance docs +type OrderStatusResponse struct { + ExecutedQty string `json:"executedQty"` // "0" + OrderID string `json:"orderId"` // 1917641 + OrigQty string `json:"origQty"` // "0.40" + Price string `json:"price"` // "0" + ReduceOnly bool `json:"reduceOnly"` // false + PostOnly bool `json:"postOnly"` // false + PositionSide string `json:"positionSide"` // "SHORT" + Status string `json:"status"` // "NEW" + Symbol int64 `json:"symbol"` // "BTCUSDT" + Time int64 `json:"time"` // 1579276756075 + Type string `json:"type"` // "LIMIT" + UpdateTime int64 `json:"updateTime"` // 1579276756075 + Salt *big.Int `json:"salt"` +} + +type TraderPosition struct { + Market Market `json:"market"` + OpenNotional string `json:"openNotional"` + Size string `json:"size"` + UnrealisedFunding string `json:"unrealisedFunding"` + LiquidationThreshold string `json:"liquidationThreshold"` + NotionalPosition string `json:"notionalPosition"` + UnrealisedProfit string `json:"unrealisedProfit"` + MarginFraction string `json:"marginFraction"` + LiquidationPrice string `json:"liquidationPrice"` + MarkPrice string `json:"markPrice"` +} + +type GetPositionsResponse struct { + Margin string `json:"margin"` + ReservedMargin string `json:"reservedMargin"` + Positions []TraderPosition `json:"positions"` +} + +var mapStatus = map[Status]string{ + Placed: "NEW", + FulFilled: "FILLED", + Cancelled: "CANCELED", + Execution_Failed: "REJECTED", +} + +func (api *TradingAPI) GetTradingOrderBookDepth(ctx context.Context, market int8) TradingOrderBookDepthResponse { + depth := getDepthForMarket(api.db, Market(market)) + + response := transformMarketDepth(depth) + response.T = time.Now().Unix() + + return response +} + +func (api *TradingAPI) GetOrderStatus(ctx context.Context, orderId common.Hash) (OrderStatusResponse, error) { + response := OrderStatusResponse{} + + limitOrder := api.db.GetOrderById(orderId) + if limitOrder == nil { + return response, fmt.Errorf("order not found") + } + + status := mapStatus[limitOrder.getOrderStatus().Status] + if limitOrder.FilledBaseAssetQuantity.Sign() != 0 { + status = "PARTIALLY_FILLED" + } + + limitOrder.BaseAssetQuantity.Abs(limitOrder.BaseAssetQuantity) + limitOrder.FilledBaseAssetQuantity.Abs(limitOrder.FilledBaseAssetQuantity) + + var positionSide string + switch limitOrder.PositionType { + case LONG: + positionSide = "LONG" + case SHORT: + positionSide = "SHORT" + } + + var time, updateTime int64 + placedBlock, err := api.backend.BlockByNumber(ctx, rpc.BlockNumber(limitOrder.BlockNumber.Int64())) + if err == nil { + time = int64(placedBlock.Time()) + } + + updateBlock, err := api.backend.BlockByNumber(ctx, rpc.BlockNumber(limitOrder.getOrderStatus().BlockNumber)) + if err == nil { + updateTime = int64(updateBlock.Time()) + } + + response = OrderStatusResponse{ + ExecutedQty: utils.BigIntToDecimal(limitOrder.FilledBaseAssetQuantity, 18, 8), + OrderID: limitOrder.Id.String(), + OrigQty: utils.BigIntToDecimal(limitOrder.BaseAssetQuantity, 18, 8), + Price: utils.BigIntToDecimal(limitOrder.Price, 6, 8), + ReduceOnly: limitOrder.ReduceOnly, + PostOnly: limitOrder.isPostOnly(), + PositionSide: positionSide, + Status: status, + Symbol: int64(limitOrder.Market), + Time: time, + Type: "LIMIT_ORDER", + UpdateTime: updateTime, + Salt: limitOrder.Salt, + } + + return response, nil +} + +func (api *TradingAPI) GetMarginAndPositions(ctx context.Context, trader string) (GetPositionsResponse, error) { + response := GetPositionsResponse{Positions: []TraderPosition{}} + + traderAddr := common.HexToAddress(trader) + + traderInfo := api.db.GetTraderInfo(traderAddr) + if traderInfo == nil { + return response, fmt.Errorf("trader not found") + } + + count := int64(len(api.configService.GetMarketsIncludingSettled())) + markets := make([]Market, count) + for i := int64(0); i < count; i++ { + markets[i] = Market(i) + } + + assets := api.configService.GetCollaterals() + pendingFunding := getTotalFunding(traderInfo, markets) + margin := new(big.Int).Sub(getNormalisedMargin(traderInfo, assets), pendingFunding) + response.Margin = utils.BigIntToDecimal(margin, 6, 8) + response.ReservedMargin = utils.BigIntToDecimal(traderInfo.Margin.Reserved, 6, 8) + + for market, position := range traderInfo.Positions { + midPrice := api.configService.GetMidPrices()[market] + notionalPosition, uPnL, mf := getPositionMetadata(midPrice, position.OpenNotional, position.Size, margin) + + response.Positions = append(response.Positions, TraderPosition{ + Market: market, + OpenNotional: utils.BigIntToDecimal(position.OpenNotional, 6, 8), + Size: utils.BigIntToDecimal(position.Size, 18, 8), + UnrealisedFunding: utils.BigIntToDecimal(position.UnrealisedFunding, 6, 8), + LiquidationThreshold: utils.BigIntToDecimal(position.LiquidationThreshold, 6, 8), + UnrealisedProfit: utils.BigIntToDecimal(uPnL, 6, 8), + MarginFraction: utils.BigIntToDecimal(mf, 6, 8), + NotionalPosition: utils.BigIntToDecimal(notionalPosition, 6, 8), + LiquidationPrice: "0", // todo: calculate + MarkPrice: utils.BigIntToDecimal(midPrice, 6, 8), + }) + } + + return response, nil +} + +// used by the sdk +func (api *TradingAPI) StreamDepthUpdateForMarket(ctx context.Context, market int) (*rpc.Subscription, error) { + notifier, _ := rpc.NotifierFromContext(ctx) + rpcSub := notifier.CreateSubscription() + + ticker := time.NewTicker(1 * time.Second) + + var oldMarketDepth = &MarketDepth{} + + go executeFuncAndRecoverPanic(func() { + for { + select { + case <-ticker.C: + newMarketDepth := getDepthForMarket(api.db, Market(market)) + depthUpdate := getUpdateInDepth(newMarketDepth, oldMarketDepth) + transformedDepthUpdate := transformMarketDepth(depthUpdate) + response := TradingOrderBookDepthUpdateResponse{ + T: time.Now().Unix(), + Symbol: int64(market), + Bids: transformedDepthUpdate.Bids, + Asks: transformedDepthUpdate.Asks, + } + notifier.Notify(rpcSub.ID, response) + oldMarketDepth = newMarketDepth + case <-notifier.Closed(): + ticker.Stop() + return + } + } + }, "panic in StreamDepthUpdateForMarket", RPCPanicsCounter) + + return rpcSub, nil +} + +func transformMarketDepth(depth *MarketDepth) TradingOrderBookDepthResponse { + response := TradingOrderBookDepthResponse{} + for price, quantity := range depth.Longs { + bigPrice, _ := big.NewInt(0).SetString(price, 10) + bigQuantity, _ := big.NewInt(0).SetString(quantity, 10) + response.Bids = append(response.Bids, []string{ + utils.BigIntToDecimal(bigPrice, 6, 8), + utils.BigIntToDecimal(bigQuantity, 18, 8), + }) + } + + for price, quantity := range depth.Shorts { + bigPrice, _ := big.NewInt(0).SetString(price, 10) + bigQuantity, _ := big.NewInt(0).SetString(quantity, 10) + response.Asks = append(response.Asks, []string{ + utils.BigIntToDecimal(bigPrice, 6, 8), + utils.BigIntToDecimal(bigQuantity, 18, 8), + }) + } + + return response +} + +func (api *TradingAPI) StreamTraderUpdates(ctx context.Context, trader string, blockStatus string) (*rpc.Subscription, error) { + notifier, _ := rpc.NotifierFromContext(ctx) + rpcSub := notifier.CreateSubscription() + confirmationLevel := BlockConfirmationLevel(blockStatus) + + traderFeedCh := make(chan TraderEvent) + traderFeedSubscription := traderFeed.Subscribe(traderFeedCh) + go executeFuncAndRecoverPanic(func() { + defer traderFeedSubscription.Unsubscribe() + + for { + select { + case event := <-traderFeedCh: + if strings.EqualFold(event.Trader.String(), trader) && event.BlockStatus == confirmationLevel { + notifier.Notify(rpcSub.ID, event) + } + case <-notifier.Closed(): + return + } + } + }, "panic in StreamTraderUpdates", RPCPanicsCounter) + + return rpcSub, nil +} + +func (api *TradingAPI) StreamMarketTrades(ctx context.Context, market Market, blockStatus string) (*rpc.Subscription, error) { + notifier, _ := rpc.NotifierFromContext(ctx) + rpcSub := notifier.CreateSubscription() + confirmationLevel := BlockConfirmationLevel(blockStatus) + + marketFeedCh := make(chan MarketFeedEvent) + acceptedLogsSubscription := marketFeed.Subscribe(marketFeedCh) + go executeFuncAndRecoverPanic(func() { + defer acceptedLogsSubscription.Unsubscribe() + + for { + select { + case event := <-marketFeedCh: + if event.Market == market && event.BlockStatus == confirmationLevel { + notifier.Notify(rpcSub.ID, event) + } + case <-notifier.Closed(): + return + } + } + }, "panic in StreamMarketTrades", RPCPanicsCounter) + + return rpcSub, nil +} + +// @todo cache api.configService values to avoid db lookups on every order placement +func (api *TradingAPI) PlaceOrder(order *hu.SignedOrder) (common.Hash, bool, error) { + if api.configService.IsSettledAll() { + return common.Hash{}, false, errors.New("all markets are settled now") + } + + orderId, err := order.Hash() + if err != nil { + return common.Hash{}, false, fmt.Errorf("failed to hash order: %s", err) + } + fields := api.db.GetOrderValidationFields(orderId, order) + // P1. Order is not already in memdb + if fields.Exists { + return orderId, false, hu.ErrOrderAlreadyExists + } + marketId := int(order.AmmIndex.Int64()) + trader, signer, err := hu.ValidateSignedOrder( + order, + hu.SignedOrderValidationFields{ + OrderHash: orderId, + Now: uint64(time.Now().Unix()), + ActiveMarketsCount: api.configService.GetActiveMarketsCount(), + MinSize: api.configService.getMinSizeRequirement(marketId), + PriceMultiplier: api.configService.GetPriceMultiplier(marketId), + Status: api.configService.GetSignedOrderStatus(orderId), + }, + ) + if err != nil { + return orderId, false, err + } + if trader != signer && !api.configService.IsTradingAuthority(trader, signer) { + log.Error("not trading authority", "trader", trader.String(), "signer", signer.String()) + return orderId, false, hu.ErrNoTradingAuthority + } + + requiredMargin := big.NewInt(0) + if !order.ReduceOnly { + // P2. Margin is available for non-reduce only orders + minAllowableMargin := api.configService.GetMinAllowableMargin() + // even tho order might be matched at a different price, we reserve margin at the price the order was placed at to keep it simple + requiredMargin = hu.GetRequiredMargin(order.Price, hu.Abs(order.BaseAssetQuantity), minAllowableMargin, big.NewInt(0)) + availableMargin := api.db.GetMarginAvailableForMakerbook(trader, hu.ArrayToMap(api.configService.GetUnderlyingPrices())) + if availableMargin.Cmp(requiredMargin) == -1 { + return orderId, false, hu.ErrInsufficientMargin + } + } else { + // @todo P3. Sum of all reduce only orders should not exceed the total position size + return orderId, false, errors.New("reduce only orders via makerbook are not supported yet") + } + + // P4. Post only order shouldn't cross the market + if order.PostOnly { + orderSide := hu.Side(hu.Long) + if order.BaseAssetQuantity.Sign() == -1 { + orderSide = hu.Side(hu.Short) + } + asksHead := fields.AsksHead + bidsHead := fields.BidsHead + if (orderSide == hu.Side(hu.Short) && bidsHead.Sign() != 0 && order.Price.Cmp(bidsHead) != 1) || (orderSide == hu.Side(hu.Long) && asksHead.Sign() != 0 && order.Price.Cmp(asksHead) != -1) { + return orderId, false, hu.ErrCrossingMarket + } + } + + // P5. HasReferrer + if !api.configService.HasReferrer(order.Trader) { + return orderId, false, hu.ErrNoReferrer + } + + // validations passed, add to db + signedOrder := &Order{ + Id: orderId, + Market: Market(order.AmmIndex.Int64()), + PositionType: getPositionTypeBasedOnBaseAssetQuantity(order.BaseAssetQuantity), + Trader: trader, + BaseAssetQuantity: order.BaseAssetQuantity, + FilledBaseAssetQuantity: big.NewInt(0), + Price: order.Price, + Salt: order.Salt, + ReduceOnly: order.ReduceOnly, + BlockNumber: big.NewInt(0), + RawOrder: order, + OrderType: Signed, + } + + placeSignedOrderCounter.Inc(1) + api.db.AddSignedOrder(signedOrder, requiredMargin) + + if len(MakerbookDatabaseFile) > 0 { + go func() { + select { + case api.makerbookFileWriteChan <- *signedOrder: + log.Info("Successfully sent to the makerbook file write channel") + default: + log.Error("Failed to send to the makerbook file write channel", "order", signedOrder.Id.String()) + } + }() + } + + // send to trader feed - both for head and accepted block + go func() { + orderMap := order.Map() + orderMap["orderType"] = "signed" + orderMap["expireAt"] = order.ExpireAt.String() + args := map[string]interface{}{ + "order": orderMap, + } + + traderEvent := TraderEvent{ + Trader: trader, + Removed: false, + EventName: "OrderAccepted", + Args: args, + BlockStatus: ConfirmationLevelHead, + OrderId: orderId, + OrderType: Signed.String(), + Timestamp: big.NewInt(time.Now().Unix()), + } + + traderFeed.Send(traderEvent) + + traderEvent.BlockStatus = ConfirmationLevelAccepted + traderFeed.Send(traderEvent) + }() + + return orderId, fields.ShouldTriggerMatching, nil +} + +func writeOrderToFile(order Order) { + doc := map[string]interface{}{ + "type": "OrderAccepted", + "timestamp": time.Now().Unix(), + "trader": order.Trader.String(), + "orderHash": strings.ToLower(order.Id.String()), + "orderType": "signed", + "order": map[string]interface{}{ + "orderType": 2, + "expireAt": order.getExpireAt().Uint64(), + "ammIndex": int(order.Market), + "trader": order.Trader.String(), + "baseAssetQuantity": utils.BigIntToFloat(order.BaseAssetQuantity, 18), + "price": utils.BigIntToFloat(order.Price, 6), + "salt": order.Salt.Int64(), + "reduceOnly": order.ReduceOnly, + }, + } + jsonDoc, err := json.Marshal(doc) + if err != nil { + log.Error("writeOrderToFile: failed to marshal order", "err", err) + makerBookWriteFailuresCounter.Inc(1) + return + } + err = utils.AppendToFile(MakerbookDatabaseFile, jsonDoc) + if err != nil { + log.Error("writeOrderToFile: failed to write order to file", "err", err) + makerBookWriteFailuresCounter.Inc(1) + } +} diff --git a/plugin/evm/orderbook/tx_processor.go b/plugin/evm/orderbook/tx_processor.go new file mode 100644 index 0000000000..6697319b5a --- /dev/null +++ b/plugin/evm/orderbook/tx_processor.go @@ -0,0 +1,390 @@ +package orderbook + +import ( + "context" + "crypto/ecdsa" + "encoding/hex" + "time" + + // "encoding/hex" + "errors" + "fmt" + "math/big" + + "github.com/ava-labs/subnet-evm/accounts/abi" + "github.com/ava-labs/subnet-evm/core/txpool" + "github.com/ava-labs/subnet-evm/core/types" + "github.com/ava-labs/subnet-evm/eth" + "github.com/ava-labs/subnet-evm/metrics" + "github.com/ava-labs/subnet-evm/plugin/evm/orderbook/abis" + "github.com/ava-labs/subnet-evm/utils" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" +) + +var OrderBookContractAddress = common.HexToAddress("0x03000000000000000000000000000000000000b0") +var MarginAccountContractAddress = common.HexToAddress("0x03000000000000000000000000000000000000b1") +var ClearingHouseContractAddress = common.HexToAddress("0x03000000000000000000000000000000000000b2") +var LimitOrderBookContractAddress = common.HexToAddress("0x03000000000000000000000000000000000000b3") +var IOCOrderBookContractAddress = common.HexToAddress("0x03000000000000000000000000000000000000b4") + +type LimitOrderTxProcessor interface { + GetOrderBookTxsCount() uint64 + GetOrderBookTxs() map[common.Address]types.Transactions + SetOrderBookTxsBlockNumber(blockNumber uint64) + PurgeOrderBookTxs() + ExecuteMatchedOrdersTx(incomingOrder Order, matchedOrder Order, fillAmount *big.Int) error + ExecuteFundingPaymentTx() error + ExecuteSamplePITx() error + ExecuteLiquidation(trader common.Address, matchedOrder Order, fillAmount *big.Int) error + UpdateMetrics(block *types.Block) + ExecuteLimitOrderCancel(orders []LimitOrder) error +} + +type ValidatorTxFeeConfig struct { + baseFeeEstimate *big.Int + blockNumber uint64 +} + +type limitOrderTxProcessor struct { + txPool *txpool.TxPool + memoryDb LimitOrderDatabase + orderBookABI abi.ABI + limitOrderBookABI abi.ABI + clearingHouseABI abi.ABI + marginAccountABI abi.ABI + orderBookContractAddress common.Address + limitOrderBookContractAddress common.Address + clearingHouseContractAddress common.Address + marginAccountContractAddress common.Address + backend *eth.EthAPIBackend + validatorAddress common.Address + validatorPrivateKey string + validatorTxFeeConfig ValidatorTxFeeConfig +} + +func NewLimitOrderTxProcessor(txPool *txpool.TxPool, memoryDb LimitOrderDatabase, backend *eth.EthAPIBackend, validatorPrivateKey string) LimitOrderTxProcessor { + orderBookABI, err := abi.FromSolidityJson(string(abis.OrderBookAbi)) + if err != nil { + panic(err) + } + + limitOrderBookABI, err := abi.FromSolidityJson(string(abis.LimitOrderBookAbi)) + if err != nil { + panic(err) + } + + clearingHouseABI, err := abi.FromSolidityJson(string(abis.ClearingHouseAbi)) + if err != nil { + panic(err) + } + + marginAccountABI, err := abi.FromSolidityJson(string(abis.MarginAccountAbi)) + if err != nil { + panic(err) + } + + validatorAddress, err := getAddressFromPrivateKey(validatorPrivateKey) + if err != nil { + panic("Unable to get address from validator private key") + } + + lotp := &limitOrderTxProcessor{ + txPool: txPool, + limitOrderBookABI: limitOrderBookABI, + orderBookABI: orderBookABI, + clearingHouseABI: clearingHouseABI, + marginAccountABI: marginAccountABI, + memoryDb: memoryDb, + limitOrderBookContractAddress: LimitOrderBookContractAddress, + orderBookContractAddress: OrderBookContractAddress, + clearingHouseContractAddress: ClearingHouseContractAddress, + marginAccountContractAddress: MarginAccountContractAddress, + backend: backend, + validatorAddress: validatorAddress, + validatorPrivateKey: validatorPrivateKey, + validatorTxFeeConfig: ValidatorTxFeeConfig{baseFeeEstimate: big.NewInt(0), blockNumber: 0}, + } + return lotp +} + +func (lotp *limitOrderTxProcessor) ExecuteLiquidation(trader common.Address, matchedOrder Order, fillAmount *big.Int) error { + orderBytes, err := matchedOrder.RawOrder.EncodeToABI() + if err != nil { + log.Error("EncodeLimitOrder failed in ExecuteLiquidation", "order", matchedOrder, "err", err) + return err + } + txHash, err := lotp.executeLocalTx(lotp.orderBookContractAddress, lotp.orderBookABI, "liquidateAndExecuteOrder", trader, orderBytes, fillAmount) + log.Info("ExecuteLiquidation", "trader", trader, "matchedOrder", matchedOrder, "fillAmount", prettifyScaledBigInt(fillAmount, 18), "txHash", txHash.String(), "err", err) + // log.Info("ExecuteLiquidation", "trader", trader, "matchedOrder", matchedOrder, "fillAmount", prettifyScaledBigInt(fillAmount, 18), "orderBytes", hex.EncodeToString(orderBytes), "txHash", txHash.String(), "err", err) + return err +} + +func (lotp *limitOrderTxProcessor) ExecuteFundingPaymentTx() error { + txHash, err := lotp.executeLocalTx(lotp.clearingHouseContractAddress, lotp.clearingHouseABI, "settleFunding") + log.Info("ExecuteFundingPaymentTx", "txHash", txHash.String(), "err", err) + return err +} + +func (lotp *limitOrderTxProcessor) ExecuteSamplePITx() error { + impactBids, impactAsks, midPrices := lotp.memoryDb.SampleImpactPrice() + txHash, err := lotp.executeLocalTx(lotp.orderBookContractAddress, lotp.orderBookABI, "commitLiquiditySample", impactBids, impactAsks, midPrices) + log.Info("ExecuteSamplePITx", "txHash", txHash.String(), "err", err) + if err == nil { + lotp.memoryDb.SignalSamplePIAttempted(uint64(time.Now().Unix())) + } + return err +} + +func (lotp *limitOrderTxProcessor) ExecuteMatchedOrdersTx(longOrder Order, shortOrder Order, fillAmount *big.Int) error { + var err error + orders := make([][]byte, 2) + orders[0], err = longOrder.RawOrder.EncodeToABI() + if err != nil { + log.Error("EncodeLimitOrder failed for longOrder", "order", longOrder, "err", err) + return err + } + + orders[1], err = shortOrder.RawOrder.EncodeToABI() + if err != nil { + log.Error("EncodeLimitOrder failed for shortOrder", "order", shortOrder, "err", err) + return err + } + log.Info("ExecuteMatchedOrdersTx", "longOrder", hex.EncodeToString(orders[0]), "shortOrder", hex.EncodeToString(orders[1]), "fillAmount", prettifyScaledBigInt(fillAmount, 18), "err", err) + + txHash, err := lotp.executeLocalTx(lotp.orderBookContractAddress, lotp.orderBookABI, "executeMatchedOrders", orders, fillAmount) + log.Info("ExecuteMatchedOrdersTx", "LongOrder", longOrder, "ShortOrder", shortOrder, "fillAmount", prettifyScaledBigInt(fillAmount, 18), "txHash", txHash.String(), "err", err) + return err +} + +func (lotp *limitOrderTxProcessor) ExecuteLimitOrderCancel(orders []LimitOrder) error { + txHash, err := lotp.executeLocalTx(lotp.limitOrderBookContractAddress, lotp.limitOrderBookABI, "cancelOrdersWithLowMargin", orders) + log.Info("ExecuteLimitOrderCancel", "orders", orders, "txHash", txHash.String(), "err", err) + return err +} + +func (lotp *limitOrderTxProcessor) executeLocalTx(contract common.Address, contractABI abi.ABI, method string, args ...interface{}) (common.Hash, error) { + var txHash common.Hash + nonce := lotp.txPool.GetOrderBookTxNonce(common.HexToAddress(lotp.validatorAddress.Hex())) // admin address + + data, err := contractABI.Pack(method, args...) + if err != nil { + log.Error("abi.Pack failed", "method", method, "args", args, "err", err) + return txHash, err + } + key, err := crypto.HexToECDSA(lotp.validatorPrivateKey) // admin private key + if err != nil { + log.Error("HexToECDSA failed", "err", err) + return txHash, err + } + txFee := lotp.getTransactionFee() + tx := types.NewTransaction(nonce, contract, big.NewInt(0), 1500000, txFee, data) + signer := types.NewLondonSigner(lotp.backend.ChainConfig().ChainID) + signedTx, err := types.SignTx(tx, signer, key) + if err != nil { + log.Error("types.SignTx failed", "err", err) + return txHash, err + } + txHash = signedTx.Hash() + err = lotp.txPool.AddOrderBookTx(signedTx) + if err != nil { + log.Error("lop.txPool.AddOrderBookTx failed", "err", err, "tx", signedTx.Hash().String(), "nonce", nonce) + return txHash, err + } + + return txHash, nil +} + +func (lotp *limitOrderTxProcessor) getTransactionFee() *big.Int { + latest := lotp.backend.CurrentHeader() + latestBlockNumber := latest.Number.Uint64() + + // if the fee is already calculated for this block, then return it + if lotp.validatorTxFeeConfig.blockNumber == latestBlockNumber { + return lotp.validatorTxFeeConfig.baseFeeEstimate + } + + baseFeeEstimate, err := lotp.backend.SuggestPrice(context.Background()) + if err != nil { + log.Error("getBaseFeeEstimate - SuggestPrice failed", "err", err) + return big.NewInt(65_000000000) // hardcoded to 65 gwei + } + // add 10% + baseFeeEstimate.Add(baseFeeEstimate, big.NewInt(0).Div(baseFeeEstimate, big.NewInt(10))) + + feeConfig, _, err := lotp.backend.GetFeeConfigAt(latest) + if err != nil { + log.Error("getBaseFeeEstimate - GetFeeConfigAt failed", "err", err) + // if feeConfig can't be obtained, then add another 10% to the baseFeeEstimate + baseFeeEstimate.Add(baseFeeEstimate, big.NewInt(0).Div(baseFeeEstimate, big.NewInt(10))) + return baseFeeEstimate + } + // assuming pessimistically that the block is being produced within a second of the latest block + // we calculate the block gas cost as the latest block gas cost + the block gas cost step + blockGasCost := big.NewInt(0).Add(latest.BlockGasCost, feeConfig.BlockGasCostStep) + + // assuming a minimum gas usage of 200k for a tx, we calculate the tip such that the entire block has an effective tip above the threshold + // example calculation for blockGasCost = 10,000, baseFeeEstimate = 60 gwei, tx gas usage = 200,000 + // tip = (10000 * 60 * 1e9) / 200000 = 3 gwei + tip := big.NewInt(0).Div(big.NewInt(0).Mul(blockGasCost, baseFeeEstimate), big.NewInt(200000)) + + totalFee := baseFeeEstimate.Add(baseFeeEstimate, tip) + + lotp.validatorTxFeeConfig.baseFeeEstimate = totalFee + lotp.validatorTxFeeConfig.blockNumber = latestBlockNumber + return totalFee +} + +func (lotp *limitOrderTxProcessor) PurgeOrderBookTxs() { + lotp.txPool.PurgeOrderBookTxs() +} + +func (lotp *limitOrderTxProcessor) GetOrderBookTxsCount() uint64 { + return lotp.txPool.GetOrderBookTxsCount() +} + +func (lotp *limitOrderTxProcessor) GetOrderBookTxs() map[common.Address]types.Transactions { + return lotp.txPool.GetOrderBookTxs() +} + +func (lotp *limitOrderTxProcessor) SetOrderBookTxsBlockNumber(blockNumber uint64) { + lotp.txPool.SetOrderBookTxsBlockNumber(blockNumber) +} + +func getPositionTypeBasedOnBaseAssetQuantity(baseAssetQuantity *big.Int) PositionType { + if baseAssetQuantity.Sign() == 1 { + return LONG + } + return SHORT +} + +func checkIfOrderBookContractCall(tx *types.Transaction, orderBookABI abi.ABI, orderBookContractAddress common.Address) bool { + input := tx.Data() + if tx.To() != nil && tx.To().Hash() == orderBookContractAddress.Hash() && len(input) > 3 { + return true + } + return false +} + +func getOrderBookContractCallMethod(tx *types.Transaction, orderBookABI abi.ABI, orderBookContractAddress common.Address) (*abi.Method, error) { + if checkIfOrderBookContractCall(tx, orderBookABI, orderBookContractAddress) { + input := tx.Data() + method := input[:4] + m, err := orderBookABI.MethodById(method) + return m, err + } else { + err := errors.New("tx is not an orderbook contract call") + return nil, err + } +} + +func getAddressFromPrivateKey(key string) (common.Address, error) { + // blank key is allowed for non-validators + if key == "" { + return common.Address{}, nil + } + privateKey, err := crypto.HexToECDSA(key) // admin private key + if err != nil { + return common.Address{}, err + } + publicKey := privateKey.Public() + publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) + if !ok { + return common.Address{}, errors.New("unable to get address from private key") + } + address := crypto.PubkeyToAddress(*publicKeyECDSA) + return address, nil +} + +func formatReceiptForLogging(receipt *types.Receipt) string { + return fmt.Sprintf("Receipt{Status: %d, CumulativeGasUsed: %d, GasUsed: %d, EffectiveGasPrice: %d, BlockNumber: %d}", + receipt.Status, receipt.CumulativeGasUsed, receipt.GasUsed, receipt.EffectiveGasPrice, receipt.BlockNumber) +} + +func (lotp *limitOrderTxProcessor) UpdateMetrics(block *types.Block) { + // defer func(start time.Time) { log.Info("limitOrderTxProcessor.UpdateMetrics", "time", time.Since(start)) }(time.Now()) + + transactionsPerBlockHistogram.Update(int64(len(block.Transactions()))) + gasUsedPerBlockHistogram.Update(int64(block.GasUsed())) + blockGasCostPerBlockHistogram.Update(block.BlockGasCost().Int64()) + + ctx := context.Background() + txs := block.Transactions() + + receipts, err := lotp.backend.GetReceipts(ctx, block.Hash()) + if err != nil { + log.Error("UpdateMetrics - lotp.backend.GetReceipts failed", "err", err) + return + } + + bigblock := new(big.Int).SetUint64(block.NumberU64()) + timestamp := block.Header().Time + signer := types.MakeSigner(lotp.backend.ChainConfig(), bigblock, timestamp) + + currentBlock := lotp.backend.CurrentBlock() // head block + headBlockLagHistogram.Update(int64(currentBlock.Number.Uint64() - block.NumberU64())) + + for i := 0; i < len(txs); i++ { + tx := txs[i] + receipt := receipts[i] + from, _ := types.Sender(signer, tx) + contractAddress := tx.To() + input := tx.Data() + if contractAddress == nil || len(input) < 4 { + continue + } + method_ := input[:4] + method, _ := lotp.orderBookABI.MethodById(method_) + + if method == nil { + continue + } + + if from == lotp.validatorAddress { + if receipt.Status == 0 { + orderBookTransactionsFailureTotalCounter.Inc(1) + } else if receipt.Status == 1 { + orderBookTransactionsSuccessTotalCounter.Inc(1) + } + + if contractAddress != nil && (lotp.orderBookContractAddress == *contractAddress || lotp.clearingHouseContractAddress == *contractAddress) { + note := "success" + if receipt.Status == 0 { + log.Error("this validator's tx failed", "method", method.Name, "tx", tx.Hash().String(), + "receipt", formatReceiptForLogging(receipt), "from", from.String()) + note = "failure" + } + counterName := fmt.Sprintf("orderbooktxs/%s/%s", method.Name, note) + metrics.GetOrRegisterCounter(counterName, nil).Inc(1) + } + } + + if contractAddress != nil { + var contractName string + switch *contractAddress { + case lotp.orderBookContractAddress: + contractName = "OrderBook" + case lotp.clearingHouseContractAddress: + contractName = "ClearingHouse" + case lotp.marginAccountContractAddress: + contractName = "MarginAccount" + default: + continue + } + + // measure the gas usage irrespective of whether the tx is from this validator or not + gasUsageMetric := fmt.Sprintf("orderbooktxs/%s/%s/gas", contractName, method.Name) + sampler := metrics.ResettingSample(metrics.NewExpDecaySample(1028, 0.015)) + metrics.GetOrRegisterHistogram(gasUsageMetric, nil, sampler).Update(int64(receipt.GasUsed)) + + // log the failure for validator txs irrespective of whether the tx is from this validator or not + // this will help us identify tx failures that are not due to a hubble's validator + validatorMethods := []string{"liquidateAndExecuteOrder", "executeMatchedOrders", "settleFunding", "samplePI", "cancelOrdersWithLowMargin"} + if receipt.Status == 0 && utils.ContainsString(validatorMethods, method.Name) { + log.Error("validator tx failed", "method", method.Name, "contractName", contractName, "tx", tx.Hash().String(), "from", from.String(), "receipt", formatReceiptForLogging(receipt)) + } + } + } +} diff --git a/plugin/evm/orderbook_test.go b/plugin/evm/orderbook_test.go new file mode 100644 index 0000000000..1d6c538af0 --- /dev/null +++ b/plugin/evm/orderbook_test.go @@ -0,0 +1,1131 @@ +package evm + +import ( + "context" + "crypto/ecdsa" + "math/big" + "testing" + "time" + + "github.com/ava-labs/avalanchego/snow/choices" + "github.com/ava-labs/avalanchego/snow/consensus/snowman" + "github.com/ava-labs/subnet-evm/accounts/abi" + + "github.com/ava-labs/subnet-evm/core/types" + "github.com/ava-labs/subnet-evm/plugin/evm/orderbook" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" +) + +var ( + genesisJSON string + orderBookABI abi.ABI + alice, bob common.Address + aliceKey, bobKey *ecdsa.PrivateKey + orderBookABIStr string = `{ + "abi": [ + { + "anonymous": false, + "inputs": [], + "name": "EIP712DomainChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "version", + "type": "uint8" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "string", + "name": "err", + "type": "string" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "toLiquidate", + "type": "uint256" + } + ], + "name": "LiquidationError", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "signature", + "type": "bytes" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "fillAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "openInterestNotional", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "relayer", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "name": "LiquidationOrderMatched", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "int256", + "name": "baseAssetQuantity", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "salt", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "reduceOnly", + "type": "bool" + }, + { + "internalType": "bool", + "name": "postOnly", + "type": "bool" + } + ], + "indexed": false, + "internalType": "struct IOrderBook.Order", + "name": "order", + "type": "tuple" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "name": "OrderAccepted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "name": "OrderCancelled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "string", + "name": "err", + "type": "string" + } + ], + "name": "OrderMatchingError", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "orderHash0", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "orderHash1", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "fillAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "openInterestNotional", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "relayer", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "name": "OrdersMatched", + "type": "event" + }, + { + "inputs": [], + "name": "ORDER_TYPEHASH", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "int256", + "name": "baseAssetQuantity", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "salt", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "reduceOnly", + "type": "bool" + }, + { + "internalType": "bool", + "name": "postOnly", + "type": "bool" + } + ], + "internalType": "struct IOrderBook.Order", + "name": "order", + "type": "tuple" + } + ], + "name": "cancelOrder", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "eip712Domain", + "outputs": [ + { + "internalType": "bytes1", + "name": "fields", + "type": "bytes1" + }, + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "version", + "type": "string" + }, + { + "internalType": "uint256", + "name": "chainId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "verifyingContract", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "salt", + "type": "bytes32" + }, + { + "internalType": "uint256[]", + "name": "extensions", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "int256", + "name": "baseAssetQuantity", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "salt", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "reduceOnly", + "type": "bool" + }, + { + "internalType": "bool", + "name": "postOnly", + "type": "bool" + } + ], + "internalType": "struct IOrderBook.Order[2]", + "name": "orders", + "type": "tuple[2]" + }, + { + "internalType": "int256", + "name": "fillAmount", + "type": "int256" + } + ], + "name": "executeMatchedOrders", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "int256", + "name": "baseAssetQuantity", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "salt", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "reduceOnly", + "type": "bool" + }, + { + "internalType": "bool", + "name": "postOnly", + "type": "bool" + } + ], + "internalType": "struct IOrderBook.Order", + "name": "order", + "type": "tuple" + }, + { + "internalType": "bytes", + "name": "signature", + "type": "bytes" + } + ], + "name": "executeTestOrder", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "getLastTradePrices", + "outputs": [ + { + "internalType": "uint256[]", + "name": "lastTradePrices", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "int256", + "name": "baseAssetQuantity", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "salt", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "reduceOnly", + "type": "bool" + }, + { + "internalType": "bool", + "name": "postOnly", + "type": "bool" + } + ], + "internalType": "struct IOrderBook.Order", + "name": "order", + "type": "tuple" + } + ], + "name": "getOrderHash", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "version", + "type": "string" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "lastPrices", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "int256", + "name": "baseAssetQuantity", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "salt", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "reduceOnly", + "type": "bool" + }, + { + "internalType": "bool", + "name": "postOnly", + "type": "bool" + } + ], + "internalType": "struct IOrderBook.Order", + "name": "order", + "type": "tuple" + }, + { + "internalType": "bytes", + "name": "signature", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "toLiquidate", + "type": "uint256" + } + ], + "name": "liquidateAndExecuteOrder", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "numAmms", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "orderInfo", + "outputs": [ + { + "internalType": "uint256", + "name": "blockPlaced", + "type": "uint256" + }, + { + "internalType": "int256", + "name": "filledAmount", + "type": "int256" + }, + { + "internalType": "enum IOrderBook.OrderStatus", + "name": "status", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "int256", + "name": "baseAssetQuantity", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "salt", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "reduceOnly", + "type": "bool" + }, + { + "internalType": "bool", + "name": "postOnly", + "type": "bool" + } + ], + "internalType": "struct IOrderBook.Order", + "name": "order", + "type": "tuple" + } + ], + "name": "placeOrder", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "positions", + "outputs": [ + { + "internalType": "int256", + "name": "size", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "openNotional", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_num", + "type": "uint256" + } + ], + "name": "setNumAMMs", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "settleFunding", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "int256", + "name": "baseAssetQuantity", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "salt", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "reduceOnly", + "type": "bool" + }, + { + "internalType": "bool", + "name": "postOnly", + "type": "bool" + } + ], + "internalType": "struct IOrderBook.Order", + "name": "order", + "type": "tuple" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "verifySigner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + } + ] + }` + orderBookAddress common.Address = common.HexToAddress("0x03000000000000000000000000000000000000b3") + _1e18 *big.Int = big.NewInt(1e18) + _1e6 *big.Int = big.NewInt(1e6) +) + +func init() { + var err error + + genesisJSON = `{"config":{"chainId":321123,"homesteadBlock":0,"eip150Block":0,"eip150Hash":"0x2086799aeebeae135c246c65021c82b4e15a2c451340993aacfd2751886514f0","eip155Block":0,"eip158Block":0,"byzantiumBlock":0,"constantinopleBlock":0,"petersburgBlock":0,"istanbulBlock":0,"muirGlacierBlock":0,"SubnetEVMTimestamp":0,"feeConfig":{"gasLimit":500000000,"targetBlockRate":1,"minBaseFee":60000000000,"targetGas":10000000,"baseFeeChangeDenominator":50,"minBlockGasCost":0,"maxBlockGasCost":0,"blockGasCostStep":10000}},"alloc":{"835cE0760387BC894E91039a88A00b6a69E65D94":{"balance":"0xD3C21BCECCEDA1000000"},"8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC":{"balance":"0xD3C21BCECCEDA1000000"},"55ee05dF718f1a5C1441e76190EB1a19eE2C9430":{"balance":"0xD3C21BCECCEDA1000000"},"4Cf2eD3665F6bFA95cE6A11CFDb7A2EF5FC1C7E4":{"balance":"0xD3C21BCECCEDA1000000"},"f39Fd6e51aad88F6F4ce6aB8827279cffFb92266":{"balance":"0xD3C21BCECCEDA1000000"},"70997970C51812dc3A010C7d01b50e0d17dc79C8":{"balance":"0xD3C21BCECCEDA1000000"},"3C44CdDdB6a900fa2b585dd299e03d12FA4293BC":{"balance":"0xD3C21BCECCEDA1000000"},"0x03000000000000000000000000000000000000b3":{"balance":"0x0","code":"0x608060405234801561001057600080fd5b506004361061010b5760003560e01c8063508bac1f116100a25780639b809602116100715780639b809602146102aa578063e47c2384146102c6578063e684d718146102f7578063ed83d79c14610328578063f973a209146103325761010b565b8063508bac1f1461021c5780637114f7f814610238578063715d587c1461025657806384b0196e146102865761010b565b80633245dea5116100de5780633245dea51461019857806342c1f8a4146101c85780634cd88b76146101e45780634e545b4d146102005761010b565b8063238e203f146101105780632695cf011461014257806327d57a9e1461015e5780632cc751151461017c575b600080fd5b61012a60048036038101906101259190611a7b565b610350565b60405161013993929190611b51565b60405180910390f35b61015c60048036038101906101579190611e75565b610387565b005b6101666103c6565b6040516101739190611ed2565b60405180910390f35b61019660048036038101906101919190611fa3565b6103cc565b005b6101b260048036038101906101ad9190611fe5565b6106bc565b6040516101bf9190611ed2565b60405180910390f35b6101e260048036038101906101dd9190611fe5565b6106d4565b005b6101fe60048036038101906101f991906120b3565b6106de565b005b61021a6004803603810190610215919061212b565b61082a565b005b6102366004803603810190610231919061212b565b6109c9565b005b610240610ab8565b60405161024d9190612216565b60405180910390f35b610270600480360381019061026b919061212b565b610b5e565b60405161027d9190612247565b60405180910390f35b61028e610bbb565b6040516102a19796959493929190612334565b60405180910390f35b6102c460048036038101906102bf91906123b8565b610cbc565b005b6102e060048036038101906102db9190611e75565b610e7b565b6040516102ee92919061243e565b60405180910390f35b610311600480360381019061030c9190612467565b610e9d565b60405161031f9291906124a7565b60405180910390f35b610330610ece565b005b61033a610ed0565b6040516103479190612247565b60405180910390f35b60356020528060005260406000206000915090508060000154908060010154908060020160009054906101000a900460ff16905083565b600061039883838560400151610ef7565b5090506103ae81846040015185604001516110dc565b6103c1838460400151856060015161116c565b505050565b60385481565b6000826000600281106103e2576103e16124d0565b5b6020020151604001511361042b576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016104229061254b565b60405180910390fd5b600082600160028110610441576104406124d0565b5b6020020151604001511261048a576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610481906125b7565b60405180910390fd5b600081136104cd576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016104c490612623565b60405180910390fd5b816001600281106104e1576104e06124d0565b5b602002015160600151826000600281106104fe576104fd6124d0565b5b6020020151606001511015610548576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161053f9061268f565b60405180910390fd5b600061056b83600060028110610561576105606124d0565b5b6020020151610b5e565b9050600061059084600160028110610586576105856124d0565b5b6020020151610b5e565b90506105b98284866000600281106105ab576105aa6124d0565b5b6020020151604001516110dc565b6105e981846105c7906126de565b866001600281106105db576105da6124d0565b5b6020020151604001516110dc565b6000846000600281106105ff576105fe6124d0565b5b602002015160600151905061062d85600060028110610621576106206124d0565b5b6020020151858361116c565b61065985600160028110610644576106436124d0565b5b602002015185610653906126de565b8361116c565b81837faf4b403d9952e032974b549a4abad80faca307b0acc6e34d7e0b8c274d504590610685876114d5565b84856106908a6114d5565b61069a9190612727565b33426040516106ad959493929190612781565b60405180910390a35050505050565b60376020528060005260406000206000915090505481565b8060388190555050565b60008060019054906101000a900460ff1615905080801561070f5750600160008054906101000a900460ff1660ff16105b8061073c575061071e30611522565b15801561073b5750600160008054906101000a900460ff1660ff16145b5b61077b576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161077290612846565b60405180910390fd5b60016000806101000a81548160ff021916908360ff16021790555080156107b8576001600060016101000a81548160ff0219169083151502179055505b6107c28383611545565b6107cc60016106d4565b80156108255760008060016101000a81548160ff0219169083151502179055507f7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb3847402498600160405161081c91906128b8565b60405180910390a15b505050565b806020015173ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161461089c576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016108939061291f565b60405180910390fd5b60006108a782610b5e565b9050600160038111156108bd576108bc611ada565b5b6035600083815260200190815260200160002060020160009054906101000a900460ff1660038111156108f3576108f2611ada565b5b14610933576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161092a9061298b565b60405180910390fd5b60036035600083815260200190815260200160002060020160006101000a81548160ff0219169083600381111561096d5761096c611ada565b5b021790555080826020015173ffffffffffffffffffffffffffffffffffffffff167f26b214029d2b6a3a3bb2ae7cc0a5d4c9329a86381429e16dc45b3633cf83d369426040516109bd9190611ed2565b60405180910390a35050565b60006109d482610b5e565b905060405180606001604052804381526020016000815260200160016003811115610a0257610a01611ada565b5b81525060356000838152602001908152602001600020600082015181600001556020820151816001015560408201518160020160006101000a81548160ff02191690836003811115610a5757610a56611ada565b5b021790555090505080826020015173ffffffffffffffffffffffffffffffffffffffff167f70efd0c97c9e59c5cbc4bd4e40365b942df3603cd71c223f6940e3fca16356358442604051610aac929190612a66565b60405180910390a35050565b606060385467ffffffffffffffff811115610ad657610ad5611b9e565b5b604051908082528060200260200182016040528015610b045781602001602082028036833780820191505090505b50905060005b603854811015610b5a576037600082815260200190815260200160002054828281518110610b3b57610b3a6124d0565b5b6020026020010181815250508080610b5290612a90565b915050610b0a565b5090565b6000610bb47f0a2e4d36552888a97d5a8975ad22b04e90efe5ea0a8abb97691b63b431eb25d260001b83604051602001610b99929190612ad9565b604051602081830303815290604052805190602001206115a2565b9050919050565b6000606080600080600060606000801b600154148015610bdf57506000801b600254145b610c1e576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610c1590612b4f565b60405180910390fd5b610c266115bc565b610c2e61164e565b46306000801b600067ffffffffffffffff811115610c4f57610c4e611b9e565b5b604051908082528060200260200182016040528015610c7d5781602001602082028036833780820191505090505b507f0f00000000000000000000000000000000000000000000000000000000000000959493929190965096509650965096509650965090919293949596565b670de0b6b3a7640000818460600151610cd59190612727565b610cdf9190612b9e565b603660008560000151815260200190815260200160002060008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000206001016000828254610d459190612bcf565b92505081905550610d55816116e0565b603660008560000151815260200190815260200160002060008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000206000016000828254610dbb9190612c03565b925050819055506000610dd78484610dd2856116e0565b610ef7565b509050610df181610de7846116e0565b86604001516110dc565b610e0884610dfe846116e0565b866060015161116c565b808573ffffffffffffffffffffffffffffffffffffffff167fd7a2e338b47db7ba2c25b55a69d8eb13126b1ec669de521cd1985aae9ee32ca185858860600151878a60600151610e589190612727565b3342604051610e6c96959493929190612cec565b60405180910390a35050505050565b6000806000610e8985610b5e565b905084602001518192509250509250929050565b6036602052816000526040600020602052806000526040600020600091509150508060000154908060010154905082565b565b7f0a2e4d36552888a97d5a8975ad22b04e90efe5ea0a8abb97691b63b431eb25d260001b81565b6000806000610f068686610e7b565b91505060016003811115610f1d57610f1c611ada565b5b6035600083815260200190815260200160002060020160009054906101000a900460ff166003811115610f5357610f52611ada565b5b14610f93576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610f8a90612da0565b60405180910390fd5b6000848760400151610fa59190612dc0565b13610fe5576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610fdc90612f23565b60405180910390fd5b60008460356000848152602001908152602001600020600101546110099190612dc0565b121561104a576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161104190612f8f565b60405180910390fd5b611057866040015161174d565b611076603560008481526020019081526020016000206001015461174d565b13156110b7576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016110ae90613021565b60405180910390fd5b8060356000838152602001908152602001600020600001549250925050935093915050565b816035600085815260200190815260200160002060010160008282546111029190613041565b9250508190555080603560008581526020019081526020016000206001015414156111675760026035600085815260200190815260200160002060020160006101000a81548160ff0219169083600381111561116157611160611ada565b5b02179055505b505050565b6000670de0b6b3a7640000826111896111848661174d565b6114d5565b6111939190612727565b61119d9190612b9e565b905060008460200151905060008560000151905060385481106111f5576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016111ec90613121565b60405180910390fd5b6000856036600084815260200190815260200160002060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600001546112569190612dc0565b126112ca57826036600083815260200190815260200160002060008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060010160008282546112be9190613141565b9250508190555061144b565b826036600083815260200190815260200160002060008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600101541061139357826036600083815260200190815260200160002060008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060010160008282546113879190612bcf565b9250508190555061144a565b6036600082815260200190815260200160002060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060010154836113f29190612bcf565b6036600083815260200190815260200160002060008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600101819055505b5b846036600083815260200190815260200160002060008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060000160008282546114ae9190613041565b92505081905550836037600083815260200190815260200160002081905550505050505050565b60008082121561151a576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401611511906131e3565b60405180910390fd5b819050919050565b6000808273ffffffffffffffffffffffffffffffffffffffff163b119050919050565b600060019054906101000a900460ff16611594576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161158b90613275565b60405180910390fd5b61159e828261176f565b5050565b60006115b56115af611804565b83611813565b9050919050565b6060600380546115cb906132c4565b80601f01602080910402602001604051908101604052809291908181526020018280546115f7906132c4565b80156116445780601f1061161957610100808354040283529160200191611644565b820191906000526020600020905b81548152906001019060200180831161162757829003601f168201915b5050505050905090565b60606004805461165d906132c4565b80601f0160208091040260200160405190810160405280929190818152602001828054611689906132c4565b80156116d65780601f106116ab576101008083540402835291602001916116d6565b820191906000526020600020905b8154815290600101906020018083116116b957829003601f168201915b5050505050905090565b60007f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff821115611745576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161173c90613368565b60405180910390fd5b819050919050565b6000808212156117665781611761906126de565b611768565b815b9050919050565b600060019054906101000a900460ff166117be576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016117b590613275565b60405180910390fd5b81600390805190602001906117d492919061198e565b5080600490805190602001906117eb92919061198e565b506000801b6001819055506000801b6002819055505050565b600061180e611854565b905090565b60006040517f190100000000000000000000000000000000000000000000000000000000000081528360028201528260228201526042812091505092915050565b60007f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f61187f6118b8565b611887611923565b463060405160200161189d959493929190613388565b60405160208183030381529060405280519060200120905090565b6000806118c36115bc565b90506000815111156118df578080519060200120915050611920565b600060015490506000801b81146118fa578092505050611920565b7fc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470925050505b90565b60008061192e61164e565b905060008151111561194a57808051906020012091505061198b565b600060025490506000801b811461196557809250505061198b565b7fc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470925050505b90565b82805461199a906132c4565b90600052602060002090601f0160209004810192826119bc5760008555611a03565b82601f106119d557805160ff1916838001178555611a03565b82800160010185558215611a03579182015b82811115611a025782518255916020019190600101906119e7565b5b509050611a109190611a14565b5090565b5b80821115611a2d576000816000905550600101611a15565b5090565b6000604051905090565b600080fd5b600080fd5b6000819050919050565b611a5881611a45565b8114611a6357600080fd5b50565b600081359050611a7581611a4f565b92915050565b600060208284031215611a9157611a90611a3b565b5b6000611a9f84828501611a66565b91505092915050565b6000819050919050565b611abb81611aa8565b82525050565b6000819050919050565b611ad481611ac1565b82525050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fd5b60048110611b1a57611b19611ada565b5b50565b6000819050611b2b82611b09565b919050565b6000611b3b82611b1d565b9050919050565b611b4b81611b30565b82525050565b6000606082019050611b666000830186611ab2565b611b736020830185611acb565b611b806040830184611b42565b949350505050565b600080fd5b6000601f19601f8301169050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b611bd682611b8d565b810181811067ffffffffffffffff82111715611bf557611bf4611b9e565b5b80604052505050565b6000611c08611a31565b9050611c148282611bcd565b919050565b611c2281611aa8565b8114611c2d57600080fd5b50565b600081359050611c3f81611c19565b92915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000611c7082611c45565b9050919050565b611c8081611c65565b8114611c8b57600080fd5b50565b600081359050611c9d81611c77565b92915050565b611cac81611ac1565b8114611cb757600080fd5b50565b600081359050611cc981611ca3565b92915050565b60008115159050919050565b611ce481611ccf565b8114611cef57600080fd5b50565b600081359050611d0181611cdb565b92915050565b600060e08284031215611d1d57611d1c611b88565b5b611d2760e0611bfe565b90506000611d3784828501611c30565b6000830152506020611d4b84828501611c8e565b6020830152506040611d5f84828501611cba565b6040830152506060611d7384828501611c30565b6060830152506080611d8784828501611c30565b60808301525060a0611d9b84828501611cf2565b60a08301525060c0611daf84828501611cf2565b60c08301525092915050565b600080fd5b600080fd5b600067ffffffffffffffff821115611de057611ddf611b9e565b5b611de982611b8d565b9050602081019050919050565b82818337600083830152505050565b6000611e18611e1384611dc5565b611bfe565b905082815260208101848484011115611e3457611e33611dc0565b5b611e3f848285611df6565b509392505050565b600082601f830112611e5c57611e5b611dbb565b5b8135611e6c848260208601611e05565b91505092915050565b6000806101008385031215611e8d57611e8c611a3b565b5b6000611e9b85828601611d07565b92505060e083013567ffffffffffffffff811115611ebc57611ebb611a40565b5b611ec885828601611e47565b9150509250929050565b6000602082019050611ee76000830184611ab2565b92915050565b600067ffffffffffffffff821115611f0857611f07611b9e565b5b602082029050919050565b600080fd5b6000611f2b611f2684611eed565b611bfe565b90508060e08402830185811115611f4557611f44611f13565b5b835b81811015611f6e5780611f5a8882611d07565b84526020840193505060e081019050611f47565b5050509392505050565b600082601f830112611f8d57611f8c611dbb565b5b6002611f9a848285611f18565b91505092915050565b6000806101e08385031215611fbb57611fba611a3b565b5b6000611fc985828601611f78565b9250506101c0611fdb85828601611cba565b9150509250929050565b600060208284031215611ffb57611ffa611a3b565b5b600061200984828501611c30565b91505092915050565b600067ffffffffffffffff82111561202d5761202c611b9e565b5b61203682611b8d565b9050602081019050919050565b600061205661205184612012565b611bfe565b90508281526020810184848401111561207257612071611dc0565b5b61207d848285611df6565b509392505050565b600082601f83011261209a57612099611dbb565b5b81356120aa848260208601612043565b91505092915050565b600080604083850312156120ca576120c9611a3b565b5b600083013567ffffffffffffffff8111156120e8576120e7611a40565b5b6120f485828601612085565b925050602083013567ffffffffffffffff81111561211557612114611a40565b5b61212185828601612085565b9150509250929050565b600060e0828403121561214157612140611a3b565b5b600061214f84828501611d07565b91505092915050565b600081519050919050565b600082825260208201905092915050565b6000819050602082019050919050565b61218d81611aa8565b82525050565b600061219f8383612184565b60208301905092915050565b6000602082019050919050565b60006121c382612158565b6121cd8185612163565b93506121d883612174565b8060005b838110156122095781516121f08882612193565b97506121fb836121ab565b9250506001810190506121dc565b5085935050505092915050565b6000602082019050818103600083015261223081846121b8565b905092915050565b61224181611a45565b82525050565b600060208201905061225c6000830184612238565b92915050565b60007fff0000000000000000000000000000000000000000000000000000000000000082169050919050565b61229781612262565b82525050565b600081519050919050565b600082825260208201905092915050565b60005b838110156122d75780820151818401526020810190506122bc565b838111156122e6576000848401525b50505050565b60006122f78261229d565b61230181856122a8565b93506123118185602086016122b9565b61231a81611b8d565b840191505092915050565b61232e81611c65565b82525050565b600060e082019050612349600083018a61228e565b818103602083015261235b81896122ec565b9050818103604083015261236f81886122ec565b905061237e6060830187611ab2565b61238b6080830186612325565b61239860a0830185612238565b81810360c08301526123aa81846121b8565b905098975050505050505050565b60008060008061014085870312156123d3576123d2611a3b565b5b60006123e187828801611c8e565b94505060206123f287828801611d07565b93505061010085013567ffffffffffffffff81111561241457612413611a40565b5b61242087828801611e47565b92505061012061243287828801611c30565b91505092959194509250565b60006040820190506124536000830185612325565b6124606020830184612238565b9392505050565b6000806040838503121561247e5761247d611a3b565b5b600061248c85828601611c30565b925050602061249d85828601611c8e565b9150509250929050565b60006040820190506124bc6000830185611acb565b6124c96020830184611ab2565b9392505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b7f4f425f6f726465725f305f69735f6e6f745f6c6f6e6700000000000000000000600082015250565b60006125356016836122a8565b9150612540826124ff565b602082019050919050565b6000602082019050818103600083015261256481612528565b9050919050565b7f4f425f6f726465725f315f69735f6e6f745f73686f7274000000000000000000600082015250565b60006125a16017836122a8565b91506125ac8261256b565b602082019050919050565b600060208201905081810360008301526125d081612594565b9050919050565b7f4f425f66696c6c416d6f756e745f69735f6e6567000000000000000000000000600082015250565b600061260d6014836122a8565b9150612618826125d7565b602082019050919050565b6000602082019050818103600083015261263c81612600565b9050919050565b7f4f425f6f72646572735f646f5f6e6f745f6d6174636800000000000000000000600082015250565b60006126796016836122a8565b915061268482612643565b602082019050919050565b600060208201905081810360008301526126a88161266c565b9050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60006126e982611ac1565b91507f800000000000000000000000000000000000000000000000000000000000000082141561271c5761271b6126af565b5b816000039050919050565b600061273282611aa8565b915061273d83611aa8565b9250817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0483118215151615612776576127756126af565b5b828202905092915050565b600060a0820190506127966000830188611ab2565b6127a36020830187611ab2565b6127b06040830186611ab2565b6127bd6060830185612325565b6127ca6080830184611ab2565b9695505050505050565b7f496e697469616c697a61626c653a20636f6e747261637420697320616c72656160008201527f647920696e697469616c697a6564000000000000000000000000000000000000602082015250565b6000612830602e836122a8565b915061283b826127d4565b604082019050919050565b6000602082019050818103600083015261285f81612823565b9050919050565b6000819050919050565b600060ff82169050919050565b6000819050919050565b60006128a261289d61289884612866565b61287d565b612870565b9050919050565b6128b281612887565b82525050565b60006020820190506128cd60008301846128a9565b92915050565b7f4f425f73656e6465725f69735f6e6f745f747261646572000000000000000000600082015250565b60006129096017836122a8565b9150612914826128d3565b602082019050919050565b60006020820190508181036000830152612938816128fc565b9050919050565b7f4f425f4f726465725f646f65735f6e6f745f6578697374000000000000000000600082015250565b60006129756017836122a8565b91506129808261293f565b602082019050919050565b600060208201905081810360008301526129a481612968565b9050919050565b6129b481611c65565b82525050565b6129c381611ac1565b82525050565b6129d281611ccf565b82525050565b60e0820160008201516129ee6000850182612184565b506020820151612a0160208501826129ab565b506040820151612a1460408501826129ba565b506060820151612a276060850182612184565b506080820151612a3a6080850182612184565b5060a0820151612a4d60a08501826129c9565b5060c0820151612a6060c08501826129c9565b50505050565b600061010082019050612a7c60008301856129d8565b612a8960e0830184611ab2565b9392505050565b6000612a9b82611aa8565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff821415612ace57612acd6126af565b5b600182019050919050565b600061010082019050612aef6000830185612238565b612afc60208301846129d8565b9392505050565b7f4549503731323a20556e696e697469616c697a65640000000000000000000000600082015250565b6000612b396015836122a8565b9150612b4482612b03565b602082019050919050565b60006020820190508181036000830152612b6881612b2c565b9050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b6000612ba982611aa8565b9150612bb483611aa8565b925082612bc457612bc3612b6f565b5b828204905092915050565b6000612bda82611aa8565b9150612be583611aa8565b925082821015612bf857612bf76126af565b5b828203905092915050565b6000612c0e82611ac1565b9150612c1983611ac1565b9250827f800000000000000000000000000000000000000000000000000000000000000001821260008412151615612c5457612c536126af565b5b827f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff018213600084121615612c8c57612c8b6126af565b5b828203905092915050565b600081519050919050565b600082825260208201905092915050565b6000612cbe82612c97565b612cc88185612ca2565b9350612cd88185602086016122b9565b612ce181611b8d565b840191505092915050565b600060c0820190508181036000830152612d068189612cb3565b9050612d156020830188611ab2565b612d226040830187611ab2565b612d2f6060830186611ab2565b612d3c6080830185612325565b612d4960a0830184611ab2565b979650505050505050565b7f4f425f696e76616c69645f6f7264657200000000000000000000000000000000600082015250565b6000612d8a6010836122a8565b9150612d9582612d54565b602082019050919050565b60006020820190508181036000830152612db981612d7d565b9050919050565b6000612dcb82611ac1565b9150612dd683611ac1565b9250827f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0482116000841360008413161615612e1557612e146126af565b5b817f80000000000000000000000000000000000000000000000000000000000000000583126000841260008413161615612e5257612e516126af565b5b827f80000000000000000000000000000000000000000000000000000000000000000582126000841360008412161615612e8f57612e8e6126af565b5b827f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0582126000841260008412161615612ecc57612ecb6126af565b5b828202905092915050565b7f4f425f66696c6c5f616e645f626173655f7369676e5f6e6f745f6d6174636800600082015250565b6000612f0d601f836122a8565b9150612f1882612ed7565b602082019050919050565b60006020820190508181036000830152612f3c81612f00565b9050919050565b7f4f425f696e76616c69645f66696c6c416d6f756e740000000000000000000000600082015250565b6000612f796015836122a8565b9150612f8482612f43565b602082019050919050565b60006020820190508181036000830152612fa881612f6c565b9050919050565b7f4f425f66696c6c65645f616d6f756e745f6869676865725f7468616e5f6f726460008201527f65725f6261736500000000000000000000000000000000000000000000000000602082015250565b600061300b6027836122a8565b915061301682612faf565b604082019050919050565b6000602082019050818103600083015261303a81612ffe565b9050919050565b600061304c82611ac1565b915061305783611ac1565b9250817f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff03831360008312151615613092576130916126af565b5b817f80000000000000000000000000000000000000000000000000000000000000000383126000831216156130ca576130c96126af565b5b828201905092915050565b7f4f425f706c656173655f77686974656c6973745f6e65775f616d6d0000000000600082015250565b600061310b601b836122a8565b9150613116826130d5565b602082019050919050565b6000602082019050818103600083015261313a816130fe565b9050919050565b600061314c82611aa8565b915061315783611aa8565b9250827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0382111561318c5761318b6126af565b5b828201905092915050565b7f53616665436173743a2076616c7565206d75737420626520706f736974697665600082015250565b60006131cd6020836122a8565b91506131d882613197565b602082019050919050565b600060208201905081810360008301526131fc816131c0565b9050919050565b7f496e697469616c697a61626c653a20636f6e7472616374206973206e6f74206960008201527f6e697469616c697a696e67000000000000000000000000000000000000000000602082015250565b600061325f602b836122a8565b915061326a82613203565b604082019050919050565b6000602082019050818103600083015261328e81613252565b9050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b600060028204905060018216806132dc57607f821691505b602082108114156132f0576132ef613295565b5b50919050565b7f53616665436173743a2076616c756520646f65736e27742066697420696e206160008201527f6e20696e74323536000000000000000000000000000000000000000000000000602082015250565b60006133526028836122a8565b915061335d826132f6565b604082019050919050565b6000602082019050818103600083015261338181613345565b9050919050565b600060a08201905061339d6000830188612238565b6133aa6020830187612238565b6133b76040830186612238565b6133c46060830185611ab2565b6133d16080830184612325565b969550505050505056fea26469706673582212200276ef85738145c532230c22fad4f9296689988ccee3dbc3efc448b09151466064736f6c63430008090033"},"0x03000000000000000000000000000000000000b2":{"balance":"0x0","code":"0x608060405234801561001057600080fd5b50600436106100365760003560e01c806326e04f9d1461003b578063468f02d214610059575b600080fd5b610043610077565b604051610050919061010b565b60405180910390f35b61006161007d565b60405161006e91906101ee565b60405180910390f35b600c5481565b6060600167ffffffffffffffff81111561009a57610099610210565b5b6040519080825280602002602001820160405280156100c85781602001602082028036833780820191505090505b50905062989680816000815181106100e3576100e261023f565b5b60200260200101818152505090565b6000819050919050565b610105816100f2565b82525050565b600060208201905061012060008301846100fc565b92915050565b600081519050919050565b600082825260208201905092915050565b6000819050602082019050919050565b6000819050919050565b61016581610152565b82525050565b6000610177838361015c565b60208301905092915050565b6000602082019050919050565b600061019b82610126565b6101a58185610131565b93506101b083610142565b8060005b838110156101e15781516101c8888261016b565b97506101d383610183565b9250506001810190506101b4565b5085935050505092915050565b600060208201905081810360008301526102088184610190565b905092915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fdfea26469706673582212204b2bb44fa7c6b4529a24bfb7bf0eb8f7f52f80902916e5021c3b7e81b638d0f564736f6c63430008090033","storage":{"0x000000000000000000000000000000000000000000000000000000000000000C":"0x01"}}},"nonce":"0x0","timestamp":"0x0","extraData":"0x00","gasLimit":"500000000","difficulty":"0x0","mixHash":"0x0000000000000000000000000000000000000000000000000000000000000000","coinbase":"0x0000000000000000000000000000000000000000","number":"0x0","gasUsed":"0x0","parentHash":"0x0000000000000000000000000000000000000000000000000000000000000000"}` + + orderBookABI, err = abi.FromSolidityJson(orderBookABIStr) + if err != nil { + panic(err) + } + + aliceKey, _ = crypto.HexToECDSA("56289e99c94b6912bfc12adc093c9b51124f0dc54ac7a766b2bc5ccf558d8027") + bobKey, _ = crypto.HexToECDSA("31b571bf6894a248831ff937bb49f7754509fe93bbd2517c9c73c4144c0e97dc") + alice = crypto.PubkeyToAddress(aliceKey.PublicKey) + bob = crypto.PubkeyToAddress(bobKey.PublicKey) +} + +func createPlaceOrderTx(t *testing.T, vm *VM, trader common.Address, privateKey *ecdsa.PrivateKey, size *big.Int, price *big.Int, salt *big.Int) common.Hash { + nonce := vm.txPool.Nonce(trader) + + order := struct { + AmmIndex *big.Int `json:"ammIndex"` + Trader common.Address `json:"trader"` + BaseAssetQuantity *big.Int `json:"baseAssetQuantity"` + Price *big.Int `json:"price"` + Salt *big.Int `json:"salt"` + ReduceOnly bool `json:"reduceOnly"` + PostOnly bool `json:"postOnly"` + }{ + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(0).Mul(size, _1e18), + Price: big.NewInt(0).Mul(price, _1e6), + Salt: salt, + ReduceOnly: false, + PostOnly: false, + } + data, err := orderBookABI.Pack("placeOrder", order) + if err != nil { + t.Fatalf("orderBookABI.Pack failed: %v", err) + } + tx := types.NewTransaction(nonce, orderBookAddress, big.NewInt(0), 8000000, big.NewInt(500000000000), data) + signer := types.NewLondonSigner(vm.chainConfig.ChainID) + signedTx, err := types.SignTx(tx, signer, privateKey) + if err != nil { + t.Fatalf("types.SignTx failed: %v", err) + } + errs := vm.txPool.AddRemotesSync([]*types.Transaction{signedTx}) + for _, err := range errs { + if err != nil { + t.Fatalf("lop.txPool.AddOrderBookTx failed: %v", err) + } + } + return signedTx.Hash() +} + +// A +// / \ +// B C + +// vm1 proposes block A containing order 1 +// block A is accepted by vm1 and vm2 +// vm1 proposes block B containing order 2 +// vm1 and vm2 set preference to block B +// vm2 proposes block C containing order 2 & order 3 +// vm1 and vm2 set preference to block C +// reorg happens when vm1 accepts block C +func TestHubbleLogs(t *testing.T) { + // Create two VMs which will agree on block A and then + // build the two distinct preferred chains above + ctx := context.Background() + issuer1, vm1, _, _ := GenesisVM(t, true, genesisJSON, "{\"pruning-enabled\":true}", "") + issuer2, vm2, _, _ := GenesisVM(t, true, genesisJSON, "{\"pruning-enabled\":true}", "") + + defer func() { + if err := vm1.Shutdown(ctx); err != nil { + t.Fatal(err) + } + + if err := vm2.Shutdown(ctx); err != nil { + t.Fatal(err) + } + }() + + // long and short order + createPlaceOrderTx(t, vm1, alice, aliceKey, big.NewInt(5), big.NewInt(10), big.NewInt(101)) + <-issuer1 + // include alice's long order + blocksA := buildBlockAndSetPreference(t, vm1, vm2) // block A - both vms accept + accept(t, blocksA...) + + createPlaceOrderTx(t, vm1, bob, bobKey, big.NewInt(-5), big.NewInt(10), big.NewInt(102)) + <-issuer1 + // bob's short order + buildBlockAndSetPreference(t, vm1) // block B - vm1 only + + // build block C parallel to block B + createPlaceOrderTx(t, vm2, bob, bobKey, big.NewInt(-5), big.NewInt(10), big.NewInt(102)) // order 2 + createPlaceOrderTx(t, vm2, alice, aliceKey, big.NewInt(5), big.NewInt(11), big.NewInt(104)) // order 3 + <-issuer2 + vm2BlockC := buildBlockAndSetPreference(t, vm2)[0] // block C - vm2 only for now + + vm1BlockC := parseBlock(t, vm1, vm2BlockC) + setPreference(t, vm1BlockC, vm1) + accept(t, vm1BlockC) // reorg happens here + accept(t, vm2BlockC) + + time.Sleep(time.Second) + + // time.Sleep(2 * time.Second) + detail1 := vm1.limitOrderProcesser.GetOrderBookAPI().GetDetailedOrderBookData(context.Background()) + detail2 := vm2.limitOrderProcesser.GetOrderBookAPI().GetDetailedOrderBookData(context.Background()) + t.Logf("VM1 Orders: %+v", detail1) + t.Logf("VM2 Orders: %+v", detail2) + + // verify that order 1, 2, 3 are in both VMs + if order := filterOrderMapBySalt(detail1.Orders, big.NewInt(101)); order == nil { + t.Fatalf("Order 1 is not in VM1") + } + if order := filterOrderMapBySalt(detail2.Orders, big.NewInt(101)); order == nil { + t.Fatalf("Order 1 is not in VM2") + } + if order := filterOrderMapBySalt(detail1.Orders, big.NewInt(102)); order == nil { + t.Fatalf("Order 2 is not in VM1") + } + if order := filterOrderMapBySalt(detail2.Orders, big.NewInt(102)); order == nil { + t.Fatalf("Order 2 is not in VM2") + } + if order := filterOrderMapBySalt(detail1.Orders, big.NewInt(104)); order == nil { + t.Fatalf("Order 3 is not in VM1") + } + if order := filterOrderMapBySalt(detail2.Orders, big.NewInt(104)); order == nil { + t.Fatalf("Order 3 is not in VM2") + } +} + +func buildBlockAndSetPreference(t *testing.T, vms ...*VM) []snowman.Block { + if len(vms) == 0 { + t.Fatal("No VMs provided") + } + response := []snowman.Block{} + vm1 := vms[0] + vm1Blk, err := vm1.BuildBlock(context.Background()) + if err != nil { + t.Fatal(err) + } + + if err := vm1Blk.Verify(context.Background()); err != nil { + t.Fatal(err) + } + + if status := vm1Blk.Status(); status != choices.Processing { + t.Fatalf("Expected status of built block to be %s, but found %s", choices.Processing, status) + } + + if err := vm1.SetPreference(context.Background(), vm1Blk.ID()); err != nil { + t.Fatal(err) + } + + response = append(response, vm1Blk) + + for _, vm := range vms[1:] { + + vm2Blk, err := vm.ParseBlock(context.Background(), vm1Blk.Bytes()) + if err != nil { + t.Fatalf("Unexpected error parsing block from vm2: %s", err) + } + if err := vm2Blk.Verify(context.Background()); err != nil { + t.Fatalf("Block failed verification on VM2: %s", err) + } + if status := vm2Blk.Status(); status != choices.Processing { + t.Fatalf("Expected status of block on VM2 to be %s, but found %s", choices.Processing, status) + } + if err := vm.SetPreference(context.Background(), vm2Blk.ID()); err != nil { + t.Fatal(err) + } + response = append(response, vm2Blk) + } + + return response +} + +func buildBlock(t *testing.T, vm *VM) snowman.Block { + vmBlk, err := vm.BuildBlock(context.Background()) + if err != nil { + t.Fatal(err) + } + + if err := vmBlk.Verify(context.Background()); err != nil { + t.Fatal(err) + } + + if status := vmBlk.Status(); status != choices.Processing { + t.Fatalf("Expected status of built block to be %s, but found %s", choices.Processing, status) + } + + return vmBlk +} + +func parseBlock(t *testing.T, vm *VM, block snowman.Block) snowman.Block { + newBlock, err := vm.ParseBlock(context.Background(), block.Bytes()) + if err != nil { + t.Fatalf("Unexpected error parsing block from vm: %s", err) + } + if err := newBlock.Verify(context.Background()); err != nil { + t.Fatal(err) + } + + if status := newBlock.Status(); status != choices.Processing { + t.Fatalf("Expected status of built block to be %s, but found %s", choices.Processing, status) + } + + return newBlock +} + +func setPreference(t *testing.T, block snowman.Block, vms ...*VM) { + for _, vm := range vms { + if err := vm.SetPreference(context.Background(), block.ID()); err != nil { + t.Fatal(err) + } + } +} + +func accept(t *testing.T, blocks ...snowman.Block) { + for _, block := range blocks { + if err := block.Accept(context.Background()); err != nil { + t.Fatalf("VM failed to accept block: %s", err) + } + } +} + +func filterOrderMapBySalt(orderMap map[common.Hash]*orderbook.Order, salt *big.Int) *orderbook.Order { + for _, order := range orderMap { + if order.Salt.Cmp(salt) == 0 { + return order + } + } + return nil +} diff --git a/plugin/evm/vm.go b/plugin/evm/vm.go index 37c4aa764e..b17d94be3e 100644 --- a/plugin/evm/vm.go +++ b/plugin/evm/vm.go @@ -37,7 +37,7 @@ import ( "github.com/ava-labs/subnet-evm/params" "github.com/ava-labs/subnet-evm/peer" "github.com/ava-labs/subnet-evm/plugin/evm/message" - + "github.com/ava-labs/subnet-evm/plugin/evm/orderbook" "github.com/ava-labs/subnet-evm/rpc" statesyncclient "github.com/ava-labs/subnet-evm/sync/client" "github.com/ava-labs/subnet-evm/sync/client/stats" @@ -136,6 +136,7 @@ var ( acceptedPrefix = []byte("snowman_accepted") metadataPrefix = []byte("metadata") warpPrefix = []byte("warp") + hubbleDBPrefix = []byte("hubble") ethDBPrefix = []byte("ethdb") ) @@ -169,6 +170,14 @@ var legacyApiNames = map[string]string{ "private-debug": "debug", } +// metrics +var ( + buildBlockCalledCounter = metrics.NewRegisteredCounter("vm/buildblock/called", nil) + buildBlockSuccessCounter = metrics.NewRegisteredCounter("vm/buildblock/success", nil) + buildBlockFailureCounter = metrics.NewRegisteredCounter("vm/buildblock/failure", nil) + buildBlockTimeHistogram = metrics.NewRegisteredHistogram("vm/buildblock/time", nil, metrics.ResettingSample(metrics.NewExpDecaySample(1028, 0.015))) +) + // VM implements the snowman.ChainVM interface type VM struct { ctx *snow.Context @@ -208,12 +217,20 @@ type VM struct { // set to a prefixDB with the prefix [warpPrefix] warpDB database.Database + // [hubbleDB] is used to store orderbook related data + // set to a prefixDB with the prefix [hubbleDBPrefix] + hubbleDB database.Database + toEngine chan<- commonEng.Message syntacticBlockValidator BlockValidator builder *blockBuilder + limitOrderProcesser LimitOrderProcesser + + orderGossiper OrderGossiper + clock mockable.Clock shutdownChan chan struct{} @@ -307,6 +324,7 @@ func (vm *VM) Initialize( // that warp signatures are committed to the database atomically with // the last accepted block. vm.warpDB = prefixdb.New(warpPrefix, db) + vm.hubbleDB = prefixdb.New(hubbleDBPrefix, vm.db) if vm.config.InspectDatabase { start := time.Now() @@ -550,6 +568,9 @@ func (vm *VM) initializeChain(lastAcceptedHash common.Hash, ethConfig ethconfig. vm.blockChain = vm.eth.BlockChain() vm.miner = vm.eth.Miner() + vm.limitOrderProcesser = vm.NewLimitOrderProcesser() + tempMatcher := orderbook.NewTempMatcher(vm.limitOrderProcesser.GetMemoryDB(), vm.limitOrderProcesser.GetLimitOrderTxProcessor()) + vm.eth.SetOrderbookChecker(tempMatcher) vm.eth.Start() return vm.initChainState(vm.blockChain.LastAcceptedBlock()) } @@ -718,6 +739,7 @@ func (vm *VM) initBlockBuilding() error { // NOTE: gossip network must be initialized first otherwise ETH tx gossip will not work. gossipStats := NewGossipStats() + vm.orderGossiper = vm.createOrderGossiper(gossipStats) vm.builder = vm.NewBlockBuilder(vm.toEngine) vm.builder.awaitSubmittedTxs() vm.Network.SetGossipHandler(NewGossipHandler(vm, gossipStats)) @@ -765,7 +787,7 @@ func (vm *VM) initBlockBuilding() error { gossip.Every(ctx, vm.ctx.Log, vm.ethTxPullGossiper, vm.config.PullGossipFrequency.Duration) vm.shutdownWg.Done() }() - + vm.limitOrderProcesser.ListenAndProcessTransactions(vm.builder) return nil } @@ -818,20 +840,52 @@ func (vm *VM) buildBlock(ctx context.Context) (snowman.Block, error) { return vm.buildBlockWithContext(ctx, nil) } -func (vm *VM) buildBlockWithContext(ctx context.Context, proposerVMBlockCtx *block.Context) (snowman.Block, error) { +func (vm *VM) buildBlockWithContext(ctx context.Context, proposerVMBlockCtx *block.Context) (blk_ snowman.Block, err error) { + defer func(start time.Time) { + buildBlockCalledCounter.Inc(1) + if err != nil { + buildBlockFailureCounter.Inc(1) + } else { + buildBlockSuccessCounter.Inc(1) + } + + buildBlockTimeHistogram.Update(time.Since(start).Microseconds()) + log.Info("buildBlock complete", "duration", time.Since(start)) + }(time.Now()) + if proposerVMBlockCtx != nil { - log.Debug("Building block with context", "pChainBlockHeight", proposerVMBlockCtx.PChainHeight) + log.Info("Building block with context", "pChainBlockHeight", proposerVMBlockCtx.PChainHeight) } else { - log.Debug("Building block without context") + log.Info("Building block without context") } predicateCtx := &precompileconfig.PredicateContext{ SnowCtx: vm.ctx, ProposerVMBlockCtx: proposerVMBlockCtx, } + currentHeadBlock := vm.blockChain.CurrentBlock() + orderbookTxsBlockNumber := vm.txPool.GetOrderBookTxsBlockNumber() + if orderbookTxsBlockNumber > 0 && currentHeadBlock.Number.Uint64() >= orderbookTxsBlockNumber { + // the orderbooks txs in mempool could be outdated cuz the + // current head block is ahead of the orderbook txs block(the block at which matching pipeline evaluated the txs) + // it's possible that another validator has already included the orderbook txs in the current head block + + log.Warn("buildBlock - current head block is ahead of OrderBookTxsBlockNumber", "orderbookTxsBlockNumber", orderbookTxsBlockNumber, "currentHeadBlockNumber", currentHeadBlock.Number.Uint64()) + vm.txPool.PurgeOrderBookTxs() + + // don't return now, attempt to generate a block without the matching orderbook txs + } + block, err := vm.miner.GenerateBlock(predicateCtx) vm.builder.handleGenerateBlock() if err != nil { + if vm.txPool.GetOrderBookTxsCount() > 0 && strings.Contains(err.Error(), "BLOCK_GAS_TOO_LOW") { + // orderbook txs from the validator were part of the block that failed to be generated because of low block gas + orderbook.BuildBlockFailedWithLowBlockGasCounter.Inc(1) + log.Error("buildBlock - GenerateBlock failed with low gas cost", "err", err, "orderbookTxsCount", vm.txPool.GetOrderBookTxsCount()) + } else { + log.Error("buildBlock - GenerateBlock failed", "err", err) + } return nil, err } @@ -850,7 +904,7 @@ func (vm *VM) buildBlockWithContext(ctx context.Context, proposerVMBlockCtx *blo // We call verify without writes here to avoid generating a reference // to the blk state root in the triedb when we are going to call verify // again from the consensus engine with writes enabled. - if err := blk.verify(predicateCtx, false /*=writes*/); err != nil { + if err = blk.verify(predicateCtx, false /*=writes*/); err != nil { return nil, fmt.Errorf("block failed verification due to: %w", err) } @@ -977,6 +1031,25 @@ func (vm *VM) CreateHandlers(context.Context) (map[string]http.Handler, error) { } enabledAPIs = append(enabledAPIs, "snowman") } + if err := handler.RegisterName("order", NewOrderAPI(vm.limitOrderProcesser.GetTradingAPI(), vm)); err != nil { + return nil, err + } + orderbook.MakerbookDatabaseFile = vm.config.MakerbookDatabasePath + + if err := handler.RegisterName("orderbook", vm.limitOrderProcesser.GetOrderBookAPI()); err != nil { + return nil, err + } + + if vm.config.TradingAPIEnabled { + if err := handler.RegisterName("trading", vm.limitOrderProcesser.GetTradingAPI()); err != nil { + return nil, err + } + } + if vm.config.TestingApiEnabled { + if err := handler.RegisterName("testing", vm.limitOrderProcesser.GetTestingAPI()); err != nil { + return nil, err + } + } if vm.config.WarpAPIEnabled { validatorsState := warpValidators.NewState(vm.ctx) @@ -1115,3 +1188,36 @@ func attachEthService(handler *rpc.Server, apis []rpc.API, names []string) error return nil } + +func (vm *VM) NewLimitOrderProcesser() LimitOrderProcesser { + var validatorPrivateKey string + var err error + if vm.config.IsValidator { + validatorPrivateKey, err = loadPrivateKeyFromFile(vm.config.ValidatorPrivateKeyFile) + if err != nil { + panic(fmt.Sprint("please specify correct path for validator-private-key-file in chain.json ", err)) + } + if validatorPrivateKey == "" { + panic("validator private key is empty") + } + } + return NewLimitOrderProcesser( + vm.ctx, + vm.txPool, + vm.shutdownChan, + &vm.shutdownWg, + vm.eth.APIBackend, + vm.blockChain, + vm.hubbleDB, + validatorPrivateKey, + vm.config, + ) +} + +func loadPrivateKeyFromFile(keyFile string) (string, error) { + key, err := os.ReadFile(keyFile) + if err != nil { + return "", err + } + return strings.TrimSuffix(string(key), "\n"), nil +} diff --git a/plugin/evm/vm_test.go b/plugin/evm/vm_test.go index ea96cc7689..adc10a7cd8 100644 --- a/plugin/evm/vm_test.go +++ b/plugin/evm/vm_test.go @@ -209,6 +209,7 @@ func GenesisVM(t *testing.T, appSender := &commonEng.SenderTest{T: t} appSender.CantSendAppGossip = true appSender.SendAppGossipF = func(context.Context, []byte, int, int, int) error { return nil } + createValidatorPrivateKeyIfNotExists() err := vm.Initialize( context.Background(), ctx, @@ -487,6 +488,7 @@ func TestBuildEthTxBlock(t *testing.T) { restartedVM := &VM{} genesisBytes := buildGenesisTest(t, genesisJSONSubnetEVM) + createValidatorPrivateKeyIfNotExists() if err := restartedVM.Initialize( context.Background(), NewContext(), @@ -1995,6 +1997,7 @@ func TestConfigureLogLevel(t *testing.T) { appSender := &commonEng.SenderTest{T: t} appSender.CantSendAppGossip = true appSender.SendAppGossipF = func(context.Context, []byte, int, int, int) error { return nil } + createValidatorPrivateKeyIfNotExists() err := vm.Initialize( context.Background(), ctx, @@ -3095,6 +3098,7 @@ func TestSkipChainConfigCheckCompatible(t *testing.T) { require.NoError(t, err) // this will not be allowed + createValidatorPrivateKeyIfNotExists() err = reinitVM.Initialize(context.Background(), vm.ctx, dbManager, genesisWithUpgradeBytes, []byte{}, []byte{}, issuer, []*commonEng.Fx{}, appSender) require.ErrorContains(t, err, "mismatching SubnetEVM fork block timestamp in database") @@ -3270,3 +3274,23 @@ func TestCrossChainMessagestoVM(t *testing.T) { require.NoError(err) require.True(calledSendCrossChainAppResponseFn, "sendCrossChainAppResponseFn was not called") } + +func TestVMOrderGossiperIsSet(t *testing.T) { + _, vm, _, _ := GenesisVM(t, true, "", "", "") + require.NotNil(t, vm.orderGossiper, "legacy gossiper should be initialized") + require.NoError(t, vm.Shutdown(context.Background())) +} + +func createValidatorPrivateKeyIfNotExists() { + // Create a new validator private key file + defaultValidatorPrivateKeyFile = "/tmp/validator.pk" + fileContent, _ := os.ReadFile(defaultValidatorPrivateKeyFile) + text := string(fileContent) + + key := "31b571bf6894a248831ff937bb49f7754509fe93bbd2517c9c73c4144c0e97dc" + if text != key { + fmt.Println("file does not exists") + privateKey := []byte(key) + os.WriteFile(defaultValidatorPrivateKeyFile, privateKey, 0644) + } +} diff --git a/plugin/evm/vm_upgrade_bytes_test.go b/plugin/evm/vm_upgrade_bytes_test.go index 01c709ebb3..6f19398525 100644 --- a/plugin/evm/vm_upgrade_bytes_test.go +++ b/plugin/evm/vm_upgrade_bytes_test.go @@ -95,6 +95,7 @@ func TestVMUpgradeBytesPrecompile(t *testing.T) { defer func() { metrics.Enabled = true }() + createValidatorPrivateKeyIfNotExists() if err := vm.Initialize( context.Background(), vm.ctx, dbManager, []byte(genesisJSONSubnetEVM), upgradeBytesJSON, []byte{}, issuer, []*commonEng.Fx{}, appSender, ); err != nil { diff --git a/precompile/contracts/bibliophile/amm.go b/precompile/contracts/bibliophile/amm.go new file mode 100644 index 0000000000..744c33c668 --- /dev/null +++ b/precompile/contracts/bibliophile/amm.go @@ -0,0 +1,168 @@ +package bibliophile + +import ( + "math/big" + + hu "github.com/ava-labs/subnet-evm/plugin/evm/orderbook/hubbleutils" + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" +) + +const ( + VAR_POSITIONS_SLOT int64 = 1 + VAR_CUMULATIVE_PREMIUM_FRACTION int64 = 2 + MAX_ORACLE_SPREAD_RATIO_SLOT int64 = 3 + MAX_LIQUIDATION_RATIO_SLOT int64 = 4 + MIN_SIZE_REQUIREMENT_SLOT int64 = 5 + UNDERLYING_ASSET_SLOT int64 = 6 + MAX_LIQUIDATION_PRICE_SPREAD int64 = 11 + MULTIPLIER_SLOT int64 = 12 + IMPACT_MARGIN_NOTIONAL_SLOT int64 = 19 + LAST_TRADE_PRICE_SLOT int64 = 20 + BIDS_SLOT int64 = 21 + ASKS_SLOT int64 = 22 + BIDS_HEAD_SLOT int64 = 23 + ASKS_HEAD_SLOT int64 = 24 + SETTLEMENT_PRICE_SLOT int64 = 28 +) + +// AMM State +func getBidsHead(stateDB contract.StateDB, market common.Address) *big.Int { + return stateDB.GetState(market, common.BigToHash(big.NewInt(BIDS_HEAD_SLOT))).Big() +} + +func getAsksHead(stateDB contract.StateDB, market common.Address) *big.Int { + return stateDB.GetState(market, common.BigToHash(big.NewInt(ASKS_HEAD_SLOT))).Big() +} + +func getLastPrice(stateDB contract.StateDB, market common.Address) *big.Int { + return stateDB.GetState(market, common.BigToHash(big.NewInt(LAST_TRADE_PRICE_SLOT))).Big() +} + +func GetCumulativePremiumFraction(stateDB contract.StateDB, market common.Address) *big.Int { + return fromTwosComplement(stateDB.GetState(market, common.BigToHash(big.NewInt(VAR_CUMULATIVE_PREMIUM_FRACTION))).Bytes()) +} + +// GetMaxOraclePriceSpread returns the maxOracleSpreadRatio for a given market +func GetMaxOraclePriceSpread(stateDB contract.StateDB, marketID int64) *big.Int { + return getMaxOraclePriceSpread(stateDB, GetMarketAddressFromMarketID(marketID, stateDB)) +} + +func getMaxOraclePriceSpread(stateDB contract.StateDB, market common.Address) *big.Int { + return fromTwosComplement(stateDB.GetState(market, common.BigToHash(big.NewInt(MAX_ORACLE_SPREAD_RATIO_SLOT))).Bytes()) +} + +// GetMaxLiquidationPriceSpread returns the maxOracleSpreadRatio for a given market +func GetMaxLiquidationPriceSpread(stateDB contract.StateDB, marketID int64) *big.Int { + return getMaxLiquidationPriceSpread(stateDB, GetMarketAddressFromMarketID(marketID, stateDB)) +} + +func getMaxLiquidationPriceSpread(stateDB contract.StateDB, market common.Address) *big.Int { + return fromTwosComplement(stateDB.GetState(market, common.BigToHash(big.NewInt(MAX_LIQUIDATION_PRICE_SPREAD))).Bytes()) +} + +// GetMaxLiquidationRatio returns the maxLiquidationPriceSpread for a given market +func GetMaxLiquidationRatio(stateDB contract.StateDB, marketID int64) *big.Int { + return getMaxLiquidationRatio(stateDB, GetMarketAddressFromMarketID(marketID, stateDB)) +} + +func getMaxLiquidationRatio(stateDB contract.StateDB, market common.Address) *big.Int { + return fromTwosComplement(stateDB.GetState(market, common.BigToHash(big.NewInt(MAX_LIQUIDATION_RATIO_SLOT))).Bytes()) +} + +// GetMinSizeRequirement returns the minSizeRequirement for a given market +func GetMinSizeRequirement(stateDB contract.StateDB, marketID int64) *big.Int { + market := GetMarketAddressFromMarketID(marketID, stateDB) + return fromTwosComplement(stateDB.GetState(market, common.BigToHash(big.NewInt(MIN_SIZE_REQUIREMENT_SLOT))).Bytes()) +} + +func getMultiplier(stateDB contract.StateDB, market common.Address) *big.Int { + return stateDB.GetState(market, common.BigToHash(big.NewInt(MULTIPLIER_SLOT))).Big() +} + +func GetMultiplier(stateDB contract.StateDB, marketID int64) *big.Int { + return getMultiplier(stateDB, GetMarketAddressFromMarketID(marketID, stateDB)) +} + +func getUnderlyingAssetAddress(stateDB contract.StateDB, market common.Address) common.Address { + return common.BytesToAddress(stateDB.GetState(market, common.BigToHash(big.NewInt(UNDERLYING_ASSET_SLOT))).Bytes()) +} + +func getUnderlyingPriceForMarket(stateDB contract.StateDB, marketID int64) *big.Int { + market := GetMarketAddressFromMarketID(marketID, stateDB) + return getUnderlyingPrice(stateDB, market) +} + +func getSettlementPrice(stateDB contract.StateDB, market common.Address) *big.Int { + return stateDB.GetState(market, common.BigToHash(big.NewInt(SETTLEMENT_PRICE_SLOT))).Big() +} + +// Trader State + +func positionsStorageSlot(trader *common.Address) *big.Int { + return new(big.Int).SetBytes(crypto.Keccak256(append(common.LeftPadBytes(trader.Bytes(), 32), common.LeftPadBytes(big.NewInt(VAR_POSITIONS_SLOT).Bytes(), 32)...))) +} + +func getSize(stateDB contract.StateDB, market common.Address, trader *common.Address) *big.Int { + return fromTwosComplement(stateDB.GetState(market, common.BigToHash(positionsStorageSlot(trader))).Bytes()) +} + +func getOpenNotional(stateDB contract.StateDB, market common.Address, trader *common.Address) *big.Int { + return stateDB.GetState(market, common.BigToHash(new(big.Int).Add(positionsStorageSlot(trader), big.NewInt(1)))).Big() +} + +func GetLastPremiumFraction(stateDB contract.StateDB, market common.Address, trader *common.Address) *big.Int { + return fromTwosComplement(stateDB.GetState(market, common.BigToHash(new(big.Int).Add(positionsStorageSlot(trader), big.NewInt(2)))).Bytes()) +} + +func bidsStorageSlot(price *big.Int) common.Hash { + return common.BytesToHash(crypto.Keccak256(append(common.LeftPadBytes(price.Bytes(), 32), common.LeftPadBytes(big.NewInt(BIDS_SLOT).Bytes(), 32)...))) +} + +func asksStorageSlot(price *big.Int) common.Hash { + return common.BytesToHash(crypto.Keccak256(append(common.LeftPadBytes(price.Bytes(), 32), common.LeftPadBytes(big.NewInt(ASKS_SLOT).Bytes(), 32)...))) +} + +func getBidSize(stateDB contract.StateDB, market common.Address, price *big.Int) *big.Int { + return stateDB.GetState(market, common.BigToHash(new(big.Int).Add(bidsStorageSlot(price).Big(), big.NewInt(1)))).Big() +} + +func getAskSize(stateDB contract.StateDB, market common.Address, price *big.Int) *big.Int { + return stateDB.GetState(market, common.BigToHash(new(big.Int).Add(asksStorageSlot(price).Big(), big.NewInt(1)))).Big() +} + +func getNextBid(stateDB contract.StateDB, market common.Address, price *big.Int) *big.Int { + return stateDB.GetState(market, bidsStorageSlot(price)).Big() +} + +func getNextAsk(stateDB contract.StateDB, market common.Address, price *big.Int) *big.Int { + return stateDB.GetState(market, asksStorageSlot(price)).Big() +} + +func GetImpactMarginNotional(stateDB contract.StateDB, market common.Address) *big.Int { + return stateDB.GetState(market, common.BigToHash(big.NewInt(IMPACT_MARGIN_NOTIONAL_SLOT))).Big() +} + +func getPosition(stateDB contract.StateDB, market common.Address, trader *common.Address) *hu.Position { + return &hu.Position{ + Size: getSize(stateDB, market, trader), + OpenNotional: getOpenNotional(stateDB, market, trader), + } +} + +// Utils + +func getPendingFundingPayment(stateDB contract.StateDB, market common.Address, trader *common.Address) *big.Int { + cumulativePremiumFraction := GetCumulativePremiumFraction(stateDB, market) + return hu.Div1e18(new(big.Int).Mul(new(big.Int).Sub(cumulativePremiumFraction, GetLastPremiumFraction(stateDB, market, trader)), getSize(stateDB, market, trader))) +} + +// Common Utils +func fromTwosComplement(b []byte) *big.Int { + t := new(big.Int).SetBytes(b) + if b[0]&0x80 != 0 { + t.Sub(t, new(big.Int).Lsh(big.NewInt(1), uint(len(b)*8))) + } + return t +} diff --git a/precompile/contracts/bibliophile/api.go b/precompile/contracts/bibliophile/api.go new file mode 100644 index 0000000000..313a7b18da --- /dev/null +++ b/precompile/contracts/bibliophile/api.go @@ -0,0 +1,216 @@ +package bibliophile + +import ( + "math/big" + + hu "github.com/ava-labs/subnet-evm/plugin/evm/orderbook/hubbleutils" + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ethereum/go-ethereum/common" +) + +type VariablesReadFromClearingHouseSlots struct { + MaintenanceMargin *big.Int `json:"maintenance_margin"` + MinAllowableMargin *big.Int `json:"min_allowable_margin"` + TakerFee *big.Int `json:"taker_fee"` + Amms []common.Address `json:"amms"` + ActiveMarketsCount int64 `json:"active_markets_count"` + NotionalPosition *big.Int `json:"notional_position"` + Margin *big.Int `json:"margin"` + TotalFunding *big.Int `json:"total_funding"` + UnderlyingPrices []*big.Int `json:"underlying_prices"` + PositionSizes []*big.Int `json:"position_sizes"` +} + +func GetClearingHouseVariables(stateDB contract.StateDB, trader common.Address) VariablesReadFromClearingHouseSlots { + maintenanceMargin := GetMaintenanceMargin(stateDB) + minAllowableMargin := GetMinAllowableMargin(stateDB) + takerFee := GetTakerFee(stateDB) + amms := GetMarkets(stateDB) + activeMarketsCount := GetActiveMarketsCount(stateDB) + notionalPositionAndMargin := getNotionalPositionAndMargin(stateDB, &GetNotionalPositionAndMarginInput{ + Trader: trader, + IncludeFundingPayments: false, + Mode: 0, + }, hu.V2) + totalFunding := GetTotalFunding(stateDB, &trader) + positionSizes := getPosSizes(stateDB, &trader) + underlyingPrices := GetUnderlyingPrices(stateDB) + + return VariablesReadFromClearingHouseSlots{ + MaintenanceMargin: maintenanceMargin, + MinAllowableMargin: minAllowableMargin, + TakerFee: takerFee, + Amms: amms, + ActiveMarketsCount: activeMarketsCount, + NotionalPosition: notionalPositionAndMargin.NotionalPosition, + Margin: notionalPositionAndMargin.Margin, + TotalFunding: totalFunding, + PositionSizes: positionSizes, + UnderlyingPrices: underlyingPrices, + } +} + +type VariablesReadFromMarginAccountSlots struct { + Margin *big.Int `json:"margin"` + NormalizedMargin *big.Int `json:"normalized_margin"` + ReservedMargin *big.Int `json:"reserved_margin"` +} + +func GetMarginAccountVariables(stateDB contract.StateDB, collateralIdx *big.Int, trader common.Address) VariablesReadFromMarginAccountSlots { + margin := getMargin(stateDB, collateralIdx, trader) + normalizedMargin := GetNormalizedMargin(stateDB, trader) + reservedMargin := getReservedMargin(stateDB, trader) + return VariablesReadFromMarginAccountSlots{ + Margin: margin, + NormalizedMargin: normalizedMargin, + ReservedMargin: reservedMargin, + } +} + +type VariablesReadFromAMMSlots struct { + // positions, cumulativePremiumFraction, maxOracleSpreadRatio, maxLiquidationRatio, minSizeRequirement, oracle, underlyingAsset, + // maxLiquidationPriceSpread, redStoneAdapter, redStoneFeedId, impactMarginNotional, lastTradePrice, bids, asks, bidsHead, asksHead + LastPrice *big.Int `json:"last_price"` + CumulativePremiumFraction *big.Int `json:"cumulative_premium_fraction"` + MaxOracleSpreadRatio *big.Int `json:"max_oracle_spread_ratio"` + OracleAddress common.Address `json:"oracle_address"` + MaxLiquidationRatio *big.Int `json:"max_liquidation_ratio"` + MinSizeRequirement *big.Int `json:"min_size_requirement"` + UnderlyingAssetAddress common.Address `json:"underlying_asset_address"` + UnderlyingPriceForMarket *big.Int `json:"underlying_price_for_market"` + UnderlyingPrice *big.Int `json:"underlying_price"` + MaxLiquidationPriceSpread *big.Int `json:"max_liquidation_price_spread"` + RedStoneAdapterAddress common.Address `json:"red_stone_adapter_address"` + RedStoneFeedId common.Hash `json:"red_stone_feed_id"` + ImpactMarginNotional *big.Int `json:"impact_margin_notional"` + Position Position `json:"position"` + BidsHead *big.Int `json:"bids_head"` + BidsHeadSize *big.Int `json:"bids_head_size"` + AsksHead *big.Int `json:"asks_head"` + AsksHeadSize *big.Int `json:"asks_head_size"` + UpperBound *big.Int `json:"upper_bound"` + LowerBound *big.Int `json:"lower_bound"` + MinAllowableMargin *big.Int `json:"min_allowable_margin"` + TakerFee *big.Int `json:"taker_fee"` + TotalMargin *big.Int `json:"total_margin"` + AvailableMargin *big.Int `json:"available_margin"` + ReduceOnlyAmount *big.Int `json:"reduce_only_amount"` + LongOpenOrders *big.Int `json:"long_open_orders"` + ShortOpenOrders *big.Int `json:"short_open_orders"` +} + +type Position struct { + Size *big.Int `json:"size"` + OpenNotional *big.Int `json:"open_notional"` + LastPremiumFraction *big.Int `json:"last_premium_fraction"` + LiquidationThreshold *big.Int `json:"liquidation_threshold"` +} + +func GetAMMVariables(stateDB contract.StateDB, ammAddress common.Address, ammIndex int64, trader common.Address) VariablesReadFromAMMSlots { + lastPrice := getLastPrice(stateDB, ammAddress) + position := Position{ + Size: getSize(stateDB, ammAddress, &trader), + OpenNotional: getOpenNotional(stateDB, ammAddress, &trader), + LastPremiumFraction: GetLastPremiumFraction(stateDB, ammAddress, &trader), + } + cumulativePremiumFraction := GetCumulativePremiumFraction(stateDB, ammAddress) + maxOracleSpreadRatio := GetMaxOraclePriceSpread(stateDB, ammIndex) + maxLiquidationRatio := GetMaxLiquidationRatio(stateDB, ammIndex) + maxLiquidationPriceSpread := GetMaxLiquidationPriceSpread(stateDB, ammIndex) + minSizeRequirement := GetMinSizeRequirement(stateDB, ammIndex) + oracleAddress := getOracleAddress(stateDB) + underlyingAssetAddress := getUnderlyingAssetAddress(stateDB, ammAddress) + underlyingPriceForMarket := getUnderlyingPriceForMarket(stateDB, ammIndex) + underlyingPrice := getUnderlyingPrice(stateDB, ammAddress) + redStoneAdapterAddress := getRedStoneAdapterAddress(stateDB, oracleAddress) + redStoneFeedId := getRedStoneFeedId(stateDB, oracleAddress, underlyingAssetAddress) + bidsHead := getBidsHead(stateDB, ammAddress) + bidsHeadSize := getBidSize(stateDB, ammAddress, bidsHead) + asksHead := getAsksHead(stateDB, ammAddress) + asksHeadSize := getAskSize(stateDB, ammAddress, asksHead) + upperBound, lowerBound := GetAcceptableBoundsForLiquidation(stateDB, ammIndex) + minAllowableMargin := GetMinAllowableMargin(stateDB) + takerFee := GetTakerFee(stateDB) + totalMargin := GetNormalizedMargin(stateDB, trader) + availableMargin := GetAvailableMargin(stateDB, trader, hu.V2) + reduceOnlyAmount := getReduceOnlyAmount(stateDB, trader, big.NewInt(ammIndex)) + longOpenOrdersAmount := getLongOpenOrdersAmount(stateDB, trader, big.NewInt(ammIndex)) + shortOpenOrdersAmount := getShortOpenOrdersAmount(stateDB, trader, big.NewInt(ammIndex)) + impactMarginNotional := GetImpactMarginNotional(stateDB, ammAddress) + return VariablesReadFromAMMSlots{ + LastPrice: lastPrice, + CumulativePremiumFraction: cumulativePremiumFraction, + MaxOracleSpreadRatio: maxOracleSpreadRatio, + OracleAddress: oracleAddress, + MaxLiquidationRatio: maxLiquidationRatio, + MinSizeRequirement: minSizeRequirement, + UnderlyingAssetAddress: underlyingAssetAddress, + UnderlyingPriceForMarket: underlyingPriceForMarket, + UnderlyingPrice: underlyingPrice, + MaxLiquidationPriceSpread: maxLiquidationPriceSpread, + RedStoneAdapterAddress: redStoneAdapterAddress, + RedStoneFeedId: redStoneFeedId, + ImpactMarginNotional: impactMarginNotional, + Position: position, + BidsHead: bidsHead, + BidsHeadSize: bidsHeadSize, + AsksHead: asksHead, + AsksHeadSize: asksHeadSize, + UpperBound: upperBound, + LowerBound: lowerBound, + MinAllowableMargin: minAllowableMargin, + TotalMargin: totalMargin, + AvailableMargin: availableMargin, + TakerFee: takerFee, + ReduceOnlyAmount: reduceOnlyAmount, + LongOpenOrders: longOpenOrdersAmount, + ShortOpenOrders: shortOpenOrdersAmount, + } +} + +type VariablesReadFromIOCOrdersSlots struct { + OrderDetails OrderDetails `json:"order_details"` + IocExpirationCap *big.Int `json:"ioc_expiration_cap"` +} + +type OrderDetails struct { + BlockPlaced *big.Int `json:"block_placed"` + FilledAmount *big.Int `json:"filled_amount"` + OrderStatus int64 `json:"order_status"` +} + +func GetIOCOrdersVariables(stateDB contract.StateDB, orderHash common.Hash) VariablesReadFromIOCOrdersSlots { + blockPlaced := iocGetBlockPlaced(stateDB, orderHash) + filledAmount := iocGetOrderFilledAmount(stateDB, orderHash) + orderStatus := IOCGetOrderStatus(stateDB, orderHash) + + iocExpirationCap := iocGetExpirationCap(stateDB) + return VariablesReadFromIOCOrdersSlots{ + OrderDetails: OrderDetails{ + BlockPlaced: blockPlaced, + FilledAmount: filledAmount, + OrderStatus: orderStatus, + }, + IocExpirationCap: iocExpirationCap, + } +} + +type VariablesReadFromOrderbookSlots struct { + OrderDetails OrderDetails `json:"order_details"` + IsTradingAuthoriy bool `json:"is_trading_authority"` +} + +func GetOrderBookVariables(stateDB contract.StateDB, traderAddress string, senderAddress string, orderHash common.Hash) VariablesReadFromOrderbookSlots { + blockPlaced := getBlockPlaced(stateDB, orderHash) + filledAmount := getOrderFilledAmount(stateDB, orderHash) + orderStatus := GetOrderStatus(stateDB, orderHash) + isTradingAuthoriy := IsTradingAuthority(stateDB, common.HexToAddress(traderAddress), common.HexToAddress(senderAddress)) + return VariablesReadFromOrderbookSlots{ + OrderDetails: OrderDetails{ + BlockPlaced: blockPlaced, + FilledAmount: filledAmount, + OrderStatus: orderStatus, + }, + IsTradingAuthoriy: isTradingAuthoriy, + } +} diff --git a/precompile/contracts/bibliophile/clearing_house.go b/precompile/contracts/bibliophile/clearing_house.go new file mode 100644 index 0000000000..5465c38fc9 --- /dev/null +++ b/precompile/contracts/bibliophile/clearing_house.go @@ -0,0 +1,207 @@ +package bibliophile + +import ( + "math/big" + + hu "github.com/ava-labs/subnet-evm/plugin/evm/orderbook/hubbleutils" + "github.com/ava-labs/subnet-evm/precompile/contract" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" +) + +const ( + CLEARING_HOUSE_GENESIS_ADDRESS = "0x03000000000000000000000000000000000000b2" + MAINTENANCE_MARGIN_SLOT int64 = 1 + MIN_ALLOWABLE_MARGIN_SLOT int64 = 2 + TAKER_FEE_SLOT int64 = 3 + AMMS_SLOT int64 = 12 + REFERRAL_SLOT int64 = 13 + SETTLED_ALL_SLOT int64 = 19 +) + +type MarginMode uint8 + +const ( + Maintenance_Margin MarginMode = iota + Min_Allowable_Margin +) + +func GetMarginMode(marginMode uint8) MarginMode { + if marginMode == 0 { + return Maintenance_Margin + } + return Min_Allowable_Margin +} + +func marketsStorageSlot() *big.Int { + return new(big.Int).SetBytes(crypto.Keccak256(common.LeftPadBytes(big.NewInt(AMMS_SLOT).Bytes(), 32))) +} + +func GetActiveMarketsCount(stateDB contract.StateDB) int64 { + if IsSettledAll(stateDB) { + return 0 + } + return GetMarketsCountRaw(stateDB) +} + +func GetMarketsCountRaw(stateDB contract.StateDB) int64 { + rawVal := stateDB.GetState(common.HexToAddress(CLEARING_HOUSE_GENESIS_ADDRESS), common.BytesToHash(common.LeftPadBytes(big.NewInt(AMMS_SLOT).Bytes(), 32))) + return new(big.Int).SetBytes(rawVal.Bytes()).Int64() +} + +func IsSettledAll(stateDB contract.StateDB) bool { + return stateDB.GetState(common.HexToAddress(CLEARING_HOUSE_GENESIS_ADDRESS), common.BigToHash(big.NewInt(SETTLED_ALL_SLOT))).Big().Sign() == 1 +} + +func GetMarkets(stateDB contract.StateDB) []common.Address { + numMarkets := GetActiveMarketsCount(stateDB) + markets := make([]common.Address, numMarkets) + baseStorageSlot := marketsStorageSlot() + for i := int64(0); i < numMarkets; i++ { + amm := stateDB.GetState(common.HexToAddress(CLEARING_HOUSE_GENESIS_ADDRESS), common.BigToHash(new(big.Int).Add(baseStorageSlot, big.NewInt(i)))) + markets[i] = common.BytesToAddress(amm.Bytes()) + } + return markets +} + +func GetMarketsIncludingSettled(stateDB contract.StateDB) []common.Address { + numMarkets := GetMarketsCountRaw(stateDB) + markets := make([]common.Address, numMarkets) + baseStorageSlot := marketsStorageSlot() + for i := int64(0); i < numMarkets; i++ { + amm := stateDB.GetState(common.HexToAddress(CLEARING_HOUSE_GENESIS_ADDRESS), common.BigToHash(new(big.Int).Add(baseStorageSlot, big.NewInt(i)))) + markets[i] = common.BytesToAddress(amm.Bytes()) + } + return markets +} + +type GetNotionalPositionAndMarginInput struct { + Trader common.Address + IncludeFundingPayments bool + Mode uint8 +} + +type GetNotionalPositionAndMarginOutput struct { + NotionalPosition *big.Int + Margin *big.Int +} + +func getNotionalPositionAndMargin(stateDB contract.StateDB, input *GetNotionalPositionAndMarginInput, upgradeVersion hu.UpgradeVersion) GetNotionalPositionAndMarginOutput { + markets := GetMarketsIncludingSettled(stateDB) + numMarkets := len(markets) + positions := make(map[int]*hu.Position, numMarkets) + underlyingPrices := make(map[int]*big.Int, numMarkets) + midPrices := make(map[int]*big.Int, numMarkets) + settlementPrices := make(map[int]*big.Int, numMarkets) + var activeMarketIds []int + for i, market := range markets { + positions[i] = getPosition(stateDB, GetMarketAddressFromMarketID(int64(i), stateDB), &input.Trader) + underlyingPrices[i] = getUnderlyingPrice(stateDB, market) + midPrices[i] = getMidPrice(stateDB, market) + settlementPrices[i] = getSettlementPrice(stateDB, market) + if settlementPrices[i] == nil || settlementPrices[i].Sign() == 0 { + activeMarketIds = append(activeMarketIds, i) + } + } + pendingFunding := big.NewInt(0) + if input.IncludeFundingPayments { + pendingFunding = GetTotalFunding(stateDB, &input.Trader) + } + notionalPosition, margin := hu.GetNotionalPositionAndMargin( + &hu.HubbleState{ + Assets: GetCollaterals(stateDB), + OraclePrices: underlyingPrices, + MidPrices: midPrices, + SettlementPrices: settlementPrices, + ActiveMarkets: activeMarketIds, + UpgradeVersion: upgradeVersion, + }, + &hu.UserState{ + Positions: positions, + Margins: getMargins(stateDB, input.Trader), + PendingFunding: pendingFunding, + }, + input.Mode, + ) + return GetNotionalPositionAndMarginOutput{ + NotionalPosition: notionalPosition, + Margin: margin, + } +} + +func GetTotalFunding(stateDB contract.StateDB, trader *common.Address) *big.Int { + totalFunding := big.NewInt(0) + for _, market := range GetMarketsIncludingSettled(stateDB) { + totalFunding.Add(totalFunding, getPendingFundingPayment(stateDB, market, trader)) + } + return totalFunding +} + +// GetMaintenanceMargin returns the maintenance margin for a trader +func GetMaintenanceMargin(stateDB contract.StateDB) *big.Int { + return new(big.Int).SetBytes(stateDB.GetState(common.HexToAddress(CLEARING_HOUSE_GENESIS_ADDRESS), common.BytesToHash(common.LeftPadBytes(big.NewInt(MAINTENANCE_MARGIN_SLOT).Bytes(), 32))).Bytes()) +} + +// GetMinAllowableMargin returns the minimum allowable margin for a trader +func GetMinAllowableMargin(stateDB contract.StateDB) *big.Int { + return new(big.Int).SetBytes(stateDB.GetState(common.HexToAddress(CLEARING_HOUSE_GENESIS_ADDRESS), common.BytesToHash(common.LeftPadBytes(big.NewInt(MIN_ALLOWABLE_MARGIN_SLOT).Bytes(), 32))).Bytes()) +} + +// GetTakerFee returns the taker fee for a trader +func GetTakerFee(stateDB contract.StateDB) *big.Int { + return fromTwosComplement(stateDB.GetState(common.HexToAddress(CLEARING_HOUSE_GENESIS_ADDRESS), common.BigToHash(big.NewInt(TAKER_FEE_SLOT))).Bytes()) +} + +func GetUnderlyingPrices(stateDB contract.StateDB) []*big.Int { + underlyingPrices := make([]*big.Int, 0) + for _, market := range GetMarkets(stateDB) { + underlyingPrices = append(underlyingPrices, getUnderlyingPrice(stateDB, market)) + } + return underlyingPrices +} + +func GetMidPrices(stateDB contract.StateDB) []*big.Int { + underlyingPrices := make([]*big.Int, 0) + for _, market := range GetMarkets(stateDB) { + underlyingPrices = append(underlyingPrices, getMidPrice(stateDB, market)) + } + return underlyingPrices +} + +func GetSettlementPrices(stateDB contract.StateDB) []*big.Int { + underlyingPrices := make([]*big.Int, 0) + for _, market := range GetMarketsIncludingSettled(stateDB) { + underlyingPrices = append(underlyingPrices, getSettlementPrice(stateDB, market)) + } + return underlyingPrices +} + +func GetReduceOnlyAmounts(stateDB contract.StateDB, trader common.Address) []*big.Int { + numMarkets := GetActiveMarketsCount(stateDB) + sizes := make([]*big.Int, numMarkets) + for i := int64(0); i < numMarkets; i++ { + sizes[i] = getReduceOnlyAmount(stateDB, trader, big.NewInt(i)) + } + return sizes +} + +func getPosSizes(stateDB contract.StateDB, trader *common.Address) []*big.Int { + positionSizes := make([]*big.Int, 0) + for _, market := range GetMarketsIncludingSettled(stateDB) { + positionSizes = append(positionSizes, getSize(stateDB, market, trader)) + } + return positionSizes +} + +// GetMarketAddressFromMarketID returns the market address for a given marketID +func GetMarketAddressFromMarketID(marketID int64, stateDB contract.StateDB) common.Address { + baseStorageSlot := marketsStorageSlot() + amm := stateDB.GetState(common.HexToAddress(CLEARING_HOUSE_GENESIS_ADDRESS), common.BigToHash(new(big.Int).Add(baseStorageSlot, big.NewInt(marketID)))) + return common.BytesToAddress(amm.Bytes()) +} + +func getReferralAddress(stateDB contract.StateDB) common.Address { + referral := stateDB.GetState(common.HexToAddress(CLEARING_HOUSE_GENESIS_ADDRESS), common.BigToHash(big.NewInt(REFERRAL_SLOT))) + return common.BytesToAddress(referral.Bytes()) +} diff --git a/precompile/contracts/bibliophile/client.go b/precompile/contracts/bibliophile/client.go new file mode 100644 index 0000000000..688c815e00 --- /dev/null +++ b/precompile/contracts/bibliophile/client.go @@ -0,0 +1,217 @@ +package bibliophile + +import ( + "math/big" + + hu "github.com/ava-labs/subnet-evm/plugin/evm/orderbook/hubbleutils" + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ethereum/go-ethereum/common" +) + +type BibliophileClient interface { + //margin account + GetAvailableMargin(trader common.Address, upgradeVersion hu.UpgradeVersion) *big.Int + //clearing house + GetMarketAddressFromMarketID(marketId int64) common.Address + GetMinAllowableMargin() *big.Int + GetTakerFee() *big.Int + //orderbook + GetSize(market common.Address, trader *common.Address) *big.Int + GetLongOpenOrdersAmount(trader common.Address, ammIndex *big.Int) *big.Int + GetShortOpenOrdersAmount(trader common.Address, ammIndex *big.Int) *big.Int + GetReduceOnlyAmount(trader common.Address, ammIndex *big.Int) *big.Int + IsTradingAuthority(trader, senderOrSigner common.Address) bool + IsValidator(senderOrSigner common.Address) bool + + // Limit Order + GetBlockPlaced(orderHash [32]byte) *big.Int + GetOrderFilledAmount(orderHash [32]byte) *big.Int + GetOrderStatus(orderHash [32]byte) int64 + + // IOC Order + IOC_GetBlockPlaced(orderHash [32]byte) *big.Int + IOC_GetOrderFilledAmount(orderHash [32]byte) *big.Int + IOC_GetOrderStatus(orderHash [32]byte) int64 + IOC_GetExpirationCap() *big.Int + + // Signed Order + GetSignedOrderFilledAmount(orderHash [32]byte) *big.Int + GetSignedOrderStatus(orderHash [32]byte) int64 + + // AMM + GetMinSizeRequirement(marketId int64) *big.Int + GetLastPrice(ammAddress common.Address) *big.Int + GetBidSize(ammAddress common.Address, price *big.Int) *big.Int + GetAskSize(ammAddress common.Address, price *big.Int) *big.Int + GetNextBidPrice(ammAddress common.Address, price *big.Int) *big.Int + GetNextAskPrice(ammAddress common.Address, price *big.Int) *big.Int + GetImpactMarginNotional(ammAddress common.Address) *big.Int + GetBidsHead(market common.Address) *big.Int + GetAsksHead(market common.Address) *big.Int + GetPriceMultiplier(market common.Address) *big.Int + GetUpperAndLowerBoundForMarket(marketId int64) (*big.Int, *big.Int) + GetAcceptableBoundsForLiquidation(marketId int64) (*big.Int, *big.Int) + + GetTimeStamp() uint64 + GetNotionalPositionAndMargin(trader common.Address, includeFundingPayments bool, mode uint8, upgradeVersion hu.UpgradeVersion) (*big.Int, *big.Int) + HasReferrer(trader common.Address) bool + GetActiveMarketsCount() int64 + + GetAccessibleState() contract.AccessibleState +} + +// Define a structure that will implement the Bibliophile interface +type bibliophileClient struct { + accessibleState contract.AccessibleState +} + +func NewBibliophileClient(accessibleState contract.AccessibleState) BibliophileClient { + return &bibliophileClient{ + accessibleState: accessibleState, + } +} + +func (b *bibliophileClient) GetAccessibleState() contract.AccessibleState { + return b.accessibleState +} + +func (b *bibliophileClient) GetSignedOrderFilledAmount(orderHash [32]byte) *big.Int { + return GetSignedOrderFilledAmount(b.accessibleState.GetStateDB(), orderHash) +} + +func (b *bibliophileClient) GetSignedOrderStatus(orderHash [32]byte) int64 { + return GetSignedOrderStatus(b.accessibleState.GetStateDB(), orderHash) +} + +func (b *bibliophileClient) GetActiveMarketsCount() int64 { + return GetActiveMarketsCount(b.accessibleState.GetStateDB()) +} + +func (b *bibliophileClient) GetTimeStamp() uint64 { + return b.accessibleState.GetBlockContext().Timestamp() +} + +func (b *bibliophileClient) GetSize(market common.Address, trader *common.Address) *big.Int { + return getSize(b.accessibleState.GetStateDB(), market, trader) +} + +func (b *bibliophileClient) GetMinSizeRequirement(marketId int64) *big.Int { + return GetMinSizeRequirement(b.accessibleState.GetStateDB(), marketId) +} + +func (b *bibliophileClient) GetMinAllowableMargin() *big.Int { + return GetMinAllowableMargin(b.accessibleState.GetStateDB()) +} + +func (b *bibliophileClient) GetTakerFee() *big.Int { + return GetTakerFee(b.accessibleState.GetStateDB()) +} + +func (b *bibliophileClient) GetMarketAddressFromMarketID(marketID int64) common.Address { + return GetMarketAddressFromMarketID(marketID, b.accessibleState.GetStateDB()) +} + +func (b *bibliophileClient) GetBlockPlaced(orderHash [32]byte) *big.Int { + return getBlockPlaced(b.accessibleState.GetStateDB(), orderHash) +} + +func (b *bibliophileClient) GetOrderFilledAmount(orderHash [32]byte) *big.Int { + return getOrderFilledAmount(b.accessibleState.GetStateDB(), orderHash) +} + +func (b *bibliophileClient) GetOrderStatus(orderHash [32]byte) int64 { + return GetOrderStatus(b.accessibleState.GetStateDB(), orderHash) +} + +func (b *bibliophileClient) IOC_GetBlockPlaced(orderHash [32]byte) *big.Int { + return iocGetBlockPlaced(b.accessibleState.GetStateDB(), orderHash) +} + +func (b *bibliophileClient) IOC_GetOrderFilledAmount(orderHash [32]byte) *big.Int { + return iocGetOrderFilledAmount(b.accessibleState.GetStateDB(), orderHash) +} + +func (b *bibliophileClient) IOC_GetOrderStatus(orderHash [32]byte) int64 { + return IOCGetOrderStatus(b.accessibleState.GetStateDB(), orderHash) +} + +func (b *bibliophileClient) IsTradingAuthority(trader, senderOrSigner common.Address) bool { + return IsTradingAuthority(b.accessibleState.GetStateDB(), trader, senderOrSigner) +} + +func (b *bibliophileClient) IsValidator(senderOrSigner common.Address) bool { + return IsValidator(b.accessibleState.GetStateDB(), senderOrSigner) +} + +func (b *bibliophileClient) IOC_GetExpirationCap() *big.Int { + return iocGetExpirationCap(b.accessibleState.GetStateDB()) +} + +func (b *bibliophileClient) GetLastPrice(ammAddress common.Address) *big.Int { + return getLastPrice(b.accessibleState.GetStateDB(), ammAddress) +} + +func (b *bibliophileClient) GetBidSize(ammAddress common.Address, price *big.Int) *big.Int { + return getBidSize(b.accessibleState.GetStateDB(), ammAddress, price) +} + +func (b *bibliophileClient) GetAskSize(ammAddress common.Address, price *big.Int) *big.Int { + return getAskSize(b.accessibleState.GetStateDB(), ammAddress, price) +} + +func (b *bibliophileClient) GetNextBidPrice(ammAddress common.Address, price *big.Int) *big.Int { + return getNextBid(b.accessibleState.GetStateDB(), ammAddress, price) +} + +func (b *bibliophileClient) GetNextAskPrice(ammAddress common.Address, price *big.Int) *big.Int { + return getNextAsk(b.accessibleState.GetStateDB(), ammAddress, price) +} + +func (b *bibliophileClient) GetImpactMarginNotional(ammAddress common.Address) *big.Int { + return GetImpactMarginNotional(b.accessibleState.GetStateDB(), ammAddress) +} + +func (b *bibliophileClient) GetUpperAndLowerBoundForMarket(marketId int64) (*big.Int, *big.Int) { + return GetAcceptableBounds(b.accessibleState.GetStateDB(), marketId) +} + +func (b *bibliophileClient) GetAcceptableBoundsForLiquidation(marketId int64) (*big.Int, *big.Int) { + return GetAcceptableBoundsForLiquidation(b.accessibleState.GetStateDB(), marketId) +} + +func (b *bibliophileClient) GetBidsHead(market common.Address) *big.Int { + return getBidsHead(b.accessibleState.GetStateDB(), market) +} + +func (b *bibliophileClient) GetAsksHead(market common.Address) *big.Int { + return getAsksHead(b.accessibleState.GetStateDB(), market) +} + +func (b *bibliophileClient) GetPriceMultiplier(market common.Address) *big.Int { + return getMultiplier(b.accessibleState.GetStateDB(), market) +} + +func (b *bibliophileClient) GetLongOpenOrdersAmount(trader common.Address, ammIndex *big.Int) *big.Int { + return getLongOpenOrdersAmount(b.accessibleState.GetStateDB(), trader, ammIndex) +} + +func (b *bibliophileClient) GetShortOpenOrdersAmount(trader common.Address, ammIndex *big.Int) *big.Int { + return getShortOpenOrdersAmount(b.accessibleState.GetStateDB(), trader, ammIndex) +} + +func (b *bibliophileClient) GetReduceOnlyAmount(trader common.Address, ammIndex *big.Int) *big.Int { + return getReduceOnlyAmount(b.accessibleState.GetStateDB(), trader, ammIndex) +} + +func (b *bibliophileClient) GetAvailableMargin(trader common.Address, upgradeVersion hu.UpgradeVersion) *big.Int { + return GetAvailableMargin(b.accessibleState.GetStateDB(), trader, upgradeVersion) +} + +func (b *bibliophileClient) GetNotionalPositionAndMargin(trader common.Address, includeFundingPayments bool, mode uint8, upgradeVersion hu.UpgradeVersion) (*big.Int, *big.Int) { + output := getNotionalPositionAndMargin(b.accessibleState.GetStateDB(), &GetNotionalPositionAndMarginInput{Trader: trader, IncludeFundingPayments: includeFundingPayments, Mode: mode}, upgradeVersion) + return output.NotionalPosition, output.Margin +} + +func (b *bibliophileClient) HasReferrer(trader common.Address) bool { + return HasReferrer(b.accessibleState.GetStateDB(), trader) +} diff --git a/precompile/contracts/bibliophile/client_mock.go b/precompile/contracts/bibliophile/client_mock.go new file mode 100644 index 0000000000..c48907e920 --- /dev/null +++ b/precompile/contracts/bibliophile/client_mock.go @@ -0,0 +1,545 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: client.go + +// Package mock_bibliophile is a generated GoMock package. +package bibliophile + +import ( + big "math/big" + reflect "reflect" + + hubbleutils "github.com/ava-labs/subnet-evm/plugin/evm/orderbook/hubbleutils" + contract "github.com/ava-labs/subnet-evm/precompile/contract" + common "github.com/ethereum/go-ethereum/common" + gomock "github.com/golang/mock/gomock" +) + +// MockBibliophileClient is a mock of BibliophileClient interface. +type MockBibliophileClient struct { + ctrl *gomock.Controller + recorder *MockBibliophileClientMockRecorder +} + +// MockBibliophileClientMockRecorder is the mock recorder for MockBibliophileClient. +type MockBibliophileClientMockRecorder struct { + mock *MockBibliophileClient +} + +// NewMockBibliophileClient creates a new mock instance. +func NewMockBibliophileClient(ctrl *gomock.Controller) *MockBibliophileClient { + mock := &MockBibliophileClient{ctrl: ctrl} + mock.recorder = &MockBibliophileClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockBibliophileClient) EXPECT() *MockBibliophileClientMockRecorder { + return m.recorder +} + +// GetAcceptableBoundsForLiquidation mocks base method. +func (m *MockBibliophileClient) GetAcceptableBoundsForLiquidation(marketId int64) (*big.Int, *big.Int) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAcceptableBoundsForLiquidation", marketId) + ret0, _ := ret[0].(*big.Int) + ret1, _ := ret[1].(*big.Int) + return ret0, ret1 +} + +// GetAcceptableBoundsForLiquidation indicates an expected call of GetAcceptableBoundsForLiquidation. +func (mr *MockBibliophileClientMockRecorder) GetAcceptableBoundsForLiquidation(marketId interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAcceptableBoundsForLiquidation", reflect.TypeOf((*MockBibliophileClient)(nil).GetAcceptableBoundsForLiquidation), marketId) +} + +// GetAccessibleState mocks base method. +func (m *MockBibliophileClient) GetAccessibleState() contract.AccessibleState { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAccessibleState") + ret0, _ := ret[0].(contract.AccessibleState) + return ret0 +} + +// GetAccessibleState indicates an expected call of GetAccessibleState. +func (mr *MockBibliophileClientMockRecorder) GetAccessibleState() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccessibleState", reflect.TypeOf((*MockBibliophileClient)(nil).GetAccessibleState)) +} + +// GetActiveMarketsCount mocks base method. +func (m *MockBibliophileClient) GetActiveMarketsCount() int64 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetActiveMarketsCount") + ret0, _ := ret[0].(int64) + return ret0 +} + +// GetActiveMarketsCount indicates an expected call of GetActiveMarketsCount. +func (mr *MockBibliophileClientMockRecorder) GetActiveMarketsCount() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActiveMarketsCount", reflect.TypeOf((*MockBibliophileClient)(nil).GetActiveMarketsCount)) +} + +// GetAskSize mocks base method. +func (m *MockBibliophileClient) GetAskSize(ammAddress common.Address, price *big.Int) *big.Int { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAskSize", ammAddress, price) + ret0, _ := ret[0].(*big.Int) + return ret0 +} + +// GetAskSize indicates an expected call of GetAskSize. +func (mr *MockBibliophileClientMockRecorder) GetAskSize(ammAddress, price interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAskSize", reflect.TypeOf((*MockBibliophileClient)(nil).GetAskSize), ammAddress, price) +} + +// GetAsksHead mocks base method. +func (m *MockBibliophileClient) GetAsksHead(market common.Address) *big.Int { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAsksHead", market) + ret0, _ := ret[0].(*big.Int) + return ret0 +} + +// GetAsksHead indicates an expected call of GetAsksHead. +func (mr *MockBibliophileClientMockRecorder) GetAsksHead(market interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAsksHead", reflect.TypeOf((*MockBibliophileClient)(nil).GetAsksHead), market) +} + +// GetAvailableMargin mocks base method. +func (m *MockBibliophileClient) GetAvailableMargin(trader common.Address, upgradeVersion hubbleutils.UpgradeVersion) *big.Int { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAvailableMargin", trader, upgradeVersion) + ret0, _ := ret[0].(*big.Int) + return ret0 +} + +// GetAvailableMargin indicates an expected call of GetAvailableMargin. +func (mr *MockBibliophileClientMockRecorder) GetAvailableMargin(trader, upgradeVersion interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAvailableMargin", reflect.TypeOf((*MockBibliophileClient)(nil).GetAvailableMargin), trader, upgradeVersion) +} + +// GetBidSize mocks base method. +func (m *MockBibliophileClient) GetBidSize(ammAddress common.Address, price *big.Int) *big.Int { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBidSize", ammAddress, price) + ret0, _ := ret[0].(*big.Int) + return ret0 +} + +// GetBidSize indicates an expected call of GetBidSize. +func (mr *MockBibliophileClientMockRecorder) GetBidSize(ammAddress, price interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBidSize", reflect.TypeOf((*MockBibliophileClient)(nil).GetBidSize), ammAddress, price) +} + +// GetBidsHead mocks base method. +func (m *MockBibliophileClient) GetBidsHead(market common.Address) *big.Int { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBidsHead", market) + ret0, _ := ret[0].(*big.Int) + return ret0 +} + +// GetBidsHead indicates an expected call of GetBidsHead. +func (mr *MockBibliophileClientMockRecorder) GetBidsHead(market interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBidsHead", reflect.TypeOf((*MockBibliophileClient)(nil).GetBidsHead), market) +} + +// GetBlockPlaced mocks base method. +func (m *MockBibliophileClient) GetBlockPlaced(orderHash [32]byte) *big.Int { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBlockPlaced", orderHash) + ret0, _ := ret[0].(*big.Int) + return ret0 +} + +// GetBlockPlaced indicates an expected call of GetBlockPlaced. +func (mr *MockBibliophileClientMockRecorder) GetBlockPlaced(orderHash interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlockPlaced", reflect.TypeOf((*MockBibliophileClient)(nil).GetBlockPlaced), orderHash) +} + +// GetImpactMarginNotional mocks base method. +func (m *MockBibliophileClient) GetImpactMarginNotional(ammAddress common.Address) *big.Int { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetImpactMarginNotional", ammAddress) + ret0, _ := ret[0].(*big.Int) + return ret0 +} + +// GetImpactMarginNotional indicates an expected call of GetImpactMarginNotional. +func (mr *MockBibliophileClientMockRecorder) GetImpactMarginNotional(ammAddress interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetImpactMarginNotional", reflect.TypeOf((*MockBibliophileClient)(nil).GetImpactMarginNotional), ammAddress) +} + +// GetLastPrice mocks base method. +func (m *MockBibliophileClient) GetLastPrice(ammAddress common.Address) *big.Int { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetLastPrice", ammAddress) + ret0, _ := ret[0].(*big.Int) + return ret0 +} + +// GetLastPrice indicates an expected call of GetLastPrice. +func (mr *MockBibliophileClientMockRecorder) GetLastPrice(ammAddress interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLastPrice", reflect.TypeOf((*MockBibliophileClient)(nil).GetLastPrice), ammAddress) +} + +// GetLongOpenOrdersAmount mocks base method. +func (m *MockBibliophileClient) GetLongOpenOrdersAmount(trader common.Address, ammIndex *big.Int) *big.Int { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetLongOpenOrdersAmount", trader, ammIndex) + ret0, _ := ret[0].(*big.Int) + return ret0 +} + +// GetLongOpenOrdersAmount indicates an expected call of GetLongOpenOrdersAmount. +func (mr *MockBibliophileClientMockRecorder) GetLongOpenOrdersAmount(trader, ammIndex interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLongOpenOrdersAmount", reflect.TypeOf((*MockBibliophileClient)(nil).GetLongOpenOrdersAmount), trader, ammIndex) +} + +// GetMarketAddressFromMarketID mocks base method. +func (m *MockBibliophileClient) GetMarketAddressFromMarketID(marketId int64) common.Address { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetMarketAddressFromMarketID", marketId) + ret0, _ := ret[0].(common.Address) + return ret0 +} + +// GetMarketAddressFromMarketID indicates an expected call of GetMarketAddressFromMarketID. +func (mr *MockBibliophileClientMockRecorder) GetMarketAddressFromMarketID(marketId interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMarketAddressFromMarketID", reflect.TypeOf((*MockBibliophileClient)(nil).GetMarketAddressFromMarketID), marketId) +} + +// GetMinAllowableMargin mocks base method. +func (m *MockBibliophileClient) GetMinAllowableMargin() *big.Int { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetMinAllowableMargin") + ret0, _ := ret[0].(*big.Int) + return ret0 +} + +// GetMinAllowableMargin indicates an expected call of GetMinAllowableMargin. +func (mr *MockBibliophileClientMockRecorder) GetMinAllowableMargin() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMinAllowableMargin", reflect.TypeOf((*MockBibliophileClient)(nil).GetMinAllowableMargin)) +} + +// GetMinSizeRequirement mocks base method. +func (m *MockBibliophileClient) GetMinSizeRequirement(marketId int64) *big.Int { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetMinSizeRequirement", marketId) + ret0, _ := ret[0].(*big.Int) + return ret0 +} + +// GetMinSizeRequirement indicates an expected call of GetMinSizeRequirement. +func (mr *MockBibliophileClientMockRecorder) GetMinSizeRequirement(marketId interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMinSizeRequirement", reflect.TypeOf((*MockBibliophileClient)(nil).GetMinSizeRequirement), marketId) +} + +// GetNextAskPrice mocks base method. +func (m *MockBibliophileClient) GetNextAskPrice(ammAddress common.Address, price *big.Int) *big.Int { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetNextAskPrice", ammAddress, price) + ret0, _ := ret[0].(*big.Int) + return ret0 +} + +// GetNextAskPrice indicates an expected call of GetNextAskPrice. +func (mr *MockBibliophileClientMockRecorder) GetNextAskPrice(ammAddress, price interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNextAskPrice", reflect.TypeOf((*MockBibliophileClient)(nil).GetNextAskPrice), ammAddress, price) +} + +// GetNextBidPrice mocks base method. +func (m *MockBibliophileClient) GetNextBidPrice(ammAddress common.Address, price *big.Int) *big.Int { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetNextBidPrice", ammAddress, price) + ret0, _ := ret[0].(*big.Int) + return ret0 +} + +// GetNextBidPrice indicates an expected call of GetNextBidPrice. +func (mr *MockBibliophileClientMockRecorder) GetNextBidPrice(ammAddress, price interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNextBidPrice", reflect.TypeOf((*MockBibliophileClient)(nil).GetNextBidPrice), ammAddress, price) +} + +// GetNotionalPositionAndMargin mocks base method. +func (m *MockBibliophileClient) GetNotionalPositionAndMargin(trader common.Address, includeFundingPayments bool, mode uint8, upgradeVersion hubbleutils.UpgradeVersion) (*big.Int, *big.Int) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetNotionalPositionAndMargin", trader, includeFundingPayments, mode, upgradeVersion) + ret0, _ := ret[0].(*big.Int) + ret1, _ := ret[1].(*big.Int) + return ret0, ret1 +} + +// GetNotionalPositionAndMargin indicates an expected call of GetNotionalPositionAndMargin. +func (mr *MockBibliophileClientMockRecorder) GetNotionalPositionAndMargin(trader, includeFundingPayments, mode, upgradeVersion interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotionalPositionAndMargin", reflect.TypeOf((*MockBibliophileClient)(nil).GetNotionalPositionAndMargin), trader, includeFundingPayments, mode, upgradeVersion) +} + +// GetOrderFilledAmount mocks base method. +func (m *MockBibliophileClient) GetOrderFilledAmount(orderHash [32]byte) *big.Int { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetOrderFilledAmount", orderHash) + ret0, _ := ret[0].(*big.Int) + return ret0 +} + +// GetOrderFilledAmount indicates an expected call of GetOrderFilledAmount. +func (mr *MockBibliophileClientMockRecorder) GetOrderFilledAmount(orderHash interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOrderFilledAmount", reflect.TypeOf((*MockBibliophileClient)(nil).GetOrderFilledAmount), orderHash) +} + +// GetOrderStatus mocks base method. +func (m *MockBibliophileClient) GetOrderStatus(orderHash [32]byte) int64 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetOrderStatus", orderHash) + ret0, _ := ret[0].(int64) + return ret0 +} + +// GetOrderStatus indicates an expected call of GetOrderStatus. +func (mr *MockBibliophileClientMockRecorder) GetOrderStatus(orderHash interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOrderStatus", reflect.TypeOf((*MockBibliophileClient)(nil).GetOrderStatus), orderHash) +} + +// GetPriceMultiplier mocks base method. +func (m *MockBibliophileClient) GetPriceMultiplier(market common.Address) *big.Int { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPriceMultiplier", market) + ret0, _ := ret[0].(*big.Int) + return ret0 +} + +// GetPriceMultiplier indicates an expected call of GetPriceMultiplier. +func (mr *MockBibliophileClientMockRecorder) GetPriceMultiplier(market interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPriceMultiplier", reflect.TypeOf((*MockBibliophileClient)(nil).GetPriceMultiplier), market) +} + +// GetReduceOnlyAmount mocks base method. +func (m *MockBibliophileClient) GetReduceOnlyAmount(trader common.Address, ammIndex *big.Int) *big.Int { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetReduceOnlyAmount", trader, ammIndex) + ret0, _ := ret[0].(*big.Int) + return ret0 +} + +// GetReduceOnlyAmount indicates an expected call of GetReduceOnlyAmount. +func (mr *MockBibliophileClientMockRecorder) GetReduceOnlyAmount(trader, ammIndex interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetReduceOnlyAmount", reflect.TypeOf((*MockBibliophileClient)(nil).GetReduceOnlyAmount), trader, ammIndex) +} + +// GetShortOpenOrdersAmount mocks base method. +func (m *MockBibliophileClient) GetShortOpenOrdersAmount(trader common.Address, ammIndex *big.Int) *big.Int { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetShortOpenOrdersAmount", trader, ammIndex) + ret0, _ := ret[0].(*big.Int) + return ret0 +} + +// GetShortOpenOrdersAmount indicates an expected call of GetShortOpenOrdersAmount. +func (mr *MockBibliophileClientMockRecorder) GetShortOpenOrdersAmount(trader, ammIndex interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetShortOpenOrdersAmount", reflect.TypeOf((*MockBibliophileClient)(nil).GetShortOpenOrdersAmount), trader, ammIndex) +} + +// GetSignedOrderFilledAmount mocks base method. +func (m *MockBibliophileClient) GetSignedOrderFilledAmount(orderHash [32]byte) *big.Int { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSignedOrderFilledAmount", orderHash) + ret0, _ := ret[0].(*big.Int) + return ret0 +} + +// GetSignedOrderFilledAmount indicates an expected call of GetSignedOrderFilledAmount. +func (mr *MockBibliophileClientMockRecorder) GetSignedOrderFilledAmount(orderHash interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSignedOrderFilledAmount", reflect.TypeOf((*MockBibliophileClient)(nil).GetSignedOrderFilledAmount), orderHash) +} + +// GetSignedOrderStatus mocks base method. +func (m *MockBibliophileClient) GetSignedOrderStatus(orderHash [32]byte) int64 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSignedOrderStatus", orderHash) + ret0, _ := ret[0].(int64) + return ret0 +} + +// GetSignedOrderStatus indicates an expected call of GetSignedOrderStatus. +func (mr *MockBibliophileClientMockRecorder) GetSignedOrderStatus(orderHash interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSignedOrderStatus", reflect.TypeOf((*MockBibliophileClient)(nil).GetSignedOrderStatus), orderHash) +} + +// GetSize mocks base method. +func (m *MockBibliophileClient) GetSize(market common.Address, trader *common.Address) *big.Int { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSize", market, trader) + ret0, _ := ret[0].(*big.Int) + return ret0 +} + +// GetSize indicates an expected call of GetSize. +func (mr *MockBibliophileClientMockRecorder) GetSize(market, trader interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSize", reflect.TypeOf((*MockBibliophileClient)(nil).GetSize), market, trader) +} + +// GetTakerFee mocks base method. +func (m *MockBibliophileClient) GetTakerFee() *big.Int { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTakerFee") + ret0, _ := ret[0].(*big.Int) + return ret0 +} + +// GetTakerFee indicates an expected call of GetTakerFee. +func (mr *MockBibliophileClientMockRecorder) GetTakerFee() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTakerFee", reflect.TypeOf((*MockBibliophileClient)(nil).GetTakerFee)) +} + +// GetTimeStamp mocks base method. +func (m *MockBibliophileClient) GetTimeStamp() uint64 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTimeStamp") + ret0, _ := ret[0].(uint64) + return ret0 +} + +// GetTimeStamp indicates an expected call of GetTimeStamp. +func (mr *MockBibliophileClientMockRecorder) GetTimeStamp() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTimeStamp", reflect.TypeOf((*MockBibliophileClient)(nil).GetTimeStamp)) +} + +// GetUpperAndLowerBoundForMarket mocks base method. +func (m *MockBibliophileClient) GetUpperAndLowerBoundForMarket(marketId int64) (*big.Int, *big.Int) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUpperAndLowerBoundForMarket", marketId) + ret0, _ := ret[0].(*big.Int) + ret1, _ := ret[1].(*big.Int) + return ret0, ret1 +} + +// GetUpperAndLowerBoundForMarket indicates an expected call of GetUpperAndLowerBoundForMarket. +func (mr *MockBibliophileClientMockRecorder) GetUpperAndLowerBoundForMarket(marketId interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUpperAndLowerBoundForMarket", reflect.TypeOf((*MockBibliophileClient)(nil).GetUpperAndLowerBoundForMarket), marketId) +} + +// HasReferrer mocks base method. +func (m *MockBibliophileClient) HasReferrer(trader common.Address) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HasReferrer", trader) + ret0, _ := ret[0].(bool) + return ret0 +} + +// HasReferrer indicates an expected call of HasReferrer. +func (mr *MockBibliophileClientMockRecorder) HasReferrer(trader interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasReferrer", reflect.TypeOf((*MockBibliophileClient)(nil).HasReferrer), trader) +} + +// IOC_GetBlockPlaced mocks base method. +func (m *MockBibliophileClient) IOC_GetBlockPlaced(orderHash [32]byte) *big.Int { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IOC_GetBlockPlaced", orderHash) + ret0, _ := ret[0].(*big.Int) + return ret0 +} + +// IOC_GetBlockPlaced indicates an expected call of IOC_GetBlockPlaced. +func (mr *MockBibliophileClientMockRecorder) IOC_GetBlockPlaced(orderHash interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IOC_GetBlockPlaced", reflect.TypeOf((*MockBibliophileClient)(nil).IOC_GetBlockPlaced), orderHash) +} + +// IOC_GetExpirationCap mocks base method. +func (m *MockBibliophileClient) IOC_GetExpirationCap() *big.Int { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IOC_GetExpirationCap") + ret0, _ := ret[0].(*big.Int) + return ret0 +} + +// IOC_GetExpirationCap indicates an expected call of IOC_GetExpirationCap. +func (mr *MockBibliophileClientMockRecorder) IOC_GetExpirationCap() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IOC_GetExpirationCap", reflect.TypeOf((*MockBibliophileClient)(nil).IOC_GetExpirationCap)) +} + +// IOC_GetOrderFilledAmount mocks base method. +func (m *MockBibliophileClient) IOC_GetOrderFilledAmount(orderHash [32]byte) *big.Int { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IOC_GetOrderFilledAmount", orderHash) + ret0, _ := ret[0].(*big.Int) + return ret0 +} + +// IOC_GetOrderFilledAmount indicates an expected call of IOC_GetOrderFilledAmount. +func (mr *MockBibliophileClientMockRecorder) IOC_GetOrderFilledAmount(orderHash interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IOC_GetOrderFilledAmount", reflect.TypeOf((*MockBibliophileClient)(nil).IOC_GetOrderFilledAmount), orderHash) +} + +// IOC_GetOrderStatus mocks base method. +func (m *MockBibliophileClient) IOC_GetOrderStatus(orderHash [32]byte) int64 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IOC_GetOrderStatus", orderHash) + ret0, _ := ret[0].(int64) + return ret0 +} + +// IOC_GetOrderStatus indicates an expected call of IOC_GetOrderStatus. +func (mr *MockBibliophileClientMockRecorder) IOC_GetOrderStatus(orderHash interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IOC_GetOrderStatus", reflect.TypeOf((*MockBibliophileClient)(nil).IOC_GetOrderStatus), orderHash) +} + +// IsTradingAuthority mocks base method. +func (m *MockBibliophileClient) IsTradingAuthority(trader, senderOrSigner common.Address) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsTradingAuthority", trader, senderOrSigner) + ret0, _ := ret[0].(bool) + return ret0 +} + +// IsTradingAuthority indicates an expected call of IsTradingAuthority. +func (mr *MockBibliophileClientMockRecorder) IsTradingAuthority(trader, senderOrSigner interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsTradingAuthority", reflect.TypeOf((*MockBibliophileClient)(nil).IsTradingAuthority), trader, senderOrSigner) +} + +// IsValidator mocks base method. +func (m *MockBibliophileClient) IsValidator(senderOrSigner common.Address) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsValidator", senderOrSigner) + ret0, _ := ret[0].(bool) + return ret0 +} + +// IsValidator indicates an expected call of IsValidator. +func (mr *MockBibliophileClientMockRecorder) IsValidator(senderOrSigner interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsValidator", reflect.TypeOf((*MockBibliophileClient)(nil).IsValidator), senderOrSigner) +} diff --git a/precompile/contracts/bibliophile/ioc_order_book.go b/precompile/contracts/bibliophile/ioc_order_book.go new file mode 100644 index 0000000000..4199fc7dd9 --- /dev/null +++ b/precompile/contracts/bibliophile/ioc_order_book.go @@ -0,0 +1,41 @@ +package bibliophile + +import ( + "math/big" + + "github.com/ava-labs/subnet-evm/precompile/contract" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" +) + +const ( + IOC_ORDERBOOK_ADDRESS = "0x03000000000000000000000000000000000000b4" + IOC_ORDER_INFO_SLOT int64 = 1 + IOC_EXPIRATION_CAP_SLOT int64 = 2 +) + +// State Reader +func iocGetBlockPlaced(stateDB contract.StateDB, orderHash [32]byte) *big.Int { + orderInfo := iocOrderInfoMappingStorageSlot(orderHash) + return new(big.Int).SetBytes(stateDB.GetState(common.HexToAddress(IOC_ORDERBOOK_ADDRESS), common.BigToHash(orderInfo)).Bytes()) +} + +func iocGetOrderFilledAmount(stateDB contract.StateDB, orderHash [32]byte) *big.Int { + orderInfo := iocOrderInfoMappingStorageSlot(orderHash) + num := stateDB.GetState(common.HexToAddress(IOC_ORDERBOOK_ADDRESS), common.BigToHash(new(big.Int).Add(orderInfo, big.NewInt(1)))).Bytes() + return fromTwosComplement(num) +} + +func IOCGetOrderStatus(stateDB contract.StateDB, orderHash [32]byte) int64 { + orderInfo := iocOrderInfoMappingStorageSlot(orderHash) + return new(big.Int).SetBytes(stateDB.GetState(common.HexToAddress(IOC_ORDERBOOK_ADDRESS), common.BigToHash(new(big.Int).Add(orderInfo, big.NewInt(2)))).Bytes()).Int64() +} + +func iocOrderInfoMappingStorageSlot(orderHash [32]byte) *big.Int { + return new(big.Int).SetBytes(crypto.Keccak256(append(orderHash[:], common.LeftPadBytes(big.NewInt(IOC_ORDER_INFO_SLOT).Bytes(), 32)...))) +} + +func iocGetExpirationCap(stateDB contract.StateDB) *big.Int { + return new(big.Int).SetBytes(stateDB.GetState(common.HexToAddress(IOC_ORDERBOOK_ADDRESS), common.BigToHash(big.NewInt(IOC_EXPIRATION_CAP_SLOT))).Bytes()) +} diff --git a/precompile/contracts/bibliophile/limit_order_book.go b/precompile/contracts/bibliophile/limit_order_book.go new file mode 100644 index 0000000000..7e67653cfc --- /dev/null +++ b/precompile/contracts/bibliophile/limit_order_book.go @@ -0,0 +1,55 @@ +package bibliophile + +import ( + "math/big" + + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" +) + +const ( + LIMIT_ORDERBOOK_GENESIS_ADDRESS = "0x03000000000000000000000000000000000000b3" + ORDER_INFO_SLOT int64 = 1 + REDUCE_ONLY_AMOUNT_SLOT int64 = 2 + LONG_OPEN_ORDERS_SLOT int64 = 4 + SHORT_OPEN_ORDERS_SLOT int64 = 5 +) + +func getOrderFilledAmount(stateDB contract.StateDB, orderHash [32]byte) *big.Int { + orderInfo := orderInfoMappingStorageSlot(orderHash) + num := stateDB.GetState(common.HexToAddress(LIMIT_ORDERBOOK_GENESIS_ADDRESS), common.BigToHash(new(big.Int).Add(orderInfo, big.NewInt(1)))).Bytes() + return fromTwosComplement(num) +} + +func GetOrderStatus(stateDB contract.StateDB, orderHash [32]byte) int64 { + orderInfo := orderInfoMappingStorageSlot(orderHash) + return new(big.Int).SetBytes(stateDB.GetState(common.HexToAddress(LIMIT_ORDERBOOK_GENESIS_ADDRESS), common.BigToHash(new(big.Int).Add(orderInfo, big.NewInt(3)))).Bytes()).Int64() +} + +func orderInfoMappingStorageSlot(orderHash [32]byte) *big.Int { + return new(big.Int).SetBytes(crypto.Keccak256(append(orderHash[:], common.LeftPadBytes(big.NewInt(ORDER_INFO_SLOT).Bytes(), 32)...))) +} + +func getReduceOnlyAmount(stateDB contract.StateDB, trader common.Address, ammIndex *big.Int) *big.Int { + baseMappingHash := crypto.Keccak256(append(common.LeftPadBytes(trader.Bytes(), 32), common.LeftPadBytes(big.NewInt(REDUCE_ONLY_AMOUNT_SLOT).Bytes(), 32)...)) + nestedMappingHash := crypto.Keccak256(append(common.LeftPadBytes(ammIndex.Bytes(), 32), baseMappingHash...)) + return fromTwosComplement(stateDB.GetState(common.HexToAddress(LIMIT_ORDERBOOK_GENESIS_ADDRESS), common.BytesToHash(nestedMappingHash)).Bytes()) +} + +func getLongOpenOrdersAmount(stateDB contract.StateDB, trader common.Address, ammIndex *big.Int) *big.Int { + baseMappingHash := crypto.Keccak256(append(common.LeftPadBytes(trader.Bytes(), 32), common.LeftPadBytes(big.NewInt(LONG_OPEN_ORDERS_SLOT).Bytes(), 32)...)) + nestedMappingHash := crypto.Keccak256(append(common.LeftPadBytes(ammIndex.Bytes(), 32), baseMappingHash...)) + return stateDB.GetState(common.HexToAddress(LIMIT_ORDERBOOK_GENESIS_ADDRESS), common.BytesToHash(nestedMappingHash)).Big() +} + +func getShortOpenOrdersAmount(stateDB contract.StateDB, trader common.Address, ammIndex *big.Int) *big.Int { + baseMappingHash := crypto.Keccak256(append(common.LeftPadBytes(trader.Bytes(), 32), common.LeftPadBytes(big.NewInt(SHORT_OPEN_ORDERS_SLOT).Bytes(), 32)...)) + nestedMappingHash := crypto.Keccak256(append(common.LeftPadBytes(ammIndex.Bytes(), 32), baseMappingHash...)) + return stateDB.GetState(common.HexToAddress(LIMIT_ORDERBOOK_GENESIS_ADDRESS), common.BytesToHash(nestedMappingHash)).Big() +} + +func getBlockPlaced(stateDB contract.StateDB, orderHash [32]byte) *big.Int { + orderInfo := orderInfoMappingStorageSlot(orderHash) + return new(big.Int).SetBytes(stateDB.GetState(common.HexToAddress(LIMIT_ORDERBOOK_GENESIS_ADDRESS), common.BigToHash(orderInfo)).Bytes()) +} diff --git a/precompile/contracts/bibliophile/margin_account.go b/precompile/contracts/bibliophile/margin_account.go new file mode 100644 index 0000000000..9784662d03 --- /dev/null +++ b/precompile/contracts/bibliophile/margin_account.go @@ -0,0 +1,83 @@ +package bibliophile + +import ( + "math/big" + + "github.com/ava-labs/subnet-evm/precompile/contract" + + hu "github.com/ava-labs/subnet-evm/plugin/evm/orderbook/hubbleutils" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" +) + +const ( + MARGIN_ACCOUNT_GENESIS_ADDRESS = "0x03000000000000000000000000000000000000b1" + ORACLE_SLOT int64 = 4 + SUPPORTED_COLLATERAL_SLOT int64 = 8 + MARGIN_MAPPING_SLOT int64 = 10 + RESERVED_MARGIN_SLOT int64 = 11 +) + +func GetNormalizedMargin(stateDB contract.StateDB, trader common.Address) *big.Int { + assets := GetCollaterals(stateDB) + margins := getMargins(stateDB, trader) + return hu.GetNormalizedMargin(assets, margins) +} + +func getMargins(stateDB contract.StateDB, trader common.Address) []*big.Int { + numAssets := getCollateralCount(stateDB) + margins := make([]*big.Int, numAssets) + for i := uint8(0); i < numAssets; i++ { + margins[i] = getMargin(stateDB, big.NewInt(int64(i)), trader) + } + return margins +} + +func getMargin(stateDB contract.StateDB, idx *big.Int, trader common.Address) *big.Int { + marginStorageSlot := crypto.Keccak256(append(common.LeftPadBytes(idx.Bytes(), 32), common.LeftPadBytes(big.NewInt(MARGIN_MAPPING_SLOT).Bytes(), 32)...)) + marginStorageSlot = crypto.Keccak256(append(common.LeftPadBytes(trader.Bytes(), 32), marginStorageSlot...)) + return fromTwosComplement(stateDB.GetState(common.HexToAddress(MARGIN_ACCOUNT_GENESIS_ADDRESS), common.BytesToHash(marginStorageSlot)).Bytes()) +} + +func getReservedMargin(stateDB contract.StateDB, trader common.Address) *big.Int { + baseMappingHash := crypto.Keccak256(append(common.LeftPadBytes(trader.Bytes(), 32), common.LeftPadBytes(big.NewInt(RESERVED_MARGIN_SLOT).Bytes(), 32)...)) + return stateDB.GetState(common.HexToAddress(MARGIN_ACCOUNT_GENESIS_ADDRESS), common.BytesToHash(baseMappingHash)).Big() +} + +func GetAvailableMargin(stateDB contract.StateDB, trader common.Address, upgradeVersion hu.UpgradeVersion) *big.Int { + output := getNotionalPositionAndMargin(stateDB, &GetNotionalPositionAndMarginInput{Trader: trader, IncludeFundingPayments: true, Mode: uint8(1)}, upgradeVersion) // Min_Allowable_Margin + return hu.GetAvailableMargin_(output.NotionalPosition, output.Margin, getReservedMargin(stateDB, trader), GetMinAllowableMargin(stateDB)) +} + +func getOracleAddress(stateDB contract.StateDB) common.Address { + return common.BytesToAddress(stateDB.GetState(common.HexToAddress(MARGIN_ACCOUNT_GENESIS_ADDRESS), common.BigToHash(big.NewInt(ORACLE_SLOT))).Bytes()) +} + +func GetCollaterals(stateDB contract.StateDB) []hu.Collateral { + numAssets := getCollateralCount(stateDB) + assets := make([]hu.Collateral, numAssets) + for i := uint8(0); i < numAssets; i++ { + assets[i] = getCollateralAt(stateDB, i) + } + return assets +} + +func getCollateralCount(stateDB contract.StateDB) uint8 { + rawVal := stateDB.GetState(common.HexToAddress(MARGIN_ACCOUNT_GENESIS_ADDRESS), common.BigToHash(big.NewInt(SUPPORTED_COLLATERAL_SLOT))) + return uint8(new(big.Int).SetBytes(rawVal.Bytes()).Uint64()) +} + +func getCollateralAt(stateDB contract.StateDB, idx uint8) hu.Collateral { + // struct Collateral { IERC20 token; uint weight; uint8 decimals; } + baseSlot := hu.Add(collateralStorageSlot(), big.NewInt(int64(idx)*3)) // collateral struct size = 3 * 32 bytes + tokenAddress := common.BytesToAddress(stateDB.GetState(common.HexToAddress(MARGIN_ACCOUNT_GENESIS_ADDRESS), common.BigToHash(baseSlot)).Bytes()) + return hu.Collateral{ + Weight: stateDB.GetState(common.HexToAddress(MARGIN_ACCOUNT_GENESIS_ADDRESS), common.BigToHash(hu.Add(baseSlot, big.NewInt(1)))).Big(), + Decimals: uint8(stateDB.GetState(common.HexToAddress(MARGIN_ACCOUNT_GENESIS_ADDRESS), common.BigToHash(hu.Add(baseSlot, big.NewInt(2)))).Big().Uint64()), + Price: getUnderlyingPrice_(stateDB, tokenAddress), + } +} + +func collateralStorageSlot() *big.Int { + return new(big.Int).SetBytes(crypto.Keccak256(common.BigToHash(big.NewInt(SUPPORTED_COLLATERAL_SLOT)).Bytes())) +} diff --git a/precompile/contracts/bibliophile/oracle.go b/precompile/contracts/bibliophile/oracle.go new file mode 100644 index 0000000000..50634f16bd --- /dev/null +++ b/precompile/contracts/bibliophile/oracle.go @@ -0,0 +1,102 @@ +package bibliophile + +import ( + "math/big" + + hu "github.com/ava-labs/subnet-evm/plugin/evm/orderbook/hubbleutils" + "github.com/ava-labs/subnet-evm/precompile/contract" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" +) + +var ( + RED_STONE_VALUES_MAPPING_STORAGE_LOCATION = common.HexToHash("0x4dd0c77efa6f6d590c97573d8c70b714546e7311202ff7c11c484cc841d91bfc") // keccak256("RedStone.oracleValuesMapping"); + RED_STONE_LATEST_ROUND_ID_STORAGE_LOCATION = common.HexToHash("0xc68d7f1ee07d8668991a8951e720010c9d44c2f11c06b5cac61fbc4083263938") // keccak256("RedStone.latestRoundId"); + + AGGREGATOR_MAP_SLOT int64 = 1 + RED_STONE_ADAPTER_SLOT int64 = 2 + CUSTOM_ORACLE_ROUND_ID_SLOT int64 = 0 + CUSTOM_ORACLE_ENTRIES_SLOT int64 = 1 +) + +const ( + // this slot is from TestOracle.sol + TEST_ORACLE_PRICES_MAPPING_SLOT int64 = 3 +) + +func getUnderlyingPrice(stateDB contract.StateDB, market common.Address) *big.Int { + return getUnderlyingPrice_(stateDB, getUnderlyingAssetAddress(stateDB, market)) +} + +func getUnderlyingPrice_(stateDB contract.StateDB, underlying common.Address) *big.Int { + oracle := getOracleAddress(stateDB) // this comes from margin account + + // 1. Check for redstone feed id + feedId := getRedStoneFeedId(stateDB, oracle, underlying) + if feedId.Big().Sign() != 0 { + // redstone oracle is configured for this market + redStoneAdapter := getRedStoneAdapterAddress(stateDB, oracle) + redstonePrice := getRedStonePrice(stateDB, redStoneAdapter, feedId) + return redstonePrice + } + + // 2. Check for custom oracle + aggregator := getAggregatorAddress(stateDB, oracle, underlying) + if aggregator.Big().Sign() != 0 { + // custom oracle is configured for this market + price := getCustomOraclePrice(stateDB, aggregator) + log.Info("custom-oracle-price", "underlying", underlying, "price", price) + return price + } + + // 3. neither red stone nor custom oracle is enabled for this market, we use the default TestOracle + slot := crypto.Keccak256(append(common.LeftPadBytes(underlying.Bytes(), 32), common.BigToHash(big.NewInt(TEST_ORACLE_PRICES_MAPPING_SLOT)).Bytes()...)) + return fromTwosComplement(stateDB.GetState(oracle, common.BytesToHash(slot)).Bytes()) +} + +func getMidPrice(stateDB contract.StateDB, market common.Address) *big.Int { + asksHead := getAsksHead(stateDB, market) + bidsHead := getBidsHead(stateDB, market) + if asksHead.Sign() == 0 || bidsHead.Sign() == 0 { + return getUnderlyingPrice(stateDB, market) + } + return hu.Div(hu.Add(asksHead, bidsHead), big.NewInt(2)) +} + +func getRedStoneAdapterAddress(stateDB contract.StateDB, oracle common.Address) common.Address { + return common.BytesToAddress(stateDB.GetState(oracle, common.BigToHash(big.NewInt(RED_STONE_ADAPTER_SLOT))).Bytes()) +} + +func getRedStonePrice(stateDB contract.StateDB, adapterAddress common.Address, redStoneFeedId common.Hash) *big.Int { + latestRoundId := getlatestRoundId(stateDB, adapterAddress) + slot := common.BytesToHash(crypto.Keccak256(append(append(redStoneFeedId.Bytes(), common.LeftPadBytes(latestRoundId.Bytes(), 32)...), RED_STONE_VALUES_MAPPING_STORAGE_LOCATION.Bytes()...))) + return new(big.Int).Div(fromTwosComplement(stateDB.GetState(adapterAddress, slot).Bytes()), big.NewInt(100)) // we use 6 decimals precision everywhere +} + +func getlatestRoundId(stateDB contract.StateDB, adapterAddress common.Address) *big.Int { + return fromTwosComplement(stateDB.GetState(adapterAddress, RED_STONE_LATEST_ROUND_ID_STORAGE_LOCATION).Bytes()) +} + +func aggregatorMapSlot(underlying common.Address) *big.Int { + return new(big.Int).SetBytes(crypto.Keccak256(append(common.LeftPadBytes(underlying.Bytes(), 32), common.BigToHash(big.NewInt(AGGREGATOR_MAP_SLOT)).Bytes()...))) +} + +func getRedStoneFeedId(stateDB contract.StateDB, oracle, underlying common.Address) common.Hash { + aggregatorMapSlot := aggregatorMapSlot(underlying) + return stateDB.GetState(oracle, common.BigToHash(aggregatorMapSlot)) +} + +func getAggregatorAddress(stateDB contract.StateDB, oracle, underlying common.Address) common.Address { + aggregatorMapSlot := aggregatorMapSlot(underlying) + aggregatorSlot := hu.Add(aggregatorMapSlot, big.NewInt(1)) + return common.BytesToAddress(stateDB.GetState(oracle, common.BigToHash(aggregatorSlot)).Bytes()) +} + +func getCustomOraclePrice(stateDB contract.StateDB, aggregator common.Address) *big.Int { + roundId := stateDB.GetState(aggregator, common.BigToHash(big.NewInt(CUSTOM_ORACLE_ROUND_ID_SLOT))).Bytes() + entriesSlot := new(big.Int).SetBytes(crypto.Keccak256(append(common.LeftPadBytes(roundId, 32), common.BigToHash(big.NewInt(CUSTOM_ORACLE_ENTRIES_SLOT)).Bytes()...))) + priceSlot := hu.Add(entriesSlot, big.NewInt(1)) + return hu.Div(fromTwosComplement(stateDB.GetState(aggregator, common.BigToHash(priceSlot)).Bytes()), big.NewInt(100)) // we use 6 decimals precision everywhere +} diff --git a/precompile/contracts/bibliophile/orderbook.go b/precompile/contracts/bibliophile/orderbook.go new file mode 100644 index 0000000000..ce0c834f53 --- /dev/null +++ b/precompile/contracts/bibliophile/orderbook.go @@ -0,0 +1,69 @@ +package bibliophile + +import ( + "errors" + "math/big" + + "github.com/ava-labs/subnet-evm/precompile/contract" + + hu "github.com/ava-labs/subnet-evm/plugin/evm/orderbook/hubbleutils" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" +) + +const ( + ORDERBOOK_GENESIS_ADDRESS = "0x03000000000000000000000000000000000000b0" + IS_VALIDATOR_SLOT int64 = 1 + IS_TRADING_AUTHORITY_SLOT int64 = 2 + JUROR_SLOT int64 = 3 + ORDER_HANDLER_STORAGE_SLOT int64 = 5 +) + +var ( + ErrNotLongOrder = errors.New("OB_order_0_is_not_long") + ErrNotShortOrder = errors.New("OB_order_1_is_not_short") + ErrNotSameAMM = errors.New("OB_orders_for_different_amms") + ErrNoMatch = errors.New("OB_orders_do_not_match") + ErrInvalidOrder = errors.New("OB_invalid_order") + ErrNotMultiple = errors.New("OB.not_multiple") + ErrTooLow = errors.New("OB_long_order_price_too_low") + ErrTooHigh = errors.New("OB_short_order_price_too_high") +) + +// State Reader + +func IsTradingAuthority(stateDB contract.StateDB, trader, senderOrSigner common.Address) bool { + tradingAuthorityMappingSlot := crypto.Keccak256(append(common.LeftPadBytes(trader.Bytes(), 32), common.LeftPadBytes(big.NewInt(IS_TRADING_AUTHORITY_SLOT).Bytes(), 32)...)) + tradingAuthorityMappingSlot = crypto.Keccak256(append(common.LeftPadBytes(senderOrSigner.Bytes(), 32), tradingAuthorityMappingSlot...)) + return stateDB.GetState(common.HexToAddress(ORDERBOOK_GENESIS_ADDRESS), common.BytesToHash(tradingAuthorityMappingSlot)).Big().Cmp(big.NewInt(1)) == 0 +} + +func IsValidator(stateDB contract.StateDB, senderOrSigner common.Address) bool { + isValidatorMappingSlot := crypto.Keccak256(append(common.LeftPadBytes(senderOrSigner.Bytes(), 32), common.LeftPadBytes(big.NewInt(IS_VALIDATOR_SLOT).Bytes(), 32)...)) + return stateDB.GetState(common.HexToAddress(ORDERBOOK_GENESIS_ADDRESS), common.BytesToHash(isValidatorMappingSlot)).Big().Cmp(big.NewInt(1)) == 0 +} + +func JurorAddress(stateDB contract.StateDB) common.Address { + return common.BytesToAddress(stateDB.GetState(common.HexToAddress(ORDERBOOK_GENESIS_ADDRESS), common.BigToHash(new(big.Int).SetInt64(JUROR_SLOT))).Bytes()) +} + +// Helper functions + +func GetAcceptableBounds(stateDB contract.StateDB, marketID int64) (upperBound, lowerBound *big.Int) { + market := GetMarketAddressFromMarketID(marketID, stateDB) + return calculateBounds(getMaxOraclePriceSpread(stateDB, market), getUnderlyingPrice(stateDB, market), getMultiplier(stateDB, market)) +} + +func GetAcceptableBoundsForLiquidation(stateDB contract.StateDB, marketID int64) (upperBound, lowerBound *big.Int) { + market := GetMarketAddressFromMarketID(marketID, stateDB) + return calculateBounds(getMaxLiquidationPriceSpread(stateDB, market), getUnderlyingPrice(stateDB, market), getMultiplier(stateDB, market)) +} + +func calculateBounds(spreadLimit, oraclePrice, multiplier *big.Int) (*big.Int, *big.Int) { + upperbound := hu.RoundOff(hu.Div1e6(hu.Mul(oraclePrice, hu.Add1e6(spreadLimit))), multiplier) + lowerbound := big.NewInt(0) + if spreadLimit.Cmp(hu.ONE_E_6) == -1 { + lowerbound = hu.RoundOff(hu.Div1e6(hu.Mul(oraclePrice, hu.Sub(hu.ONE_E_6, spreadLimit))), multiplier) + } + return upperbound, lowerbound +} diff --git a/precompile/contracts/bibliophile/referral.go b/precompile/contracts/bibliophile/referral.go new file mode 100644 index 0000000000..f093eaad64 --- /dev/null +++ b/precompile/contracts/bibliophile/referral.go @@ -0,0 +1,29 @@ +package bibliophile + +import ( + "math/big" + + "github.com/ava-labs/subnet-evm/precompile/contract" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" +) + +const ( + TRADER_TO_REFERRER_SLOT int64 = 3 + RESTRICTED_INVITES_SLOT int64 = 6 +) + +func restrictedInvites(stateDB contract.StateDB, referralContract common.Address) bool { + return stateDB.GetState(referralContract, common.BigToHash(big.NewInt(RESTRICTED_INVITES_SLOT))).Big().Uint64() == 1 +} + +func traderToReferrer(stateDB contract.StateDB, referralContract, trader common.Address) common.Address { + pos := crypto.Keccak256(append(common.LeftPadBytes(trader.Bytes(), 32), common.LeftPadBytes(big.NewInt(TRADER_TO_REFERRER_SLOT).Bytes(), 32)...)) + return common.BytesToAddress(stateDB.GetState(referralContract, common.BytesToHash(pos)).Bytes()) +} + +func HasReferrer(stateDB contract.StateDB, trader common.Address) bool { + referralContract := getReferralAddress(stateDB) + return !restrictedInvites(stateDB, referralContract) || traderToReferrer(stateDB, referralContract, trader) != common.Address{} +} diff --git a/precompile/contracts/bibliophile/signed_order_book.go b/precompile/contracts/bibliophile/signed_order_book.go new file mode 100644 index 0000000000..5aeb33ffd6 --- /dev/null +++ b/precompile/contracts/bibliophile/signed_order_book.go @@ -0,0 +1,36 @@ +package bibliophile + +import ( + "math/big" + + "github.com/ava-labs/subnet-evm/precompile/contract" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" +) + +const ( + SIGNED_ORDER_INFO_SLOT int64 = 53 +) + +// State Reader +func GetSignedOrderFilledAmount(stateDB contract.StateDB, orderHash [32]byte) *big.Int { + orderInfo := signedOrderInfoMappingStorageSlot(orderHash) + num := stateDB.GetState(GetSignedOrderBookAddress(stateDB), common.BigToHash(orderInfo)).Bytes() + return fromTwosComplement(num) +} + +func GetSignedOrderStatus(stateDB contract.StateDB, orderHash [32]byte) int64 { + a := GetSignedOrderBookAddress(stateDB) + orderInfo := signedOrderInfoMappingStorageSlot(orderHash) + return new(big.Int).SetBytes(stateDB.GetState(a, common.BigToHash(new(big.Int).Add(orderInfo, big.NewInt(1)))).Bytes()).Int64() +} + +func signedOrderInfoMappingStorageSlot(orderHash [32]byte) *big.Int { + return new(big.Int).SetBytes(crypto.Keccak256(append(orderHash[:], common.LeftPadBytes(big.NewInt(SIGNED_ORDER_INFO_SLOT).Bytes(), 32)...))) +} + +func GetSignedOrderBookAddress(stateDB contract.StateDB) common.Address { + slot := crypto.Keccak256(append(common.LeftPadBytes(big.NewInt(2).Bytes() /* orderType */, 32), common.LeftPadBytes(big.NewInt(ORDER_HANDLER_STORAGE_SLOT).Bytes(), 32)...)) + return common.BytesToAddress(stateDB.GetState(common.HexToAddress(ORDERBOOK_GENESIS_ADDRESS), common.BytesToHash(slot)).Bytes()) +} diff --git a/precompile/contracts/juror/README.md b/precompile/contracts/juror/README.md new file mode 100644 index 0000000000..d81e622b2b --- /dev/null +++ b/precompile/contracts/juror/README.md @@ -0,0 +1,23 @@ +There are some must-be-done changes waiting in the generated file. Each area requiring you to add your code is marked with CUSTOM CODE to make them easy to find and modify. +Additionally there are other files you need to edit to activate your precompile. +These areas are highlighted with comments "ADD YOUR PRECOMPILE HERE". +For testing take a look at other precompile tests in contract_test.go and config_test.go in other precompile folders. +See the tutorial in for more information about precompile development. + +General guidelines for precompile development: +1- Set a suitable config key in generated module.go. E.g: "yourPrecompileConfig" +2- Read the comment and set a suitable contract address in generated module.go. E.g: +ContractAddress = common.HexToAddress("ASUITABLEHEXADDRESS") +3- It is recommended to only modify code in the highlighted areas marked with "CUSTOM CODE STARTS HERE". Typically, custom codes are required in only those areas. +Modifying code outside of these areas should be done with caution and with a deep understanding of how these changes may impact the EVM. +4- Set gas costs in generated contract.go +5- Force import your precompile package in precompile/registry/registry.go +6- Add your config unit tests under generated package config_test.go +7- Add your contract unit tests under generated package contract_test.go +8- Additionally you can add a full-fledged VM test for your precompile under plugin/vm/vm_test.go. See existing precompile tests for examples. +9- Add your solidity interface and test contract to contracts/contracts +10- Write solidity contract tests for your precompile in contracts/contracts/test +11- Write TypeScript DS-Test counterparts for your solidity tests in contracts/test +12- Create your genesis with your precompile enabled in tests/precompile/genesis/ +13- Create e2e test for your solidity test in tests/precompile/solidity/suites.go +14- Run your e2e precompile Solidity tests with './scripts/run_ginkgo.sh` diff --git a/precompile/contracts/juror/config.go b/precompile/contracts/juror/config.go new file mode 100644 index 0000000000..9a9502355f --- /dev/null +++ b/precompile/contracts/juror/config.go @@ -0,0 +1,68 @@ +// Code generated +// This file is a generated precompile contract config with stubbed abstract functions. +// The file is generated by a template. Please inspect every code and comment in this file before use. + +package juror + +import ( + "math/big" + + "github.com/ava-labs/subnet-evm/precompile/precompileconfig" +) + +var _ precompileconfig.Config = &Config{} + +// Config implements the precompileconfig.Config interface and +// adds specific configuration for Juror. +type Config struct { + precompileconfig.Upgrade + // CUSTOM CODE STARTS HERE + // Add your own custom fields for Config here +} + +// NewConfig returns a config for a network upgrade at [blockTimestamp] that enables +// Juror. +func NewConfig(blockTimestamp *big.Int) *Config { + val := blockTimestamp.Uint64() + return &Config{ + Upgrade: precompileconfig.Upgrade{BlockTimestamp: &val}, + } +} + +// NewDisableConfig returns config for a network upgrade at [blockTimestamp] +// that disables Juror. +func NewDisableConfig(blockTimestamp *big.Int) *Config { + val := blockTimestamp.Uint64() + return &Config{ + Upgrade: precompileconfig.Upgrade{ + BlockTimestamp: &val, + Disable: true, + }, + } +} + +// Key returns the key for the Juror precompileconfig. +// This should be the same key as used in the precompile module. +func (*Config) Key() string { return ConfigKey } + +// Verify tries to verify Config and returns an error accordingly. +func (c *Config) Verify(precompileconfig.ChainConfig) error { + // CUSTOM CODE STARTS HERE + // Add your own custom verify code for Config here + // and return an error accordingly + return nil +} + +// Equal returns true if [s] is a [*Config] and it has been configured identical to [c]. +func (c *Config) Equal(s precompileconfig.Config) bool { + // typecast before comparison + other, ok := (s).(*Config) + if !ok { + return false + } + // CUSTOM CODE STARTS HERE + // modify this boolean accordingly with your custom Config, to check if [other] and the current [c] are equal + // if Config contains only Upgrade you can skip modifying it. + equals := c.Upgrade.Equal(&other.Upgrade) + return equals +} diff --git a/precompile/contracts/juror/config_test.go b/precompile/contracts/juror/config_test.go new file mode 100644 index 0000000000..cf99789d43 --- /dev/null +++ b/precompile/contracts/juror/config_test.go @@ -0,0 +1,62 @@ +// Code generated +// This file is a generated precompile config test with the skeleton of test functions. +// The file is generated by a template. Please inspect every code and comment in this file before use. + +package juror + +import ( + "math/big" + "testing" + + "github.com/ava-labs/subnet-evm/precompile/precompileconfig" + "github.com/ava-labs/subnet-evm/precompile/testutils" + "go.uber.org/mock/gomock" +) + +// TestVerify tests the verification of Config. +func TestVerify(t *testing.T) { + tests := map[string]testutils.ConfigVerifyTest{ + "valid config": { + Config: NewConfig(big.NewInt(3)), + ExpectedError: "", + }, + // CUSTOM CODE STARTS HERE + // Add your own Verify tests here, e.g.: + // "your custom test name": { + // Config: NewConfig(big.NewInt(3),), + // ExpectedError: ErrYourCustomError.Error(), + // }, + } + // Run verify tests. + testutils.RunVerifyTests(t, tests) +} + +// TestEqual tests the equality of Config with other precompile configs. +func TestEqual(t *testing.T) { + tests := map[string]testutils.ConfigEqualTest{ + "non-nil config and nil other": { + Config: NewConfig(big.NewInt(3)), + Other: nil, + Expected: false, + }, + "different type": { + Config: NewConfig(big.NewInt(3)), + Other: precompileconfig.NewMockConfig(gomock.NewController(t)), + Expected: false, + }, + "different timestamp": { + Config: NewConfig(big.NewInt(3)), + Other: NewConfig(big.NewInt(4)), + Expected: false, + }, + "same config": { + Config: NewConfig(big.NewInt(3)), + Other: NewConfig(big.NewInt(3)), + Expected: true, + }, + // CUSTOM CODE STARTS HERE + // Add your own Equal tests here + } + // Run equal tests. + testutils.RunEqualTests(t, tests) +} diff --git a/precompile/contracts/juror/contract.abi b/precompile/contracts/juror/contract.abi new file mode 100644 index 0000000000..4a9f817580 --- /dev/null +++ b/precompile/contracts/juror/contract.abi @@ -0,0 +1 @@ +[{"inputs":[{"internalType":"address","name":"trader","type":"address"},{"internalType":"bool","name":"includeFundingPayments","type":"bool"},{"internalType":"uint8","name":"mode","type":"uint8"}],"name":"getNotionalPositionAndMargin","outputs":[{"internalType":"uint256","name":"notionalPosition","type":"uint256"},{"internalType":"int256","name":"margin","type":"int256"}],"stateMutability":"view","type":"function"},{"inputs":[{"components":[{"internalType":"uint256","name":"ammIndex","type":"uint256"},{"internalType":"address","name":"trader","type":"address"},{"internalType":"int256","name":"baseAssetQuantity","type":"int256"},{"internalType":"uint256","name":"price","type":"uint256"},{"internalType":"uint256","name":"salt","type":"uint256"},{"internalType":"bool","name":"reduceOnly","type":"bool"},{"internalType":"bool","name":"postOnly","type":"bool"}],"internalType":"struct ILimitOrderBook.Order","name":"order","type":"tuple"},{"internalType":"address","name":"sender","type":"address"},{"internalType":"bool","name":"assertLowMargin","type":"bool"}],"name":"validateCancelLimitOrder","outputs":[{"internalType":"string","name":"err","type":"string"},{"internalType":"bytes32","name":"orderHash","type":"bytes32"},{"components":[{"internalType":"int256","name":"unfilledAmount","type":"int256"},{"internalType":"address","name":"amm","type":"address"}],"internalType":"struct IOrderHandler.CancelOrderRes","name":"res","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"data","type":"bytes"},{"internalType":"uint256","name":"liquidationAmount","type":"uint256"}],"name":"validateLiquidationOrderAndDetermineFillPrice","outputs":[{"internalType":"string","name":"err","type":"string"},{"internalType":"enum IJuror.BadElement","name":"element","type":"uint8"},{"components":[{"components":[{"internalType":"uint256","name":"ammIndex","type":"uint256"},{"internalType":"address","name":"trader","type":"address"},{"internalType":"bytes32","name":"orderHash","type":"bytes32"},{"internalType":"enum IClearingHouse.OrderExecutionMode","name":"mode","type":"uint8"}],"internalType":"struct IClearingHouse.Instruction","name":"instruction","type":"tuple"},{"internalType":"uint8","name":"orderType","type":"uint8"},{"internalType":"bytes","name":"encodedOrder","type":"bytes"},{"internalType":"uint256","name":"fillPrice","type":"uint256"},{"internalType":"int256","name":"fillAmount","type":"int256"}],"internalType":"struct IOrderHandler.LiquidationMatchingValidationRes","name":"res","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes[2]","name":"data","type":"bytes[2]"},{"internalType":"int256","name":"fillAmount","type":"int256"}],"name":"validateOrdersAndDetermineFillPrice","outputs":[{"internalType":"string","name":"err","type":"string"},{"internalType":"enum IJuror.BadElement","name":"element","type":"uint8"},{"components":[{"components":[{"internalType":"uint256","name":"ammIndex","type":"uint256"},{"internalType":"address","name":"trader","type":"address"},{"internalType":"bytes32","name":"orderHash","type":"bytes32"},{"internalType":"enum IClearingHouse.OrderExecutionMode","name":"mode","type":"uint8"}],"internalType":"struct IClearingHouse.Instruction[2]","name":"instructions","type":"tuple[2]"},{"internalType":"uint8[2]","name":"orderTypes","type":"uint8[2]"},{"internalType":"bytes[2]","name":"encodedOrders","type":"bytes[2]"},{"internalType":"uint256","name":"fillPrice","type":"uint256"}],"internalType":"struct IOrderHandler.MatchingValidationRes","name":"res","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"components":[{"internalType":"uint8","name":"orderType","type":"uint8"},{"internalType":"uint256","name":"expireAt","type":"uint256"},{"internalType":"uint256","name":"ammIndex","type":"uint256"},{"internalType":"address","name":"trader","type":"address"},{"internalType":"int256","name":"baseAssetQuantity","type":"int256"},{"internalType":"uint256","name":"price","type":"uint256"},{"internalType":"uint256","name":"salt","type":"uint256"},{"internalType":"bool","name":"reduceOnly","type":"bool"}],"internalType":"struct IImmediateOrCancelOrders.Order","name":"order","type":"tuple"},{"internalType":"address","name":"sender","type":"address"}],"name":"validatePlaceIOCOrder","outputs":[{"internalType":"string","name":"err","type":"string"},{"internalType":"bytes32","name":"orderHash","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"components":[{"internalType":"uint256","name":"ammIndex","type":"uint256"},{"internalType":"address","name":"trader","type":"address"},{"internalType":"int256","name":"baseAssetQuantity","type":"int256"},{"internalType":"uint256","name":"price","type":"uint256"},{"internalType":"uint256","name":"salt","type":"uint256"},{"internalType":"bool","name":"reduceOnly","type":"bool"},{"internalType":"bool","name":"postOnly","type":"bool"}],"internalType":"struct ILimitOrderBook.Order","name":"order","type":"tuple"},{"internalType":"address","name":"sender","type":"address"}],"name":"validatePlaceLimitOrder","outputs":[{"internalType":"string","name":"err","type":"string"},{"internalType":"bytes32","name":"orderhash","type":"bytes32"},{"components":[{"internalType":"uint256","name":"reserveAmount","type":"uint256"},{"internalType":"address","name":"amm","type":"address"}],"internalType":"struct IOrderHandler.PlaceOrderRes","name":"res","type":"tuple"}],"stateMutability":"view","type":"function"}] diff --git a/precompile/contracts/juror/contract.go b/precompile/contracts/juror/contract.go new file mode 100644 index 0000000000..3957d34c66 --- /dev/null +++ b/precompile/contracts/juror/contract.go @@ -0,0 +1,493 @@ +// Code generated +// This file is a generated precompile contract config with stubbed abstract functions. +// The file is generated by a template. Please inspect every code and comment in this file before use. + +package juror + +import ( + "errors" + "fmt" + "math/big" + + "github.com/ava-labs/subnet-evm/accounts/abi" + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ava-labs/subnet-evm/precompile/contracts/bibliophile" + + _ "embed" + + "github.com/ethereum/go-ethereum/common" +) + +const ( + // Gas costs for each function. These are set to 1 by default. + // You should set a gas cost for each function in your contract. + // Generally, you should not set gas costs very low as this may cause your network to be vulnerable to DoS attacks. + // There are some predefined gas costs in contract/utils.go that you can use. + GetNotionalPositionAndMarginGasCost uint64 = 69 + ValidateCancelLimitOrderGasCost uint64 = 69 + ValidateLiquidationOrderAndDetermineFillPriceGasCost uint64 = 69 + ValidateOrdersAndDetermineFillPriceGasCost uint64 = 69 + ValidatePlaceIOCOrderGasCost uint64 = 69 + ValidatePlaceLimitOrderGasCost uint64 = 69 +) + +// CUSTOM CODE STARTS HERE +// Reference imports to suppress errors from unused imports. This code and any unnecessary imports can be removed. +var ( + _ = abi.JSON + _ = errors.New + _ = big.NewInt +) + +// Singleton StatefulPrecompiledContract and signatures. +var ( + + // JurorRawABI contains the raw ABI of Juror contract. + //go:embed contract.abi + JurorRawABI string + + JurorABI = contract.ParseABI(JurorRawABI) + + JurorPrecompile = createJurorPrecompile() +) + +// IClearingHouseInstruction is an auto generated low-level Go binding around an user-defined struct. +type IClearingHouseInstruction struct { + AmmIndex *big.Int + Trader common.Address + OrderHash [32]byte + Mode uint8 +} + +// IImmediateOrCancelOrdersOrder is an auto generated low-level Go binding around an user-defined struct. +type IImmediateOrCancelOrdersOrder struct { + OrderType uint8 + ExpireAt *big.Int + AmmIndex *big.Int + Trader common.Address + BaseAssetQuantity *big.Int + Price *big.Int + Salt *big.Int + ReduceOnly bool +} + +// ILimitOrderBookOrder is an auto generated low-level Go binding around an user-defined struct. +type ILimitOrderBookOrder struct { + AmmIndex *big.Int + Trader common.Address + BaseAssetQuantity *big.Int + Price *big.Int + Salt *big.Int + ReduceOnly bool + PostOnly bool +} + +// IOrderHandlerCancelOrderRes is an auto generated low-level Go binding around an user-defined struct. +type IOrderHandlerCancelOrderRes struct { + UnfilledAmount *big.Int + Amm common.Address +} + +// IOrderHandlerLiquidationMatchingValidationRes is an auto generated low-level Go binding around an user-defined struct. +type IOrderHandlerLiquidationMatchingValidationRes struct { + Instruction IClearingHouseInstruction + OrderType uint8 + EncodedOrder []byte + FillPrice *big.Int + FillAmount *big.Int +} + +// IOrderHandlerMatchingValidationRes is an auto generated low-level Go binding around an user-defined struct. +type IOrderHandlerMatchingValidationRes struct { + Instructions [2]IClearingHouseInstruction + OrderTypes [2]uint8 + EncodedOrders [2][]byte + FillPrice *big.Int +} + +// IOrderHandlerPlaceOrderRes is an auto generated low-level Go binding around an user-defined struct. +type IOrderHandlerPlaceOrderRes struct { + ReserveAmount *big.Int + Amm common.Address +} + +type GetNotionalPositionAndMarginInput struct { + Trader common.Address + IncludeFundingPayments bool + Mode uint8 +} + +type GetNotionalPositionAndMarginOutput struct { + NotionalPosition *big.Int + Margin *big.Int +} + +type ValidateCancelLimitOrderInput struct { + Order ILimitOrderBookOrder + Sender common.Address + AssertLowMargin bool +} + +type ValidateCancelLimitOrderOutput struct { + Err string + OrderHash [32]byte + Res IOrderHandlerCancelOrderRes +} + +type ValidateLiquidationOrderAndDetermineFillPriceInput struct { + Data []byte + LiquidationAmount *big.Int +} + +type ValidateLiquidationOrderAndDetermineFillPriceOutput struct { + Err string + Element uint8 + Res IOrderHandlerLiquidationMatchingValidationRes +} + +type ValidateOrdersAndDetermineFillPriceInput struct { + Data [2][]byte + FillAmount *big.Int +} + +type ValidateOrdersAndDetermineFillPriceOutput struct { + Err string + Element uint8 + Res IOrderHandlerMatchingValidationRes +} + +type ValidatePlaceIOCOrderInput struct { + Order IImmediateOrCancelOrdersOrder + Sender common.Address +} + +type ValidatePlaceIOCOrderOutput struct { + Err string + OrderHash [32]byte +} + +type ValidatePlaceLimitOrderInput struct { + Order ILimitOrderBookOrder + Sender common.Address +} + +type ValidatePlaceLimitOrderOutput struct { + Err string + Orderhash [32]byte + Res IOrderHandlerPlaceOrderRes +} + +// UnpackGetNotionalPositionAndMarginInput attempts to unpack [input] as GetNotionalPositionAndMarginInput +// assumes that [input] does not include selector (omits first 4 func signature bytes) +func UnpackGetNotionalPositionAndMarginInput(input []byte) (GetNotionalPositionAndMarginInput, error) { + inputStruct := GetNotionalPositionAndMarginInput{} + err := JurorABI.UnpackInputIntoInterface(&inputStruct, "getNotionalPositionAndMargin", input, true) + + return inputStruct, err +} + +// PackGetNotionalPositionAndMargin packs [inputStruct] of type GetNotionalPositionAndMarginInput into the appropriate arguments for getNotionalPositionAndMargin. +func PackGetNotionalPositionAndMargin(inputStruct GetNotionalPositionAndMarginInput) ([]byte, error) { + return JurorABI.Pack("getNotionalPositionAndMargin", inputStruct.Trader, inputStruct.IncludeFundingPayments, inputStruct.Mode) +} + +// PackGetNotionalPositionAndMarginOutput attempts to pack given [outputStruct] of type GetNotionalPositionAndMarginOutput +// to conform the ABI outputs. +func PackGetNotionalPositionAndMarginOutput(outputStruct GetNotionalPositionAndMarginOutput) ([]byte, error) { + return JurorABI.PackOutput("getNotionalPositionAndMargin", + outputStruct.NotionalPosition, + outputStruct.Margin, + ) +} + +func getNotionalPositionAndMargin(accessibleState contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + if remainingGas, err = contract.DeductGas(suppliedGas, GetNotionalPositionAndMarginGasCost); err != nil { + return nil, 0, err + } + // attempts to unpack [input] into the arguments to the GetNotionalPositionAndMarginInput. + // Assumes that [input] does not include selector + // You can use unpacked [inputStruct] variable in your code + inputStruct, err := UnpackGetNotionalPositionAndMarginInput(input) + if err != nil { + return nil, remainingGas, err + } + + // CUSTOM CODE STARTS HERE + bibliophile := bibliophile.NewBibliophileClient(accessibleState) + output := GetNotionalPositionAndMargin(bibliophile, &inputStruct) + packedOutput, err := PackGetNotionalPositionAndMarginOutput(output) + if err != nil { + return nil, remainingGas, err + } + + // Return the packed output and the remaining gas + return packedOutput, remainingGas, nil +} + +// UnpackValidateCancelLimitOrderInput attempts to unpack [input] as ValidateCancelLimitOrderInput +// assumes that [input] does not include selector (omits first 4 func signature bytes) +func UnpackValidateCancelLimitOrderInput(input []byte) (ValidateCancelLimitOrderInput, error) { + inputStruct := ValidateCancelLimitOrderInput{} + err := JurorABI.UnpackInputIntoInterface(&inputStruct, "validateCancelLimitOrder", input, true) + + return inputStruct, err +} + +// PackValidateCancelLimitOrder packs [inputStruct] of type ValidateCancelLimitOrderInput into the appropriate arguments for validateCancelLimitOrder. +func PackValidateCancelLimitOrder(inputStruct ValidateCancelLimitOrderInput) ([]byte, error) { + return JurorABI.Pack("validateCancelLimitOrder", inputStruct.Order, inputStruct.Sender, inputStruct.AssertLowMargin) +} + +// PackValidateCancelLimitOrderOutput attempts to pack given [outputStruct] of type ValidateCancelLimitOrderOutput +// to conform the ABI outputs. +func PackValidateCancelLimitOrderOutput(outputStruct ValidateCancelLimitOrderOutput) ([]byte, error) { + return JurorABI.PackOutput("validateCancelLimitOrder", + outputStruct.Err, + outputStruct.OrderHash, + outputStruct.Res, + ) +} + +func validateCancelLimitOrder(accessibleState contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + if remainingGas, err = contract.DeductGas(suppliedGas, ValidateCancelLimitOrderGasCost); err != nil { + return nil, 0, err + } + // attempts to unpack [input] into the arguments to the ValidateCancelLimitOrderInput. + // Assumes that [input] does not include selector + // You can use unpacked [inputStruct] variable in your code + inputStruct, err := UnpackValidateCancelLimitOrderInput(input) + if err != nil { + return nil, remainingGas, err + } + + // CUSTOM CODE STARTS HERE + bibliophile := bibliophile.NewBibliophileClient(accessibleState) + output := ValidateCancelLimitOrder(bibliophile, &inputStruct) + packedOutput, err := PackValidateCancelLimitOrderOutput(output) + if err != nil { + return nil, remainingGas, err + } + + // Return the packed output and the remaining gas + return packedOutput, remainingGas, nil +} + +// UnpackValidateLiquidationOrderAndDetermineFillPriceInput attempts to unpack [input] as ValidateLiquidationOrderAndDetermineFillPriceInput +// assumes that [input] does not include selector (omits first 4 func signature bytes) +func UnpackValidateLiquidationOrderAndDetermineFillPriceInput(input []byte) (ValidateLiquidationOrderAndDetermineFillPriceInput, error) { + inputStruct := ValidateLiquidationOrderAndDetermineFillPriceInput{} + err := JurorABI.UnpackInputIntoInterface(&inputStruct, "validateLiquidationOrderAndDetermineFillPrice", input, true) + + return inputStruct, err +} + +// PackValidateLiquidationOrderAndDetermineFillPrice packs [inputStruct] of type ValidateLiquidationOrderAndDetermineFillPriceInput into the appropriate arguments for validateLiquidationOrderAndDetermineFillPrice. +func PackValidateLiquidationOrderAndDetermineFillPrice(inputStruct ValidateLiquidationOrderAndDetermineFillPriceInput) ([]byte, error) { + return JurorABI.Pack("validateLiquidationOrderAndDetermineFillPrice", inputStruct.Data, inputStruct.LiquidationAmount) +} + +// PackValidateLiquidationOrderAndDetermineFillPriceOutput attempts to pack given [outputStruct] of type ValidateLiquidationOrderAndDetermineFillPriceOutput +// to conform the ABI outputs. +func PackValidateLiquidationOrderAndDetermineFillPriceOutput(outputStruct ValidateLiquidationOrderAndDetermineFillPriceOutput) ([]byte, error) { + return JurorABI.PackOutput("validateLiquidationOrderAndDetermineFillPrice", + outputStruct.Err, + outputStruct.Element, + outputStruct.Res, + ) +} + +func validateLiquidationOrderAndDetermineFillPrice(accessibleState contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + if remainingGas, err = contract.DeductGas(suppliedGas, ValidateLiquidationOrderAndDetermineFillPriceGasCost); err != nil { + return nil, 0, err + } + // attempts to unpack [input] into the arguments to the ValidateLiquidationOrderAndDetermineFillPriceInput. + // Assumes that [input] does not include selector + // You can use unpacked [inputStruct] variable in your code + inputStruct, err := UnpackValidateLiquidationOrderAndDetermineFillPriceInput(input) + if err != nil { + return nil, remainingGas, err + } + + // CUSTOM CODE STARTS HERE + bibliophile := bibliophile.NewBibliophileClient(accessibleState) + output := ValidateLiquidationOrderAndDetermineFillPrice(bibliophile, &inputStruct) + packedOutput, err := PackValidateLiquidationOrderAndDetermineFillPriceOutput(output) + if err != nil { + return nil, remainingGas, err + } + + // Return the packed output and the remaining gas + return packedOutput, remainingGas, nil +} + +// UnpackValidateOrdersAndDetermineFillPriceInput attempts to unpack [input] as ValidateOrdersAndDetermineFillPriceInput +// assumes that [input] does not include selector (omits first 4 func signature bytes) +func UnpackValidateOrdersAndDetermineFillPriceInput(input []byte) (ValidateOrdersAndDetermineFillPriceInput, error) { + inputStruct := ValidateOrdersAndDetermineFillPriceInput{} + err := JurorABI.UnpackInputIntoInterface(&inputStruct, "validateOrdersAndDetermineFillPrice", input, true) + + return inputStruct, err +} + +// PackValidateOrdersAndDetermineFillPrice packs [inputStruct] of type ValidateOrdersAndDetermineFillPriceInput into the appropriate arguments for validateOrdersAndDetermineFillPrice. +func PackValidateOrdersAndDetermineFillPrice(inputStruct ValidateOrdersAndDetermineFillPriceInput) ([]byte, error) { + return JurorABI.Pack("validateOrdersAndDetermineFillPrice", inputStruct.Data, inputStruct.FillAmount) +} + +// PackValidateOrdersAndDetermineFillPriceOutput attempts to pack given [outputStruct] of type ValidateOrdersAndDetermineFillPriceOutput +// to conform the ABI outputs. +func PackValidateOrdersAndDetermineFillPriceOutput(outputStruct ValidateOrdersAndDetermineFillPriceOutput) ([]byte, error) { + return JurorABI.PackOutput("validateOrdersAndDetermineFillPrice", + outputStruct.Err, + outputStruct.Element, + outputStruct.Res, + ) +} + +func validateOrdersAndDetermineFillPrice(accessibleState contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + if remainingGas, err = contract.DeductGas(suppliedGas, ValidateOrdersAndDetermineFillPriceGasCost); err != nil { + return nil, 0, err + } + // attempts to unpack [input] into the arguments to the ValidateOrdersAndDetermineFillPriceInput. + // Assumes that [input] does not include selector + // You can use unpacked [inputStruct] variable in your code + inputStruct, err := UnpackValidateOrdersAndDetermineFillPriceInput(input) + if err != nil { + return nil, remainingGas, err + } + + // CUSTOM CODE STARTS HERE + bibliophile := bibliophile.NewBibliophileClient(accessibleState) + output := ValidateOrdersAndDetermineFillPrice(bibliophile, &inputStruct) + packedOutput, err := PackValidateOrdersAndDetermineFillPriceOutput(output) + if err != nil { + return nil, remainingGas, err + } + + // Return the packed output and the remaining gas + return packedOutput, remainingGas, nil +} + +// UnpackValidatePlaceIOCOrderInput attempts to unpack [input] as ValidatePlaceIOCOrderInput +// assumes that [input] does not include selector (omits first 4 func signature bytes) +func UnpackValidatePlaceIOCOrderInput(input []byte) (ValidatePlaceIOCOrderInput, error) { + inputStruct := ValidatePlaceIOCOrderInput{} + err := JurorABI.UnpackInputIntoInterface(&inputStruct, "validatePlaceIOCOrder", input, true) + + return inputStruct, err +} + +// PackValidatePlaceIOCOrder packs [inputStruct] of type ValidatePlaceIOCOrderInput into the appropriate arguments for validatePlaceIOCOrder. +func PackValidatePlaceIOCOrder(inputStruct ValidatePlaceIOCOrderInput) ([]byte, error) { + return JurorABI.Pack("validatePlaceIOCOrder", inputStruct.Order, inputStruct.Sender) +} + +// PackValidatePlaceIOCOrderOutput attempts to pack given [outputStruct] of type ValidatePlaceIOCOrderOutput +// to conform the ABI outputs. +func PackValidatePlaceIOCOrderOutput(outputStruct ValidatePlaceIOCOrderOutput) ([]byte, error) { + return JurorABI.PackOutput("validatePlaceIOCOrder", + outputStruct.Err, + outputStruct.OrderHash, + ) +} + +func validatePlaceIOCOrder(accessibleState contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + if remainingGas, err = contract.DeductGas(suppliedGas, ValidatePlaceIOCOrderGasCost); err != nil { + return nil, 0, err + } + // attempts to unpack [input] into the arguments to the ValidatePlaceIOCOrderInput. + // Assumes that [input] does not include selector + // You can use unpacked [inputStruct] variable in your code + inputStruct, err := UnpackValidatePlaceIOCOrderInput(input) + if err != nil { + return nil, remainingGas, err + } + + // CUSTOM CODE STARTS HERE + bibliophile := bibliophile.NewBibliophileClient(accessibleState) + output := ValidatePlaceIOCorder(bibliophile, &inputStruct) + packedOutput, err := PackValidatePlaceIOCOrderOutput(output) + if err != nil { + return nil, remainingGas, err + } + + // Return the packed output and the remaining gas + return packedOutput, remainingGas, nil +} + +// UnpackValidatePlaceLimitOrderInput attempts to unpack [input] as ValidatePlaceLimitOrderInput +// assumes that [input] does not include selector (omits first 4 func signature bytes) +func UnpackValidatePlaceLimitOrderInput(input []byte) (ValidatePlaceLimitOrderInput, error) { + inputStruct := ValidatePlaceLimitOrderInput{} + err := JurorABI.UnpackInputIntoInterface(&inputStruct, "validatePlaceLimitOrder", input, true) + + return inputStruct, err +} + +// PackValidatePlaceLimitOrder packs [inputStruct] of type ValidatePlaceLimitOrderInput into the appropriate arguments for validatePlaceLimitOrder. +func PackValidatePlaceLimitOrder(inputStruct ValidatePlaceLimitOrderInput) ([]byte, error) { + return JurorABI.Pack("validatePlaceLimitOrder", inputStruct.Order, inputStruct.Sender) +} + +// PackValidatePlaceLimitOrderOutput attempts to pack given [outputStruct] of type ValidatePlaceLimitOrderOutput +// to conform the ABI outputs. +func PackValidatePlaceLimitOrderOutput(outputStruct ValidatePlaceLimitOrderOutput) ([]byte, error) { + return JurorABI.PackOutput("validatePlaceLimitOrder", + outputStruct.Err, + outputStruct.Orderhash, + outputStruct.Res, + ) +} + +func validatePlaceLimitOrder(accessibleState contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + if remainingGas, err = contract.DeductGas(suppliedGas, ValidatePlaceLimitOrderGasCost); err != nil { + return nil, 0, err + } + // attempts to unpack [input] into the arguments to the ValidatePlaceLimitOrderInput. + // Assumes that [input] does not include selector + // You can use unpacked [inputStruct] variable in your code + inputStruct, err := UnpackValidatePlaceLimitOrderInput(input) + if err != nil { + return nil, remainingGas, err + } + + // CUSTOM CODE STARTS HERE + bibliophile := bibliophile.NewBibliophileClient(accessibleState) + output := ValidatePlaceLimitOrder(bibliophile, &inputStruct) + packedOutput, err := PackValidatePlaceLimitOrderOutput(output) + if err != nil { + return nil, remainingGas, err + } + + // Return the packed output and the remaining gas + return packedOutput, remainingGas, nil +} + +// createJurorPrecompile returns a StatefulPrecompiledContract with getters and setters for the precompile. + +func createJurorPrecompile() contract.StatefulPrecompiledContract { + var functions []*contract.StatefulPrecompileFunction + + abiFunctionMap := map[string]contract.RunStatefulPrecompileFunc{ + "getNotionalPositionAndMargin": getNotionalPositionAndMargin, + "validateCancelLimitOrder": validateCancelLimitOrder, + "validateLiquidationOrderAndDetermineFillPrice": validateLiquidationOrderAndDetermineFillPrice, + "validateOrdersAndDetermineFillPrice": validateOrdersAndDetermineFillPrice, + "validatePlaceIOCOrder": validatePlaceIOCOrder, + "validatePlaceLimitOrder": validatePlaceLimitOrder, + } + + for name, function := range abiFunctionMap { + method, ok := JurorABI.Methods[name] + if !ok { + panic(fmt.Errorf("given method (%s) does not exist in the ABI", name)) + } + functions = append(functions, contract.NewStatefulPrecompileFunction(method.ID, function)) + } + // Construct the contract with no fallback function. + statefulContract, err := contract.NewStatefulPrecompileContract(nil, functions) + if err != nil { + panic(err) + } + return statefulContract +} diff --git a/precompile/contracts/juror/contract_test.go b/precompile/contracts/juror/contract_test.go new file mode 100644 index 0000000000..b124d6a4a2 --- /dev/null +++ b/precompile/contracts/juror/contract_test.go @@ -0,0 +1,168 @@ +// Code generated +// This file is a generated precompile contract test with the skeleton of test functions. +// The file is generated by a template. Please inspect every code and comment in this file before use. + +package juror + +import ( + "math/big" + "testing" + + "github.com/ava-labs/subnet-evm/core/state" + ob "github.com/ava-labs/subnet-evm/plugin/evm/orderbook" + "github.com/ava-labs/subnet-evm/precompile/testutils" + "github.com/ava-labs/subnet-evm/vmerrs" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +// These tests are run against the precompile contract directly with +// the given input and expected output. They're just a guide to +// help you write your own tests. These tests are for general cases like +// allowlist, readOnly behaviour, and gas cost. You should write your own +// tests for specific cases. +var ( + tests = map[string]testutils.PrecompileTest{ + "insufficient gas for getNotionalPositionAndMargin should fail": { + Caller: common.Address{1}, + InputFn: func(t testing.TB) []byte { + // CUSTOM CODE STARTS HERE + // populate test input here + testInput := GetNotionalPositionAndMarginInput{ + Trader: common.Address{1}, + } + input, err := PackGetNotionalPositionAndMargin(testInput) + require.NoError(t, err) + return input + }, + SuppliedGas: GetNotionalPositionAndMarginGasCost - 1, + ReadOnly: false, + ExpectedErr: vmerrs.ErrOutOfGas.Error(), + }, + "insufficient gas for validateCancelLimitOrder should fail": { + Caller: common.Address{1}, + InputFn: func(t testing.TB) []byte { + // CUSTOM CODE STARTS HERE + // populate test input here + testInput := ValidateCancelLimitOrderInput{ + Order: ILimitOrderBookOrder{ + AmmIndex: big.NewInt(0), + Trader: common.Address{1}, + BaseAssetQuantity: big.NewInt(0), + Price: big.NewInt(0), + Salt: big.NewInt(0), + ReduceOnly: false, + PostOnly: false, + }, + Sender: common.Address{1}, + } + input, err := PackValidateCancelLimitOrder(testInput) + require.NoError(t, err) + return input + }, + SuppliedGas: ValidateCancelLimitOrderGasCost - 1, + ReadOnly: false, + ExpectedErr: vmerrs.ErrOutOfGas.Error(), + }, + "insufficient gas for validateLiquidationOrderAndDetermineFillPrice should fail": { + Caller: common.Address{1}, + InputFn: func(t testing.TB) []byte { + // CUSTOM CODE STARTS HERE + // populate test input here + testInput := ValidateLiquidationOrderAndDetermineFillPriceInput{ + LiquidationAmount: big.NewInt(0), + } + input, err := PackValidateLiquidationOrderAndDetermineFillPrice(testInput) + require.NoError(t, err) + return input + }, + SuppliedGas: ValidateLiquidationOrderAndDetermineFillPriceGasCost - 1, + ReadOnly: false, + ExpectedErr: vmerrs.ErrOutOfGas.Error(), + }, + "insufficient gas for validateOrdersAndDetermineFillPrice should fail": { + Caller: common.Address{1}, + InputFn: func(t testing.TB) []byte { + // CUSTOM CODE STARTS HERE + // populate test input here + testInput := ValidateOrdersAndDetermineFillPriceInput{FillAmount: big.NewInt(0)} + input, err := PackValidateOrdersAndDetermineFillPrice(testInput) + require.NoError(t, err) + return input + }, + SuppliedGas: ValidateOrdersAndDetermineFillPriceGasCost - 1, + ReadOnly: false, + ExpectedErr: vmerrs.ErrOutOfGas.Error(), + }, + "insufficient gas for validatePlaceIOCOrder should fail": { + Caller: common.Address{1}, + InputFn: func(t testing.TB) []byte { + // CUSTOM CODE STARTS HERE + // populate test input here + testInput := ValidatePlaceIOCOrderInput{ + Order: IImmediateOrCancelOrdersOrder{ + OrderType: uint8(ob.IOC), + ExpireAt: big.NewInt(0), + AmmIndex: big.NewInt(0), + Trader: common.Address{1}, + BaseAssetQuantity: big.NewInt(0), + Price: big.NewInt(0), + Salt: big.NewInt(0), + ReduceOnly: false, + }, + Sender: common.Address{1}, + } + input, err := PackValidatePlaceIOCOrder(testInput) + require.NoError(t, err) + return input + }, + SuppliedGas: ValidatePlaceIOCOrderGasCost - 1, + ReadOnly: false, + ExpectedErr: vmerrs.ErrOutOfGas.Error(), + }, + "insufficient gas for validatePlaceLimitOrder should fail": { + Caller: common.Address{1}, + InputFn: func(t testing.TB) []byte { + // CUSTOM CODE STARTS HERE + // populate test input here + testInput := ValidatePlaceLimitOrderInput{ + Order: ILimitOrderBookOrder{ + AmmIndex: big.NewInt(0), + Trader: common.Address{1}, + BaseAssetQuantity: big.NewInt(0), + Price: big.NewInt(0), + Salt: big.NewInt(0), + ReduceOnly: false, + PostOnly: false, + }, + Sender: common.Address{1}, + } + input, err := PackValidatePlaceLimitOrder(testInput) + require.NoError(t, err) + return input + }, + SuppliedGas: ValidatePlaceLimitOrderGasCost - 1, + ReadOnly: false, + ExpectedErr: vmerrs.ErrOutOfGas.Error(), + }, + } +) + +// TestJurorRun tests the Run function of the precompile contract. +func TestJurorRun(t *testing.T) { + // Run tests. + for name, test := range tests { + t.Run(name, func(t *testing.T) { + test.Run(t, Module, state.NewTestStateDB(t)) + }) + } +} + +func BenchmarkJuror(b *testing.B) { + // Benchmark tests. + for name, test := range tests { + b.Run(name, func(b *testing.B) { + test.Bench(b, Module, state.NewTestStateDB(b)) + }) + } +} diff --git a/precompile/contracts/juror/ioc_orders.go b/precompile/contracts/juror/ioc_orders.go new file mode 100644 index 0000000000..9958960665 --- /dev/null +++ b/precompile/contracts/juror/ioc_orders.go @@ -0,0 +1,110 @@ +package juror + +import ( + "errors" + "math/big" + + ob "github.com/ava-labs/subnet-evm/plugin/evm/orderbook" + hu "github.com/ava-labs/subnet-evm/plugin/evm/orderbook/hubbleutils" + b "github.com/ava-labs/subnet-evm/precompile/contracts/bibliophile" + "github.com/ethereum/go-ethereum/common" +) + +func ValidatePlaceIOCorder(bibliophile b.BibliophileClient, inputStruct *ValidatePlaceIOCOrderInput) (response ValidatePlaceIOCOrderOutput) { + order := inputStruct.Order + trader := order.Trader + + var err error + response.OrderHash, err = IImmediateOrCancelOrdersOrderToIOCOrder(&inputStruct.Order).Hash() + if err != nil { + response.Err = err.Error() + return + } + + if trader != inputStruct.Sender && !bibliophile.IsTradingAuthority(trader, inputStruct.Sender) { + response.Err = ErrNoTradingAuthority.Error() + return + } + if order.BaseAssetQuantity.Sign() == 0 { + response.Err = ErrInvalidFillAmount.Error() + return + } + if ob.OrderType(order.OrderType) != ob.IOC { + response.Err = ErrNotIOCOrder.Error() + return + } + + blockTimestamp := bibliophile.GetTimeStamp() + expireWithin := blockTimestamp + bibliophile.IOC_GetExpirationCap().Uint64() + if order.ExpireAt.Uint64() < blockTimestamp { + response.Err = errors.New("ioc expired").Error() + return + } + if order.ExpireAt.Uint64() > expireWithin { + response.Err = errors.New("ioc expiration too far").Error() + return + } + minSize := bibliophile.GetMinSizeRequirement(order.AmmIndex.Int64()) + if new(big.Int).Mod(order.BaseAssetQuantity, minSize).Sign() != 0 { + response.Err = ErrNotMultiple.Error() + return + } + + if OrderStatus(bibliophile.IOC_GetOrderStatus(response.OrderHash)) != Invalid { + response.Err = ErrInvalidOrder.Error() + return + } + + if !bibliophile.HasReferrer(order.Trader) { + response.Err = ErrNoReferrer.Error() + return + } + + ammAddress := bibliophile.GetMarketAddressFromMarketID(order.AmmIndex.Int64()) + if (ammAddress == common.Address{}) { + response.Err = ErrInvalidMarket.Error() + return + } + + // this check is sort of redundant because either ways user can circumvent this by placing several reduceOnly order in a single tx/block + if order.ReduceOnly { + posSize := bibliophile.GetSize(ammAddress, &trader) + // a reduce only order should reduce position + if !reducesPosition(posSize, order.BaseAssetQuantity) { + response.Err = ErrReduceOnlyBaseAssetQuantityInvalid.Error() + return + } + + reduceOnlyAmount := bibliophile.GetReduceOnlyAmount(trader, order.AmmIndex) + if hu.Abs(hu.Add(reduceOnlyAmount, order.BaseAssetQuantity)).Cmp(hu.Abs(posSize)) == 1 { + response.Err = ErrNetReduceOnlyAmountExceeded.Error() + return + } + } + + if order.Price.Sign() != 1 { + response.Err = ErrInvalidPrice.Error() + return + } + + if hu.Mod(order.Price, bibliophile.GetPriceMultiplier(ammAddress)).Sign() != 0 { + response.Err = ErrPricePrecision.Error() + return + } + return response +} + +func IImmediateOrCancelOrdersOrderToIOCOrder(order *IImmediateOrCancelOrdersOrder) *ob.IOCOrder { + return &ob.IOCOrder{ + BaseOrder: hu.BaseOrder{ + AmmIndex: order.AmmIndex, + Trader: order.Trader, + BaseAssetQuantity: order.BaseAssetQuantity, + Price: order.Price, + Salt: order.Salt, + ReduceOnly: order.ReduceOnly, + }, + OrderType: order.OrderType, + ExpireAt: order.ExpireAt, + } +} diff --git a/precompile/contracts/juror/ioc_orders_test.go b/precompile/contracts/juror/ioc_orders_test.go new file mode 100644 index 0000000000..c73f0f5e6a --- /dev/null +++ b/precompile/contracts/juror/ioc_orders_test.go @@ -0,0 +1,532 @@ +package juror + +import ( + "errors" + "math/big" + "testing" + + "github.com/ava-labs/subnet-evm/plugin/evm/orderbook" + hu "github.com/ava-labs/subnet-evm/plugin/evm/orderbook/hubbleutils" + b "github.com/ava-labs/subnet-evm/precompile/contracts/bibliophile" + "github.com/ethereum/go-ethereum/common" + gomock "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" +) + +type ValidatePlaceIOCOrderTestCase struct { + Order IImmediateOrCancelOrdersOrder + Sender common.Address + Error error // response error +} + +func testValidatePlaceIOCOrderTestCase(t *testing.T, mockBibliophile *b.MockBibliophileClient, c ValidatePlaceIOCOrderTestCase) { + testInput := ValidatePlaceIOCOrderInput{ + Order: c.Order, + Sender: c.Sender, + } + + // call precompile + response := ValidatePlaceIOCorder(mockBibliophile, &testInput) + + // verify results + if c.Error == nil && response.Err != "" { + t.Fatalf("expected no error, got %v", response.Err) + } + if c.Error != nil && response.Err != c.Error.Error() { + t.Fatalf("expected %v, got %v", c.Error, response.Err) + } +} + +func TestValidatePlaceIOCOrder(t *testing.T) { + trader := common.HexToAddress("0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC") + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + t.Run("no trading authority", func(t *testing.T) { + mockBibliophile := b.NewMockBibliophileClient(ctrl) + mockBibliophile.EXPECT().IsTradingAuthority(trader, common.Address{1}).Return(false) + + testValidatePlaceIOCOrderTestCase(t, mockBibliophile, ValidatePlaceIOCOrderTestCase{ + Order: IImmediateOrCancelOrdersOrder{ + OrderType: 1, + ExpireAt: big.NewInt(0), + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(5), + Price: big.NewInt(100), + Salt: big.NewInt(1), + ReduceOnly: false, + }, + Sender: common.Address{1}, + Error: ErrNoTradingAuthority, + }) + }) + + t.Run("invalid fill amount", func(t *testing.T) { + mockBibliophile := b.NewMockBibliophileClient(ctrl) + mockBibliophile.EXPECT().IsTradingAuthority(trader, common.Address{1}).Return(true) + + testValidatePlaceIOCOrderTestCase(t, mockBibliophile, ValidatePlaceIOCOrderTestCase{ + Order: IImmediateOrCancelOrdersOrder{ + OrderType: 1, + ExpireAt: big.NewInt(0), + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(0), + Price: big.NewInt(100), + Salt: big.NewInt(2), + ReduceOnly: false, + }, + Sender: common.Address{1}, + Error: ErrInvalidFillAmount, + }) + }) + + t.Run("not IOC order", func(t *testing.T) { + mockBibliophile := b.NewMockBibliophileClient(ctrl) + mockBibliophile.EXPECT().IsTradingAuthority(trader, common.Address{1}).Return(true) + + testValidatePlaceIOCOrderTestCase(t, mockBibliophile, ValidatePlaceIOCOrderTestCase{ + Order: IImmediateOrCancelOrdersOrder{ + OrderType: 0, + ExpireAt: big.NewInt(0), + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(5), + Price: big.NewInt(100), + Salt: big.NewInt(3), + ReduceOnly: false, + }, + Sender: common.Address{1}, + Error: ErrNotIOCOrder, + }) + }) + + t.Run("ioc expired", func(t *testing.T) { + mockBibliophile := b.NewMockBibliophileClient(ctrl) + mockBibliophile.EXPECT().IsTradingAuthority(trader, common.Address{1}).Return(true) + mockBibliophile.EXPECT().GetTimeStamp().Return(uint64(1000)) + mockBibliophile.EXPECT().IOC_GetExpirationCap().Return(big.NewInt(5)) + + testValidatePlaceIOCOrderTestCase(t, mockBibliophile, ValidatePlaceIOCOrderTestCase{ + Order: IImmediateOrCancelOrdersOrder{ + OrderType: 1, + ExpireAt: big.NewInt(900), + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(5), + Price: big.NewInt(100), + Salt: big.NewInt(4), + ReduceOnly: false, + }, + Sender: common.Address{1}, + Error: errors.New("ioc expired"), + }) + }) + + t.Run("ioc expiration too far", func(t *testing.T) { + mockBibliophile := b.NewMockBibliophileClient(ctrl) + mockBibliophile.EXPECT().IsTradingAuthority(trader, common.Address{1}).Return(true) + mockBibliophile.EXPECT().GetTimeStamp().Return(uint64(1000)) + mockBibliophile.EXPECT().IOC_GetExpirationCap().Return(big.NewInt(5)) + + testValidatePlaceIOCOrderTestCase(t, mockBibliophile, ValidatePlaceIOCOrderTestCase{ + Order: IImmediateOrCancelOrdersOrder{ + OrderType: 1, + ExpireAt: big.NewInt(1006), + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(5), + Price: big.NewInt(100), + Salt: big.NewInt(5), + ReduceOnly: false, + }, + Sender: common.Address{1}, + Error: errors.New("ioc expiration too far"), + }) + }) + + t.Run("not multiple", func(t *testing.T) { + mockBibliophile := b.NewMockBibliophileClient(ctrl) + mockBibliophile.EXPECT().IsTradingAuthority(trader, common.Address{1}).Return(true) + mockBibliophile.EXPECT().GetTimeStamp().Return(uint64(1000)) + mockBibliophile.EXPECT().IOC_GetExpirationCap().Return(big.NewInt(5)) + mockBibliophile.EXPECT().GetMinSizeRequirement(int64(0)).Return(big.NewInt(5)) + + testValidatePlaceIOCOrderTestCase(t, mockBibliophile, ValidatePlaceIOCOrderTestCase{ + Order: IImmediateOrCancelOrdersOrder{ + OrderType: 1, + ExpireAt: big.NewInt(1004), + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(7), + Price: big.NewInt(100), + Salt: big.NewInt(6), + ReduceOnly: false, + }, + Sender: common.Address{1}, + Error: ErrNotMultiple, + }) + }) + + t.Run("invalid order", func(t *testing.T) { + mockBibliophile := b.NewMockBibliophileClient(ctrl) + mockBibliophile.EXPECT().IsTradingAuthority(trader, common.Address{1}).Return(true) + mockBibliophile.EXPECT().GetTimeStamp().Return(uint64(1000)) + mockBibliophile.EXPECT().IOC_GetExpirationCap().Return(big.NewInt(5)) + mockBibliophile.EXPECT().GetMinSizeRequirement(int64(0)).Return(big.NewInt(5)) + + order := IImmediateOrCancelOrdersOrder{ + OrderType: 1, + ExpireAt: big.NewInt(1004), + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(10), + Price: big.NewInt(100), + Salt: big.NewInt(7), + ReduceOnly: false, + } + hash, _ := IImmediateOrCancelOrdersOrderToIOCOrder(&order).Hash() + + mockBibliophile.EXPECT().IOC_GetOrderStatus(hash).Return(int64(1)) + + testValidatePlaceIOCOrderTestCase(t, mockBibliophile, ValidatePlaceIOCOrderTestCase{ + Order: order, + Sender: common.Address{1}, + Error: ErrInvalidOrder, + }) + }) + + t.Run("no referrer", func(t *testing.T) { + mockBibliophile := b.NewMockBibliophileClient(ctrl) + mockBibliophile.EXPECT().IsTradingAuthority(trader, common.Address{1}).Return(true) + mockBibliophile.EXPECT().GetTimeStamp().Return(uint64(1000)) + mockBibliophile.EXPECT().IOC_GetExpirationCap().Return(big.NewInt(5)) + mockBibliophile.EXPECT().GetMinSizeRequirement(int64(0)).Return(big.NewInt(5)) + + order := IImmediateOrCancelOrdersOrder{ + OrderType: 1, + ExpireAt: big.NewInt(1004), + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(10), + Price: big.NewInt(100), + Salt: big.NewInt(8), + ReduceOnly: false, + } + hash, _ := IImmediateOrCancelOrdersOrderToIOCOrder(&order).Hash() + + mockBibliophile.EXPECT().IOC_GetOrderStatus(hash).Return(int64(0)) + mockBibliophile.EXPECT().HasReferrer(trader).Return(false) + + testValidatePlaceIOCOrderTestCase(t, mockBibliophile, ValidatePlaceIOCOrderTestCase{ + Order: order, + Sender: common.Address{1}, + Error: ErrNoReferrer, + }) + }) + + t.Run("invalid market", func(t *testing.T) { + mockBibliophile := b.NewMockBibliophileClient(ctrl) + mockBibliophile.EXPECT().IsTradingAuthority(trader, common.Address{1}).Return(true) + mockBibliophile.EXPECT().GetTimeStamp().Return(uint64(1000)) + mockBibliophile.EXPECT().IOC_GetExpirationCap().Return(big.NewInt(5)) + mockBibliophile.EXPECT().GetMinSizeRequirement(int64(0)).Return(big.NewInt(5)) + + order := IImmediateOrCancelOrdersOrder{ + OrderType: 1, + ExpireAt: big.NewInt(1004), + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(10), + Price: big.NewInt(100), + Salt: big.NewInt(9), + ReduceOnly: false, + } + hash, _ := IImmediateOrCancelOrdersOrderToIOCOrder(&order).Hash() + + mockBibliophile.EXPECT().IOC_GetOrderStatus(hash).Return(int64(0)) + mockBibliophile.EXPECT().HasReferrer(trader).Return(true) + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(int64(0)).Return(common.Address{}) + + testValidatePlaceIOCOrderTestCase(t, mockBibliophile, ValidatePlaceIOCOrderTestCase{ + Order: order, + Sender: common.Address{1}, + Error: ErrInvalidMarket, + }) + }) + + t.Run("reduce only - doesn't reduce position", func(t *testing.T) { + mockBibliophile := b.NewMockBibliophileClient(ctrl) + mockBibliophile.EXPECT().IsTradingAuthority(trader, common.Address{1}).Return(true) + mockBibliophile.EXPECT().GetTimeStamp().Return(uint64(1000)) + mockBibliophile.EXPECT().IOC_GetExpirationCap().Return(big.NewInt(5)) + mockBibliophile.EXPECT().GetMinSizeRequirement(int64(0)).Return(big.NewInt(5)) + + order := IImmediateOrCancelOrdersOrder{ + OrderType: 1, + ExpireAt: big.NewInt(1004), + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(10), + Price: big.NewInt(100), + Salt: big.NewInt(10), + ReduceOnly: true, + } + hash, _ := IImmediateOrCancelOrdersOrderToIOCOrder(&order).Hash() + + mockBibliophile.EXPECT().IOC_GetOrderStatus(hash).Return(int64(0)) + mockBibliophile.EXPECT().HasReferrer(trader).Return(true) + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(int64(0)).Return(common.Address{101}) + mockBibliophile.EXPECT().GetSize(common.Address{101}, &trader).Return(big.NewInt(-5)) + + testValidatePlaceIOCOrderTestCase(t, mockBibliophile, ValidatePlaceIOCOrderTestCase{ + Order: order, + Sender: common.Address{1}, + Error: ErrReduceOnlyBaseAssetQuantityInvalid, + }) + }) + + t.Run("reduce only - reduce only amount exceeded", func(t *testing.T) { + mockBibliophile := b.NewMockBibliophileClient(ctrl) + mockBibliophile.EXPECT().IsTradingAuthority(trader, common.Address{1}).Return(true) + mockBibliophile.EXPECT().GetTimeStamp().Return(uint64(1000)) + mockBibliophile.EXPECT().IOC_GetExpirationCap().Return(big.NewInt(5)) + mockBibliophile.EXPECT().GetMinSizeRequirement(int64(0)).Return(big.NewInt(5)) + + order := IImmediateOrCancelOrdersOrder{ + OrderType: 1, + ExpireAt: big.NewInt(1004), + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(10), + Price: big.NewInt(100), + Salt: big.NewInt(11), + ReduceOnly: true, + } + hash, _ := IImmediateOrCancelOrdersOrderToIOCOrder(&order).Hash() + + mockBibliophile.EXPECT().IOC_GetOrderStatus(hash).Return(int64(0)) + mockBibliophile.EXPECT().HasReferrer(trader).Return(true) + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(int64(0)).Return(common.Address{101}) + mockBibliophile.EXPECT().GetSize(common.Address{101}, &trader).Return(big.NewInt(-15)) + mockBibliophile.EXPECT().GetReduceOnlyAmount(trader, order.AmmIndex).Return(big.NewInt(10)) + + testValidatePlaceIOCOrderTestCase(t, mockBibliophile, ValidatePlaceIOCOrderTestCase{ + Order: order, + Sender: common.Address{1}, + Error: ErrNetReduceOnlyAmountExceeded, + }) + }) + + t.Run("invalid price - negative price", func(t *testing.T) { + mockBibliophile := b.NewMockBibliophileClient(ctrl) + mockBibliophile.EXPECT().IsTradingAuthority(trader, common.Address{1}).Return(true) + mockBibliophile.EXPECT().GetTimeStamp().Return(uint64(1000)) + mockBibliophile.EXPECT().IOC_GetExpirationCap().Return(big.NewInt(5)) + mockBibliophile.EXPECT().GetMinSizeRequirement(int64(0)).Return(big.NewInt(5)) + + order := IImmediateOrCancelOrdersOrder{ + OrderType: 1, + ExpireAt: big.NewInt(1004), + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(10), + Price: big.NewInt(-100), + Salt: big.NewInt(12), + ReduceOnly: true, + } + hash, _ := IImmediateOrCancelOrdersOrderToIOCOrder(&order).Hash() + + mockBibliophile.EXPECT().IOC_GetOrderStatus(hash).Return(int64(0)) + mockBibliophile.EXPECT().HasReferrer(trader).Return(true) + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(int64(0)).Return(common.Address{101}) + mockBibliophile.EXPECT().GetSize(common.Address{101}, &trader).Return(big.NewInt(-15)) + mockBibliophile.EXPECT().GetReduceOnlyAmount(trader, order.AmmIndex).Return(big.NewInt(0)) + + testValidatePlaceIOCOrderTestCase(t, mockBibliophile, ValidatePlaceIOCOrderTestCase{ + Order: order, + Sender: common.Address{1}, + Error: ErrInvalidPrice, + }) + }) + + t.Run("invalid price - price not multiple of price multiplier", func(t *testing.T) { + mockBibliophile := b.NewMockBibliophileClient(ctrl) + mockBibliophile.EXPECT().IsTradingAuthority(trader, common.Address{1}).Return(true) + mockBibliophile.EXPECT().GetTimeStamp().Return(uint64(1000)) + mockBibliophile.EXPECT().IOC_GetExpirationCap().Return(big.NewInt(5)) + mockBibliophile.EXPECT().GetMinSizeRequirement(int64(0)).Return(big.NewInt(5)) + + order := IImmediateOrCancelOrdersOrder{ + OrderType: 1, + ExpireAt: big.NewInt(1004), + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(10), + Price: big.NewInt(101), + Salt: big.NewInt(13), + ReduceOnly: true, + } + hash, _ := IImmediateOrCancelOrdersOrderToIOCOrder(&order).Hash() + + mockBibliophile.EXPECT().IOC_GetOrderStatus(hash).Return(int64(0)) + mockBibliophile.EXPECT().HasReferrer(trader).Return(true) + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(int64(0)).Return(common.Address{101}) + mockBibliophile.EXPECT().GetSize(common.Address{101}, &trader).Return(big.NewInt(-15)) + mockBibliophile.EXPECT().GetReduceOnlyAmount(trader, order.AmmIndex).Return(big.NewInt(0)) + mockBibliophile.EXPECT().GetPriceMultiplier(common.Address{101}).Return(big.NewInt(10)) + + testValidatePlaceIOCOrderTestCase(t, mockBibliophile, ValidatePlaceIOCOrderTestCase{ + Order: order, + Sender: common.Address{1}, + Error: ErrPricePrecision, + }) + + t.Run("valid order", func(t *testing.T) { + mockBibliophile := b.NewMockBibliophileClient(ctrl) + mockBibliophile.EXPECT().IsTradingAuthority(trader, common.Address{1}).Return(true) + mockBibliophile.EXPECT().GetTimeStamp().Return(uint64(1000)) + mockBibliophile.EXPECT().IOC_GetExpirationCap().Return(big.NewInt(5)) + mockBibliophile.EXPECT().GetMinSizeRequirement(int64(0)).Return(big.NewInt(5)) + + order := IImmediateOrCancelOrdersOrder{ + OrderType: 1, + ExpireAt: big.NewInt(1004), + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(10), + Price: big.NewInt(100), + Salt: big.NewInt(13), + ReduceOnly: true, + } + hash, _ := IImmediateOrCancelOrdersOrderToIOCOrder(&order).Hash() + + mockBibliophile.EXPECT().IOC_GetOrderStatus(hash).Return(int64(0)) + mockBibliophile.EXPECT().HasReferrer(trader).Return(true) + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(int64(0)).Return(common.Address{101}) + mockBibliophile.EXPECT().GetSize(common.Address{101}, &trader).Return(big.NewInt(-15)) + mockBibliophile.EXPECT().GetReduceOnlyAmount(trader, order.AmmIndex).Return(big.NewInt(0)) + mockBibliophile.EXPECT().GetPriceMultiplier(common.Address{101}).Return(big.NewInt(10)) + + testValidatePlaceIOCOrderTestCase(t, mockBibliophile, ValidatePlaceIOCOrderTestCase{ + Order: order, + Sender: common.Address{1}, + Error: nil, + }) + }) + }) +} + +func TestValidateExecuteIOCOrder(t *testing.T) { + trader := common.HexToAddress("0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC") + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + t.Run("not ioc order", func(t *testing.T) { + mockBibliophile := b.NewMockBibliophileClient(ctrl) + + order := orderbook.IOCOrder{ + OrderType: 0, // incoreect order type + ExpireAt: big.NewInt(1001), + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(10), + Price: big.NewInt(100), + Salt: big.NewInt(1), + ReduceOnly: false, + }, + } + m, err := validateExecuteIOCOrder(mockBibliophile, &order, Long, big.NewInt(10)) + assert.EqualError(t, err, "not ioc order") + hash, _ := order.Hash() + assert.Equal(t, m.OrderHash, hash) + }) + + t.Run("ioc expired", func(t *testing.T) { + mockBibliophile := b.NewMockBibliophileClient(ctrl) + + order := orderbook.IOCOrder{ + OrderType: 1, + ExpireAt: big.NewInt(990), + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(10), + Price: big.NewInt(100), + Salt: big.NewInt(1), + ReduceOnly: false, + }, + } + mockBibliophile.EXPECT().GetTimeStamp().Return(uint64(1000)) + + m, err := validateExecuteIOCOrder(mockBibliophile, &order, Long, big.NewInt(10)) + assert.EqualError(t, err, "ioc expired") + hash, _ := order.Hash() + assert.Equal(t, m.OrderHash, hash) + }) + + t.Run("valid order", func(t *testing.T) { + mockBibliophile := b.NewMockBibliophileClient(ctrl) + + order := orderbook.IOCOrder{ + OrderType: 1, + ExpireAt: big.NewInt(1001), + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(10), + Price: big.NewInt(100), + Salt: big.NewInt(1), + ReduceOnly: false, + }, + } + hash, _ := order.Hash() + ammAddress := common.Address{101} + mockBibliophile.EXPECT().GetTimeStamp().Return(uint64(1000)) + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(int64(0)).Return(ammAddress) + mockBibliophile.EXPECT().IOC_GetOrderFilledAmount(hash).Return(big.NewInt(0)) + mockBibliophile.EXPECT().IOC_GetOrderStatus(hash).Return(int64(1)) + mockBibliophile.EXPECT().IOC_GetBlockPlaced(hash).Return(big.NewInt(21)) + + m, err := validateExecuteIOCOrder(mockBibliophile, &order, Long, big.NewInt(10)) + assert.Nil(t, err) + assertMetadataEquality(t, &Metadata{ + AmmIndex: new(big.Int).Set(order.AmmIndex), + Trader: trader, + BaseAssetQuantity: new(big.Int).Set(order.BaseAssetQuantity), + BlockPlaced: big.NewInt(21), + Price: new(big.Int).Set(order.Price), + OrderHash: hash, + }, m) + }) + + t.Run("valid order - reduce only", func(t *testing.T) { + mockBibliophile := b.NewMockBibliophileClient(ctrl) + + order := orderbook.IOCOrder{ + OrderType: 1, + ExpireAt: big.NewInt(1001), + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(10), + Price: big.NewInt(100), + Salt: big.NewInt(1), + ReduceOnly: true, + }, + } + hash, _ := order.Hash() + ammAddress := common.Address{101} + mockBibliophile.EXPECT().GetTimeStamp().Return(uint64(1000)) + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(int64(0)).Return(ammAddress) + mockBibliophile.EXPECT().GetSize(ammAddress, &trader).Return(big.NewInt(-10)) + mockBibliophile.EXPECT().IOC_GetOrderFilledAmount(hash).Return(big.NewInt(0)) + mockBibliophile.EXPECT().IOC_GetOrderStatus(hash).Return(int64(1)) + mockBibliophile.EXPECT().IOC_GetBlockPlaced(hash).Return(big.NewInt(21)) + + _, err := validateExecuteIOCOrder(mockBibliophile, &order, Long, big.NewInt(10)) + assert.Nil(t, err) + }) +} diff --git a/precompile/contracts/juror/limit_orders.go b/precompile/contracts/juror/limit_orders.go new file mode 100644 index 0000000000..1e82d55fa9 --- /dev/null +++ b/precompile/contracts/juror/limit_orders.go @@ -0,0 +1,183 @@ +package juror + +import ( + "math/big" + + ob "github.com/ava-labs/subnet-evm/plugin/evm/orderbook" + hu "github.com/ava-labs/subnet-evm/plugin/evm/orderbook/hubbleutils" + b "github.com/ava-labs/subnet-evm/precompile/contracts/bibliophile" + "github.com/ethereum/go-ethereum/common" +) + +func ValidatePlaceLimitOrder(bibliophile b.BibliophileClient, inputStruct *ValidatePlaceLimitOrderInput) (response ValidatePlaceLimitOrderOutput) { + order := inputStruct.Order + sender := inputStruct.Sender + + response = ValidatePlaceLimitOrderOutput{Res: IOrderHandlerPlaceOrderRes{}} + response.Res.ReserveAmount = big.NewInt(0) + orderHash, err := GetLimitOrderHashFromContractStruct(&order) + response.Orderhash = orderHash + + if err != nil { + response.Err = err.Error() + return + } + if order.Price.Sign() != 1 { + response.Err = ErrInvalidPrice.Error() + return + } + trader := order.Trader + if trader != sender && !bibliophile.IsTradingAuthority(trader, sender) { + response.Err = ErrNoTradingAuthority.Error() + return + } + ammAddress := bibliophile.GetMarketAddressFromMarketID(order.AmmIndex.Int64()) + + if (ammAddress == common.Address{}) { + response.Err = ErrInvalidMarket.Error() + return + } + response.Res.Amm = ammAddress + if order.BaseAssetQuantity.Sign() == 0 { + response.Err = ErrBaseAssetQuantityZero.Error() + return + } + minSize := bibliophile.GetMinSizeRequirement(order.AmmIndex.Int64()) + if new(big.Int).Mod(order.BaseAssetQuantity, minSize).Sign() != 0 { + response.Err = ErrNotMultiple.Error() + return + } + status := OrderStatus(bibliophile.GetOrderStatus(orderHash)) + if status != Invalid { + response.Err = ErrOrderAlreadyExists.Error() + return + } + + posSize := bibliophile.GetSize(ammAddress, &trader) + reduceOnlyAmount := bibliophile.GetReduceOnlyAmount(trader, order.AmmIndex) + // this should only happen when a trader with open reduce only orders was liquidated + if (posSize.Sign() == 0 && reduceOnlyAmount.Sign() != 0) || (posSize.Sign() != 0 && new(big.Int).Mul(posSize, reduceOnlyAmount).Sign() == 1) { + // if position is non-zero then reduceOnlyAmount should be zero or have the opposite sign as posSize + response.Err = ErrStaleReduceOnlyOrders.Error() + return + } + + var orderSide Side = Side(Long) + if order.BaseAssetQuantity.Sign() == -1 { + orderSide = Side(Short) + } + if order.ReduceOnly { + // a reduce only order should reduce position + if !reducesPosition(posSize, order.BaseAssetQuantity) { + response.Err = ErrReduceOnlyBaseAssetQuantityInvalid.Error() + return + } + longOrdersAmount := bibliophile.GetLongOpenOrdersAmount(trader, order.AmmIndex) + shortOrdersAmount := bibliophile.GetShortOpenOrdersAmount(trader, order.AmmIndex) + + // if the trader is placing a reduceOnly long that means they have a short position + // we allow only 1 kind of order in the opposite direction of the position + // otherwise we run the risk of having stale reduceOnly orders (orders that are not actually reducing the position) + if (orderSide == Side(Long) && longOrdersAmount.Sign() != 0) || + (orderSide == Side(Short) && shortOrdersAmount.Sign() != 0) { + response.Err = ErrOpenOrders.Error() + return + } + if hu.Abs(hu.Add(reduceOnlyAmount, order.BaseAssetQuantity)).Cmp(hu.Abs(posSize)) == 1 { + response.Err = ErrNetReduceOnlyAmountExceeded.Error() + return + } + } else { + // we allow only 1 kind of order in the opposite direction of the position + if order.BaseAssetQuantity.Sign() != posSize.Sign() && reduceOnlyAmount.Sign() != 0 { + response.Err = ErrOpenReduceOnlyOrders.Error() + return + } + availableMargin := bibliophile.GetAvailableMargin(trader, hu.UpgradeVersionV0orV1(bibliophile.GetTimeStamp())) + requiredMargin := getRequiredMargin(bibliophile, order) + if availableMargin.Cmp(requiredMargin) == -1 { + response.Err = ErrInsufficientMargin.Error() + return + } + response.Res.ReserveAmount = requiredMargin + } + + if order.PostOnly { + asksHead := bibliophile.GetAsksHead(ammAddress) + bidsHead := bibliophile.GetBidsHead(ammAddress) + if (orderSide == Side(Short) && bidsHead.Sign() != 0 && order.Price.Cmp(bidsHead) != 1) || (orderSide == Side(Long) && asksHead.Sign() != 0 && order.Price.Cmp(asksHead) != -1) { + response.Err = ErrCrossingMarket.Error() + return + } + } + + if !bibliophile.HasReferrer(order.Trader) { + response.Err = ErrNoReferrer.Error() + } + + if hu.Mod(order.Price, bibliophile.GetPriceMultiplier(ammAddress)).Sign() != 0 { + response.Err = ErrPricePrecision.Error() + return + } + + return response +} + +func ValidateCancelLimitOrder(bibliophile b.BibliophileClient, inputStruct *ValidateCancelLimitOrderInput) (response ValidateCancelLimitOrderOutput) { + order := inputStruct.Order + sender := inputStruct.Sender + assertLowMargin := inputStruct.AssertLowMargin + + response.Res.UnfilledAmount = big.NewInt(0) + + trader := order.Trader + if (!assertLowMargin && trader != sender && !bibliophile.IsTradingAuthority(trader, sender)) || + (assertLowMargin && !bibliophile.IsValidator(sender)) { + response.Err = ErrNoTradingAuthority.Error() + return + } + orderHash, err := GetLimitOrderHashFromContractStruct(&order) + response.OrderHash = orderHash + if err != nil { + response.Err = err.Error() + return + } + switch status := OrderStatus(bibliophile.GetOrderStatus(orderHash)); status { + case Invalid: + response.Err = "Invalid" + return + case Filled: + response.Err = "Filled" + return + case Cancelled: + response.Err = "Cancelled" + return + default: + } + if assertLowMargin && bibliophile.GetAvailableMargin(trader, hu.UpgradeVersionV0orV1(bibliophile.GetTimeStamp())).Sign() != -1 { + response.Err = "Not Low Margin" + return + } + response.Res.UnfilledAmount = big.NewInt(0).Sub(order.BaseAssetQuantity, bibliophile.GetOrderFilledAmount(orderHash)) + response.Res.Amm = bibliophile.GetMarketAddressFromMarketID(order.AmmIndex.Int64()) + + return response +} + +func ILimitOrderBookOrderToLimitOrder(o *ILimitOrderBookOrder) *ob.LimitOrder { + return &ob.LimitOrder{ + BaseOrder: hu.BaseOrder{ + AmmIndex: o.AmmIndex, + Trader: o.Trader, + BaseAssetQuantity: o.BaseAssetQuantity, + Price: o.Price, + Salt: o.Salt, + ReduceOnly: o.ReduceOnly, + }, + PostOnly: o.PostOnly, + } +} + +func GetLimitOrderHashFromContractStruct(o *ILimitOrderBookOrder) (common.Hash, error) { + return ILimitOrderBookOrderToLimitOrder(o).Hash() +} diff --git a/precompile/contracts/juror/limit_orders_test.go b/precompile/contracts/juror/limit_orders_test.go new file mode 100644 index 0000000000..b4ed7e2d67 --- /dev/null +++ b/precompile/contracts/juror/limit_orders_test.go @@ -0,0 +1,1456 @@ +package juror + +import ( + "encoding/hex" + "math/big" + + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/assert" + + hu "github.com/ava-labs/subnet-evm/plugin/evm/orderbook/hubbleutils" + b "github.com/ava-labs/subnet-evm/precompile/contracts/bibliophile" + gomock "github.com/golang/mock/gomock" +) + +func TestValidatePlaceLimitOrder(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockBibliophile := b.NewMockBibliophileClient(ctrl) + ammIndex := big.NewInt(0) + longBaseAssetQuantity := big.NewInt(5000000000000000000) + shortBaseAssetQuantity := big.NewInt(-5000000000000000000) + price := big.NewInt(100000000) + salt := big.NewInt(121) + reduceOnly := false + postOnly := false + trader := common.HexToAddress("0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC") + ammAddress := common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8") + + t.Run("Basic Order checks", func(t *testing.T) { + t.Run("when baseAssetQuantity is 0", func(t *testing.T) { + newBaseAssetQuantity := big.NewInt(0) + order := getOrder(ammIndex, trader, newBaseAssetQuantity, price, salt, reduceOnly, postOnly) + + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(order.AmmIndex.Int64()).Return(ammAddress).Times(1) + output := ValidatePlaceLimitOrder(mockBibliophile, &ValidatePlaceLimitOrderInput{Order: order, Sender: trader}) + assert.Equal(t, ErrBaseAssetQuantityZero.Error(), output.Err) + expectedOrderHash, _ := GetLimitOrderHashFromContractStruct(&order) + assert.Equal(t, common.BytesToHash(output.Orderhash[:]), expectedOrderHash) + assert.Equal(t, output.Res.Amm, ammAddress) + assert.Equal(t, output.Res.ReserveAmount, big.NewInt(0)) + }) + t.Run("when baseAssetQuantity is not 0", func(t *testing.T) { + t.Run("when sender is not the trader and is not trading authority, it returns error", func(t *testing.T) { + sender := common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C9") + t.Run("it returns error for a long order", func(t *testing.T) { + order := getOrder(ammIndex, trader, longBaseAssetQuantity, price, salt, reduceOnly, postOnly) + mockBibliophile.EXPECT().IsTradingAuthority(order.Trader, sender).Return(false).Times(1) + output := ValidatePlaceLimitOrder(mockBibliophile, &ValidatePlaceLimitOrderInput{Order: order, Sender: sender}) + assert.Equal(t, "de9b5c2bf047cda53602c6a3223cd4b84b2b659f2ad6bc4b3fb29aed156185bd", hex.EncodeToString(output.Orderhash[:])) + assert.Equal(t, ErrNoTradingAuthority.Error(), output.Err) + }) + t.Run("it returns error for a short order", func(t *testing.T) { + order := getOrder(ammIndex, trader, shortBaseAssetQuantity, price, salt, reduceOnly, postOnly) + mockBibliophile.EXPECT().IsTradingAuthority(order.Trader, sender).Return(false).Times(1) + output := ValidatePlaceLimitOrder(mockBibliophile, &ValidatePlaceLimitOrderInput{Order: order, Sender: sender}) + // fmt.Println("Orderhash", hex.EncodeToString(output.Orderhash[:])) + assert.Equal(t, "8c9158cccd9795896fef87cc969deb425499f230ae9a4427d314f89ac76a0288", hex.EncodeToString(output.Orderhash[:])) + assert.Equal(t, ErrNoTradingAuthority.Error(), output.Err) + }) + }) + t.Run("when either sender is trader or a trading authority", func(t *testing.T) { + t.Run("when baseAssetQuantity is not a multiple of minSizeRequirement", func(t *testing.T) { + t.Run("when |baseAssetQuantity| is >0 but less than minSizeRequirement", func(t *testing.T) { + t.Run("it returns error for a long Order", func(t *testing.T) { + minSizeRequirement := big.NewInt(0).Add(longBaseAssetQuantity, big.NewInt(1)) + order := getOrder(ammIndex, trader, longBaseAssetQuantity, price, salt, reduceOnly, postOnly) + + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(order.AmmIndex.Int64()).Return(ammAddress).Times(1) + mockBibliophile.EXPECT().GetMinSizeRequirement(order.AmmIndex.Int64()).Return(minSizeRequirement).Times(1) + output := ValidatePlaceLimitOrder(mockBibliophile, &ValidatePlaceLimitOrderInput{Order: order, Sender: trader}) + assert.Equal(t, ErrNotMultiple.Error(), output.Err) + expectedOrderHash, _ := GetLimitOrderHashFromContractStruct(&order) + assert.Equal(t, common.BytesToHash(output.Orderhash[:]), expectedOrderHash) + assert.Equal(t, output.Res.Amm, ammAddress) + assert.Equal(t, output.Res.ReserveAmount, big.NewInt(0)) + }) + t.Run("it returns error for a short Order", func(t *testing.T) { + minSizeRequirement := big.NewInt(0).Sub(shortBaseAssetQuantity, big.NewInt(1)) + order := getOrder(ammIndex, trader, shortBaseAssetQuantity, price, salt, reduceOnly, postOnly) + + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(order.AmmIndex.Int64()).Return(ammAddress).Times(1) + mockBibliophile.EXPECT().GetMinSizeRequirement(order.AmmIndex.Int64()).Return(minSizeRequirement).Times(1) + output := ValidatePlaceLimitOrder(mockBibliophile, &ValidatePlaceLimitOrderInput{Order: order, Sender: trader}) + assert.Equal(t, ErrNotMultiple.Error(), output.Err) + expectedOrderHash, _ := GetLimitOrderHashFromContractStruct(&order) + assert.Equal(t, expectedOrderHash, common.BytesToHash(output.Orderhash[:])) + assert.Equal(t, ammAddress, output.Res.Amm) + assert.Equal(t, big.NewInt(0), output.Res.ReserveAmount) + }) + }) + t.Run("when |baseAssetQuantity| is > minSizeRequirement but not a multiple of minSizeRequirement", func(t *testing.T) { + t.Run("it returns error for a long Order", func(t *testing.T) { + minSizeRequirement := big.NewInt(0).Div(big.NewInt(0).Mul(longBaseAssetQuantity, big.NewInt(3)), big.NewInt(2)) + order := getOrder(ammIndex, trader, longBaseAssetQuantity, price, salt, reduceOnly, postOnly) + + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(order.AmmIndex.Int64()).Return(ammAddress).Times(1) + mockBibliophile.EXPECT().GetMinSizeRequirement(order.AmmIndex.Int64()).Return(minSizeRequirement).Times(1) + output := ValidatePlaceLimitOrder(mockBibliophile, &ValidatePlaceLimitOrderInput{Order: order, Sender: trader}) + assert.Equal(t, ErrNotMultiple.Error(), output.Err) + expectedOrderHash, _ := GetLimitOrderHashFromContractStruct(&order) + assert.Equal(t, expectedOrderHash, common.BytesToHash(output.Orderhash[:])) + assert.Equal(t, ammAddress, output.Res.Amm) + assert.Equal(t, big.NewInt(0), output.Res.ReserveAmount) + }) + t.Run("it returns error for a short Order", func(t *testing.T) { + minSizeRequirement := big.NewInt(0).Div(big.NewInt(0).Mul(longBaseAssetQuantity, big.NewInt(3)), big.NewInt(2)) + order := getOrder(ammIndex, trader, longBaseAssetQuantity, price, salt, reduceOnly, postOnly) + + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(order.AmmIndex.Int64()).Return(ammAddress).Times(1) + mockBibliophile.EXPECT().GetMinSizeRequirement(order.AmmIndex.Int64()).Return(minSizeRequirement).Times(1) + output := ValidatePlaceLimitOrder(mockBibliophile, &ValidatePlaceLimitOrderInput{Order: order, Sender: trader}) + assert.Equal(t, ErrNotMultiple.Error(), output.Err) + expectedOrderHash, _ := GetLimitOrderHashFromContractStruct(&order) + assert.Equal(t, expectedOrderHash, common.BytesToHash(output.Orderhash[:])) + assert.Equal(t, ammAddress, output.Res.Amm) + assert.Equal(t, big.NewInt(0), output.Res.ReserveAmount) + }) + }) + }) + t.Run("when baseAssetQuantity is a multiple of minSizeRequirement", func(t *testing.T) { + minSizeRequirement := big.NewInt(0).Div(longBaseAssetQuantity, big.NewInt(2)) + + t.Run("when order was placed earlier", func(t *testing.T) { + t.Run("when order status is placed", func(t *testing.T) { + t.Run("it returns error for a longOrder", func(t *testing.T) { + longOrder := getOrder(ammIndex, trader, longBaseAssetQuantity, price, salt, reduceOnly, postOnly) + + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(longOrder.AmmIndex.Int64()).Return(ammAddress).Times(1) + mockBibliophile.EXPECT().GetMinSizeRequirement(longOrder.AmmIndex.Int64()).Return(minSizeRequirement).Times(1) + orderHash, err := GetLimitOrderHashFromContractStruct(&longOrder) + if err != nil { + panic("error in getting longOrder hash") + } + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Placed)).Times(1) + output := ValidatePlaceLimitOrder(mockBibliophile, &ValidatePlaceLimitOrderInput{Order: longOrder, Sender: trader}) + assert.Equal(t, ErrOrderAlreadyExists.Error(), output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.Orderhash[:])) + assert.Equal(t, ammAddress, output.Res.Amm) + assert.Equal(t, big.NewInt(0), output.Res.ReserveAmount) + }) + t.Run("it returns error for a shortOrder", func(t *testing.T) { + shortOrder := getOrder(ammIndex, trader, shortBaseAssetQuantity, price, salt, reduceOnly, postOnly) + + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(shortOrder.AmmIndex.Int64()).Return(ammAddress).Times(1) + mockBibliophile.EXPECT().GetMinSizeRequirement(shortOrder.AmmIndex.Int64()).Return(minSizeRequirement).Times(1) + orderHash, err := GetLimitOrderHashFromContractStruct(&shortOrder) + if err != nil { + panic("error in getting shortOrder hash") + } + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Placed)).Times(1) + output := ValidatePlaceLimitOrder(mockBibliophile, &ValidatePlaceLimitOrderInput{Order: shortOrder, Sender: trader}) + assert.Equal(t, ErrOrderAlreadyExists.Error(), output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.Orderhash[:])) + assert.Equal(t, ammAddress, output.Res.Amm) + assert.Equal(t, big.NewInt(0), output.Res.ReserveAmount) + }) + }) + t.Run("when order status is filled", func(t *testing.T) { + t.Run("it returns error for a longOrder", func(t *testing.T) { + longOrder := getOrder(ammIndex, trader, longBaseAssetQuantity, price, salt, reduceOnly, postOnly) + + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(longOrder.AmmIndex.Int64()).Return(ammAddress).Times(1) + mockBibliophile.EXPECT().GetMinSizeRequirement(longOrder.AmmIndex.Int64()).Return(minSizeRequirement).Times(1) + orderHash, err := GetLimitOrderHashFromContractStruct(&longOrder) + if err != nil { + panic("error in getting longOrder hash") + } + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Filled)).Times(1) + output := ValidatePlaceLimitOrder(mockBibliophile, &ValidatePlaceLimitOrderInput{Order: longOrder, Sender: trader}) + assert.Equal(t, ErrOrderAlreadyExists.Error(), output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.Orderhash[:])) + assert.Equal(t, ammAddress, output.Res.Amm) + assert.Equal(t, big.NewInt(0), output.Res.ReserveAmount) + }) + t.Run("it returns error for a shortOrder", func(t *testing.T) { + shortOrder := getOrder(ammIndex, trader, shortBaseAssetQuantity, price, salt, reduceOnly, postOnly) + + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(shortOrder.AmmIndex.Int64()).Return(ammAddress).Times(1) + mockBibliophile.EXPECT().GetMinSizeRequirement(shortOrder.AmmIndex.Int64()).Return(minSizeRequirement).Times(1) + orderHash, err := GetLimitOrderHashFromContractStruct(&shortOrder) + if err != nil { + panic("error in getting shortOrder hash") + } + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Filled)).Times(1) + output := ValidatePlaceLimitOrder(mockBibliophile, &ValidatePlaceLimitOrderInput{Order: shortOrder, Sender: trader}) + assert.Equal(t, ErrOrderAlreadyExists.Error(), output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.Orderhash[:])) + assert.Equal(t, ammAddress, output.Res.Amm) + assert.Equal(t, big.NewInt(0), output.Res.ReserveAmount) + }) + }) + t.Run("when order status is cancelled", func(t *testing.T) { + t.Run("it returns error for a longOrder", func(t *testing.T) { + longOrder := getOrder(ammIndex, trader, longBaseAssetQuantity, price, salt, reduceOnly, postOnly) + + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(longOrder.AmmIndex.Int64()).Return(ammAddress).Times(1) + mockBibliophile.EXPECT().GetMinSizeRequirement(longOrder.AmmIndex.Int64()).Return(minSizeRequirement).Times(1) + orderHash, err := GetLimitOrderHashFromContractStruct(&longOrder) + if err != nil { + panic("error in getting longOrder hash") + } + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Cancelled)).Times(1) + output := ValidatePlaceLimitOrder(mockBibliophile, &ValidatePlaceLimitOrderInput{Order: longOrder, Sender: trader}) + assert.Equal(t, ErrOrderAlreadyExists.Error(), output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.Orderhash[:])) + assert.Equal(t, ammAddress, output.Res.Amm) + assert.Equal(t, big.NewInt(0), output.Res.ReserveAmount) + }) + t.Run("it returns error for a shortOrder", func(t *testing.T) { + shortOrder := getOrder(ammIndex, trader, shortBaseAssetQuantity, price, salt, reduceOnly, postOnly) + + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(shortOrder.AmmIndex.Int64()).Return(ammAddress).Times(1) + mockBibliophile.EXPECT().GetMinSizeRequirement(shortOrder.AmmIndex.Int64()).Return(minSizeRequirement).Times(1) + orderHash, err := GetLimitOrderHashFromContractStruct(&shortOrder) + if err != nil { + panic("error in getting shortOrder hash") + } + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Cancelled)).Times(1) + output := ValidatePlaceLimitOrder(mockBibliophile, &ValidatePlaceLimitOrderInput{Order: shortOrder, Sender: trader}) + assert.Equal(t, ErrOrderAlreadyExists.Error(), output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.Orderhash[:])) + assert.Equal(t, ammAddress, output.Res.Amm) + assert.Equal(t, big.NewInt(0), output.Res.ReserveAmount) + }) + }) + }) + }) + }) + }) + }) + t.Run("When basic order validations pass", func(t *testing.T) { + minSizeRequirement := big.NewInt(0).Div(longBaseAssetQuantity, big.NewInt(2)) + t.Run("When order is reduceOnly order", func(t *testing.T) { + t.Run("When reduceOnly does not reduce position", func(t *testing.T) { + t.Run("when trader has longPosition", func(t *testing.T) { + t.Run("it returns error when order is longOrder", func(t *testing.T) { + positionSize := longBaseAssetQuantity + longOrder := getOrder(ammIndex, trader, longBaseAssetQuantity, price, salt, true, postOnly) + + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(longOrder.AmmIndex.Int64()).Return(ammAddress).Times(1) + mockBibliophile.EXPECT().GetMinSizeRequirement(longOrder.AmmIndex.Int64()).Return(minSizeRequirement).Times(1) + orderHash, err := GetLimitOrderHashFromContractStruct(&longOrder) + if err != nil { + panic("error in getting longOrder hash") + } + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Invalid)).Times(1) + mockBibliophile.EXPECT().GetSize(ammAddress, &trader).Return(positionSize).Times(1) + mockBibliophile.EXPECT().GetReduceOnlyAmount(trader, longOrder.AmmIndex).Return(big.NewInt(0)).Times(1) + output := ValidatePlaceLimitOrder(mockBibliophile, &ValidatePlaceLimitOrderInput{Order: longOrder, Sender: trader}) + assert.Equal(t, ErrReduceOnlyBaseAssetQuantityInvalid.Error(), output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.Orderhash[:])) + assert.Equal(t, ammAddress, output.Res.Amm) + assert.Equal(t, big.NewInt(0), output.Res.ReserveAmount) + }) + t.Run("it returns error when order is shortOrder and |baseAssetQuantity| > |positionSize|", func(t *testing.T) { + positionSize := big.NewInt(0).Abs(big.NewInt(0).Add(shortBaseAssetQuantity, big.NewInt(1))) + shortOrder := getOrder(ammIndex, trader, shortBaseAssetQuantity, price, salt, true, postOnly) + + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(shortOrder.AmmIndex.Int64()).Return(ammAddress).Times(1) + mockBibliophile.EXPECT().GetMinSizeRequirement(shortOrder.AmmIndex.Int64()).Return(minSizeRequirement).Times(1) + orderHash, err := GetLimitOrderHashFromContractStruct(&shortOrder) + if err != nil { + panic("error in getting shortOrder hash") + } + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Invalid)).Times(1) + mockBibliophile.EXPECT().GetSize(ammAddress, &trader).Return(positionSize).Times(1) + mockBibliophile.EXPECT().GetReduceOnlyAmount(trader, shortOrder.AmmIndex).Return(big.NewInt(0)).Times(1) + output := ValidatePlaceLimitOrder(mockBibliophile, &ValidatePlaceLimitOrderInput{Order: shortOrder, Sender: trader}) + assert.Equal(t, ErrReduceOnlyBaseAssetQuantityInvalid.Error(), output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.Orderhash[:])) + assert.Equal(t, ammAddress, output.Res.Amm) + assert.Equal(t, big.NewInt(0), output.Res.ReserveAmount) + }) + }) + t.Run("when trader has shortPosition", func(t *testing.T) { + t.Run("it returns when order is shortOrder", func(t *testing.T) { + positionSize := shortBaseAssetQuantity + shortOrder := getOrder(ammIndex, trader, shortBaseAssetQuantity, price, salt, true, postOnly) + + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(shortOrder.AmmIndex.Int64()).Return(ammAddress).Times(1) + mockBibliophile.EXPECT().GetMinSizeRequirement(shortOrder.AmmIndex.Int64()).Return(minSizeRequirement).Times(1) + orderHash, err := GetLimitOrderHashFromContractStruct(&shortOrder) + if err != nil { + panic("error in getting shortOrder hash") + } + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Invalid)).Times(1) + mockBibliophile.EXPECT().GetSize(ammAddress, &trader).Return(positionSize).Times(1) + mockBibliophile.EXPECT().GetReduceOnlyAmount(trader, shortOrder.AmmIndex).Return(big.NewInt(0)).Times(1) + output := ValidatePlaceLimitOrder(mockBibliophile, &ValidatePlaceLimitOrderInput{Order: shortOrder, Sender: trader}) + assert.Equal(t, ErrReduceOnlyBaseAssetQuantityInvalid.Error(), output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.Orderhash[:])) + assert.Equal(t, ammAddress, output.Res.Amm) + assert.Equal(t, big.NewInt(0), output.Res.ReserveAmount) + }) + t.Run("it returns error when order is longOrder and |baseAssetQuantity| > |positionSize|", func(t *testing.T) { + positionSize := big.NewInt(0).Sub(longBaseAssetQuantity, big.NewInt(1)) + longOrder := getOrder(ammIndex, trader, longBaseAssetQuantity, price, salt, true, postOnly) + + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(longOrder.AmmIndex.Int64()).Return(ammAddress).Times(1) + mockBibliophile.EXPECT().GetMinSizeRequirement(longOrder.AmmIndex.Int64()).Return(minSizeRequirement).Times(1) + orderHash, err := GetLimitOrderHashFromContractStruct(&longOrder) + if err != nil { + panic("error in getting longOrder hash") + } + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Invalid)).Times(1) + mockBibliophile.EXPECT().GetSize(ammAddress, &trader).Return(positionSize).Times(1) + mockBibliophile.EXPECT().GetReduceOnlyAmount(trader, longOrder.AmmIndex).Return(big.NewInt(0)).Times(1) + output := ValidatePlaceLimitOrder(mockBibliophile, &ValidatePlaceLimitOrderInput{Order: longOrder, Sender: trader}) + assert.Equal(t, ErrReduceOnlyBaseAssetQuantityInvalid.Error(), output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.Orderhash[:])) + assert.Equal(t, ammAddress, output.Res.Amm) + assert.Equal(t, big.NewInt(0), output.Res.ReserveAmount) + }) + }) + }) + t.Run("When reduceOnly reduces position", func(t *testing.T) { + t.Run("when there are non reduceOnly Orders in same direction", func(t *testing.T) { + t.Run("for a short position", func(t *testing.T) { + t.Run("it returns error if order is longOrder and there are open longOrders which are not reduceOnly", func(t *testing.T) { + positionSize := shortBaseAssetQuantity + longOrder := getOrder(ammIndex, trader, longBaseAssetQuantity, price, salt, true, postOnly) + + longOpenOrdersAmount := big.NewInt(0).Div(positionSize, big.NewInt(4)) + + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(longOrder.AmmIndex.Int64()).Return(ammAddress).Times(1) + mockBibliophile.EXPECT().GetMinSizeRequirement(longOrder.AmmIndex.Int64()).Return(minSizeRequirement).Times(1) + orderHash, err := GetLimitOrderHashFromContractStruct(&longOrder) + if err != nil { + panic("error in getting longOrder hash") + } + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Invalid)).Times(1) + mockBibliophile.EXPECT().GetSize(ammAddress, &trader).Return(positionSize).Times(1) + mockBibliophile.EXPECT().GetReduceOnlyAmount(trader, longOrder.AmmIndex).Return(big.NewInt(0)).Times(1) + mockBibliophile.EXPECT().GetLongOpenOrdersAmount(trader, longOrder.AmmIndex).Return(longOpenOrdersAmount).Times(1) + mockBibliophile.EXPECT().GetShortOpenOrdersAmount(trader, longOrder.AmmIndex).Return(big.NewInt(0)).Times(1) + output := ValidatePlaceLimitOrder(mockBibliophile, &ValidatePlaceLimitOrderInput{Order: longOrder, Sender: trader}) + assert.Equal(t, ErrOpenOrders.Error(), output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.Orderhash[:])) + assert.Equal(t, ammAddress, output.Res.Amm) + assert.Equal(t, big.NewInt(0), output.Res.ReserveAmount) + }) + }) + t.Run("for a long position", func(t *testing.T) { + t.Run("it returns error if order is shortOrder and there are open shortOrders which are not reduceOnly", func(t *testing.T) { + positionSize := longBaseAssetQuantity + shortOrder := getOrder(ammIndex, trader, shortBaseAssetQuantity, price, salt, true, postOnly) + + shortOpenOrdersAmount := big.NewInt(0).Div(longBaseAssetQuantity, big.NewInt(4)) + + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(shortOrder.AmmIndex.Int64()).Return(ammAddress).Times(1) + mockBibliophile.EXPECT().GetMinSizeRequirement(shortOrder.AmmIndex.Int64()).Return(minSizeRequirement).Times(1) + orderHash, err := GetLimitOrderHashFromContractStruct(&shortOrder) + if err != nil { + panic("error in getting shortOrder hash") + } + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Invalid)).Times(1) + mockBibliophile.EXPECT().GetSize(ammAddress, &trader).Return(positionSize).Times(1) + mockBibliophile.EXPECT().GetReduceOnlyAmount(trader, shortOrder.AmmIndex).Return(big.NewInt(0)).Times(1) + mockBibliophile.EXPECT().GetLongOpenOrdersAmount(trader, shortOrder.AmmIndex).Return(big.NewInt(0)).Times(1) + mockBibliophile.EXPECT().GetShortOpenOrdersAmount(trader, shortOrder.AmmIndex).Return(shortOpenOrdersAmount).Times(1) + output := ValidatePlaceLimitOrder(mockBibliophile, &ValidatePlaceLimitOrderInput{Order: shortOrder, Sender: trader}) + assert.Equal(t, ErrOpenOrders.Error(), output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.Orderhash[:])) + assert.Equal(t, ammAddress, output.Res.Amm) + assert.Equal(t, big.NewInt(0), output.Res.ReserveAmount) + }) + }) + }) + t.Run("when there are no non reduceOnly orders in same direction", func(t *testing.T) { + t.Run("when current open reduceOnlyOrders plus currentOrder's baseAssetQuantity exceeds positionSize", func(t *testing.T) { + t.Run("it returns error for a longOrder", func(t *testing.T) { + positionSize := shortBaseAssetQuantity + longOrder := getOrder(ammIndex, trader, longBaseAssetQuantity, price, salt, true, postOnly) + + reduceOnlyAmount := big.NewInt(1) + + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(longOrder.AmmIndex.Int64()).Return(ammAddress).Times(1) + mockBibliophile.EXPECT().GetMinSizeRequirement(longOrder.AmmIndex.Int64()).Return(minSizeRequirement).Times(1) + orderHash, err := GetLimitOrderHashFromContractStruct(&longOrder) + if err != nil { + panic("error in getting longOrder hash") + } + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Invalid)).Times(1) + mockBibliophile.EXPECT().GetSize(ammAddress, &trader).Return(positionSize).Times(1) + mockBibliophile.EXPECT().GetReduceOnlyAmount(trader, longOrder.AmmIndex).Return(reduceOnlyAmount).Times(1) + mockBibliophile.EXPECT().GetLongOpenOrdersAmount(trader, longOrder.AmmIndex).Return(big.NewInt(0)).Times(1) + mockBibliophile.EXPECT().GetShortOpenOrdersAmount(trader, longOrder.AmmIndex).Return(big.NewInt(0)).Times(1) + output := ValidatePlaceLimitOrder(mockBibliophile, &ValidatePlaceLimitOrderInput{Order: longOrder, Sender: trader}) + assert.Equal(t, ErrNetReduceOnlyAmountExceeded.Error(), output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.Orderhash[:])) + assert.Equal(t, ammAddress, output.Res.Amm) + assert.Equal(t, big.NewInt(0), output.Res.ReserveAmount) + }) + t.Run("it returns error for a shortOrder", func(t *testing.T) { + positionSize := longBaseAssetQuantity + shortOrder := getOrder(ammIndex, trader, shortBaseAssetQuantity, price, salt, true, postOnly) + + reduceOnlyAmount := big.NewInt(-1) + + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(shortOrder.AmmIndex.Int64()).Return(ammAddress).Times(1) + mockBibliophile.EXPECT().GetMinSizeRequirement(shortOrder.AmmIndex.Int64()).Return(minSizeRequirement).Times(1) + orderHash, err := GetLimitOrderHashFromContractStruct(&shortOrder) + if err != nil { + panic("error in getting shortOrder hash") + } + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Invalid)).Times(1) + mockBibliophile.EXPECT().GetSize(ammAddress, &trader).Return(positionSize).Times(1) + mockBibliophile.EXPECT().GetReduceOnlyAmount(trader, shortOrder.AmmIndex).Return(reduceOnlyAmount).Times(1) + mockBibliophile.EXPECT().GetLongOpenOrdersAmount(trader, shortOrder.AmmIndex).Return(big.NewInt(0)).Times(1) + mockBibliophile.EXPECT().GetShortOpenOrdersAmount(trader, shortOrder.AmmIndex).Return(big.NewInt(0)).Times(1) + output := ValidatePlaceLimitOrder(mockBibliophile, &ValidatePlaceLimitOrderInput{Order: shortOrder, Sender: trader}) + assert.Equal(t, ErrNetReduceOnlyAmountExceeded.Error(), output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.Orderhash[:])) + assert.Equal(t, ammAddress, output.Res.Amm) + assert.Equal(t, big.NewInt(0), output.Res.ReserveAmount) + }) + }) + t.Run("when current open reduceOnlyOrders plus currentOrder's baseAssetQuantity <= positionSize", func(t *testing.T) { + t.Run("when order is not postOnly order", func(t *testing.T) { + t.Run("for a longOrder it returns no error and 0 as reserveAmount", func(t *testing.T) { + positionSize := big.NewInt(0).Mul(longBaseAssetQuantity, big.NewInt(-2)) + longOrder := getOrder(ammIndex, trader, longBaseAssetQuantity, price, salt, true, postOnly) + + reduceOnlyAmount := big.NewInt(1) + + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(longOrder.AmmIndex.Int64()).Return(ammAddress).Times(1) + mockBibliophile.EXPECT().GetMinSizeRequirement(longOrder.AmmIndex.Int64()).Return(minSizeRequirement).Times(1) + orderHash, err := GetLimitOrderHashFromContractStruct(&longOrder) + if err != nil { + panic("error in getting longOrder hash") + } + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Invalid)).Times(1) + mockBibliophile.EXPECT().GetSize(ammAddress, &trader).Return(positionSize).Times(1) + mockBibliophile.EXPECT().GetReduceOnlyAmount(trader, longOrder.AmmIndex).Return(reduceOnlyAmount).Times(1) + mockBibliophile.EXPECT().GetLongOpenOrdersAmount(trader, longOrder.AmmIndex).Return(big.NewInt(0)).Times(1) + mockBibliophile.EXPECT().GetShortOpenOrdersAmount(trader, longOrder.AmmIndex).Return(big.NewInt(0)).Times(1) + mockBibliophile.EXPECT().HasReferrer(trader).Return(true).Times(1) + mockBibliophile.EXPECT().GetPriceMultiplier(ammAddress).Return(big.NewInt(1)).Times(1) + output := ValidatePlaceLimitOrder(mockBibliophile, &ValidatePlaceLimitOrderInput{Order: longOrder, Sender: trader}) + assert.Equal(t, "", output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.Orderhash[:])) + assert.Equal(t, ammAddress, output.Res.Amm) + assert.Equal(t, big.NewInt(0), output.Res.ReserveAmount) + }) + t.Run("for a shortOrder it returns no error and 0 as reserveAmount", func(t *testing.T) { + positionSize := big.NewInt(0).Mul(shortBaseAssetQuantity, big.NewInt(-2)) + shortOrder := getOrder(ammIndex, trader, shortBaseAssetQuantity, price, salt, true, postOnly) + + reduceOnlyAmount := big.NewInt(-1) + + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(shortOrder.AmmIndex.Int64()).Return(ammAddress).Times(1) + mockBibliophile.EXPECT().GetMinSizeRequirement(shortOrder.AmmIndex.Int64()).Return(minSizeRequirement).Times(1) + orderHash, err := GetLimitOrderHashFromContractStruct(&shortOrder) + if err != nil { + panic("error in getting shortOrder hash") + } + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Invalid)).Times(1) + mockBibliophile.EXPECT().GetSize(ammAddress, &trader).Return(positionSize).Times(1) + mockBibliophile.EXPECT().GetReduceOnlyAmount(trader, shortOrder.AmmIndex).Return(reduceOnlyAmount).Times(1) + mockBibliophile.EXPECT().GetLongOpenOrdersAmount(trader, shortOrder.AmmIndex).Return(big.NewInt(0)).Times(1) + mockBibliophile.EXPECT().GetShortOpenOrdersAmount(trader, shortOrder.AmmIndex).Return(big.NewInt(0)).Times(1) + mockBibliophile.EXPECT().HasReferrer(trader).Return(true).Times(1) + mockBibliophile.EXPECT().GetPriceMultiplier(ammAddress).Return(big.NewInt(1)).Times(1) + output := ValidatePlaceLimitOrder(mockBibliophile, &ValidatePlaceLimitOrderInput{Order: shortOrder, Sender: trader}) + assert.Equal(t, "", output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.Orderhash[:])) + assert.Equal(t, ammAddress, output.Res.Amm) + assert.Equal(t, big.NewInt(0), output.Res.ReserveAmount) + }) + }) + t.Run("when order is postOnly order", func(t *testing.T) { + asksHead := big.NewInt(0).Sub(price, big.NewInt(1)) + bidsHead := big.NewInt(0).Add(price, big.NewInt(1)) + t.Run("when order crosses market", func(t *testing.T) { + t.Run("it returns error if longOrder's price >= asksHead", func(t *testing.T) { + positionSize := big.NewInt(0).Mul(longBaseAssetQuantity, big.NewInt(-1)) + reduceOnlyAmount := big.NewInt(0) + + t.Run("it returns error if longOrder's price = asksHead", func(t *testing.T) { + longPrice := big.NewInt(0).Set(asksHead) + longOrder := getOrder(ammIndex, trader, longBaseAssetQuantity, longPrice, salt, true, true) + + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(longOrder.AmmIndex.Int64()).Return(ammAddress).Times(1) + mockBibliophile.EXPECT().GetMinSizeRequirement(longOrder.AmmIndex.Int64()).Return(minSizeRequirement).Times(1) + orderHash, err := GetLimitOrderHashFromContractStruct(&longOrder) + if err != nil { + panic("error in getting longOrder hash") + } + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Invalid)).Times(1) + mockBibliophile.EXPECT().GetSize(ammAddress, &trader).Return(positionSize).Times(1) + mockBibliophile.EXPECT().GetReduceOnlyAmount(trader, longOrder.AmmIndex).Return(reduceOnlyAmount).Times(1) + mockBibliophile.EXPECT().GetLongOpenOrdersAmount(trader, longOrder.AmmIndex).Return(big.NewInt(0)).Times(1) + mockBibliophile.EXPECT().GetShortOpenOrdersAmount(trader, longOrder.AmmIndex).Return(big.NewInt(0)).Times(1) + mockBibliophile.EXPECT().GetAsksHead(ammAddress).Return(asksHead).Times(1) + mockBibliophile.EXPECT().GetBidsHead(ammAddress).Return(bidsHead).Times(1) + output := ValidatePlaceLimitOrder(mockBibliophile, &ValidatePlaceLimitOrderInput{Order: longOrder, Sender: trader}) + assert.Equal(t, ErrCrossingMarket.Error(), output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.Orderhash[:])) + assert.Equal(t, ammAddress, output.Res.Amm) + assert.Equal(t, big.NewInt(0), output.Res.ReserveAmount) + }) + t.Run("it returns error if longOrder's price > asksHead", func(t *testing.T) { + longPrice := big.NewInt(0).Add(asksHead, big.NewInt(1)) + longOrder := getOrder(ammIndex, trader, longBaseAssetQuantity, longPrice, salt, true, true) + + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(longOrder.AmmIndex.Int64()).Return(ammAddress).Times(1) + mockBibliophile.EXPECT().GetMinSizeRequirement(longOrder.AmmIndex.Int64()).Return(minSizeRequirement).Times(1) + orderHash, err := GetLimitOrderHashFromContractStruct(&longOrder) + if err != nil { + panic("error in getting longOrder hash") + } + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Invalid)).Times(1) + mockBibliophile.EXPECT().GetSize(ammAddress, &trader).Return(positionSize).Times(1) + mockBibliophile.EXPECT().GetReduceOnlyAmount(trader, longOrder.AmmIndex).Return(reduceOnlyAmount).Times(1) + mockBibliophile.EXPECT().GetLongOpenOrdersAmount(trader, longOrder.AmmIndex).Return(big.NewInt(0)).Times(1) + mockBibliophile.EXPECT().GetShortOpenOrdersAmount(trader, longOrder.AmmIndex).Return(big.NewInt(0)).Times(1) + mockBibliophile.EXPECT().GetAsksHead(ammAddress).Return(asksHead).Times(1) + mockBibliophile.EXPECT().GetBidsHead(ammAddress).Return(bidsHead).Times(1) + output := ValidatePlaceLimitOrder(mockBibliophile, &ValidatePlaceLimitOrderInput{Order: longOrder, Sender: trader}) + assert.Equal(t, ErrCrossingMarket.Error(), output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.Orderhash[:])) + assert.Equal(t, ammAddress, output.Res.Amm) + assert.Equal(t, big.NewInt(0), output.Res.ReserveAmount) + }) + }) + t.Run("it returns error if shortOrder's price <= bidsHead", func(t *testing.T) { + positionSize := big.NewInt(0).Mul(shortBaseAssetQuantity, big.NewInt(-1)) + reduceOnlyAmount := big.NewInt(0) + + t.Run("it returns error if shortOrder price = asksHead", func(t *testing.T) { + shortOrderPrice := big.NewInt(0).Set(bidsHead) + shortOrder := getOrder(ammIndex, trader, shortBaseAssetQuantity, shortOrderPrice, salt, true, true) + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(shortOrder.AmmIndex.Int64()).Return(ammAddress).Times(1) + mockBibliophile.EXPECT().GetMinSizeRequirement(shortOrder.AmmIndex.Int64()).Return(minSizeRequirement).Times(1) + orderHash, err := GetLimitOrderHashFromContractStruct(&shortOrder) + if err != nil { + panic("error in getting shortOrder hash") + } + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Invalid)).Times(1) + mockBibliophile.EXPECT().GetSize(ammAddress, &trader).Return(positionSize).Times(1) + mockBibliophile.EXPECT().GetReduceOnlyAmount(trader, shortOrder.AmmIndex).Return(reduceOnlyAmount).Times(1) + mockBibliophile.EXPECT().GetLongOpenOrdersAmount(trader, shortOrder.AmmIndex).Return(big.NewInt(0)).Times(1) + mockBibliophile.EXPECT().GetShortOpenOrdersAmount(trader, shortOrder.AmmIndex).Return(big.NewInt(0)).Times(1) + mockBibliophile.EXPECT().GetAsksHead(ammAddress).Return(asksHead).Times(1) + mockBibliophile.EXPECT().GetBidsHead(ammAddress).Return(bidsHead).Times(1) + + output := ValidatePlaceLimitOrder(mockBibliophile, &ValidatePlaceLimitOrderInput{Order: shortOrder, Sender: trader}) + assert.Equal(t, ErrCrossingMarket.Error(), output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.Orderhash[:])) + assert.Equal(t, ammAddress, output.Res.Amm) + assert.Equal(t, big.NewInt(0), output.Res.ReserveAmount) + }) + t.Run("it returns error if shortOrder price < asksHead", func(t *testing.T) { + shortOrderPrice := big.NewInt(0).Sub(bidsHead, big.NewInt(1)) + shortOrder := getOrder(ammIndex, trader, shortBaseAssetQuantity, shortOrderPrice, salt, true, true) + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(shortOrder.AmmIndex.Int64()).Return(ammAddress).Times(1) + mockBibliophile.EXPECT().GetMinSizeRequirement(shortOrder.AmmIndex.Int64()).Return(minSizeRequirement).Times(1) + orderHash, err := GetLimitOrderHashFromContractStruct(&shortOrder) + if err != nil { + panic("error in getting shortOrder hash") + } + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Invalid)).Times(1) + mockBibliophile.EXPECT().GetSize(ammAddress, &trader).Return(positionSize).Times(1) + mockBibliophile.EXPECT().GetReduceOnlyAmount(trader, shortOrder.AmmIndex).Return(reduceOnlyAmount).Times(1) + mockBibliophile.EXPECT().GetLongOpenOrdersAmount(trader, shortOrder.AmmIndex).Return(big.NewInt(0)).Times(1) + mockBibliophile.EXPECT().GetShortOpenOrdersAmount(trader, shortOrder.AmmIndex).Return(big.NewInt(0)).Times(1) + mockBibliophile.EXPECT().GetAsksHead(ammAddress).Return(asksHead).Times(1) + mockBibliophile.EXPECT().GetBidsHead(ammAddress).Return(bidsHead).Times(1) + + output := ValidatePlaceLimitOrder(mockBibliophile, &ValidatePlaceLimitOrderInput{Order: shortOrder, Sender: trader}) + assert.Equal(t, ErrCrossingMarket.Error(), output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.Orderhash[:])) + assert.Equal(t, ammAddress, output.Res.Amm) + assert.Equal(t, big.NewInt(0), output.Res.ReserveAmount) + }) + }) + }) + t.Run("when order does not cross market", func(t *testing.T) { + t.Run("for a longOrder it returns no error and 0 as reserveAmount", func(t *testing.T) { + positionSize := big.NewInt(0).Mul(longBaseAssetQuantity, big.NewInt(-1)) + reduceOnlyAmount := big.NewInt(0) + + longPrice := big.NewInt(0).Sub(asksHead, big.NewInt(1)) + longOrder := getOrder(ammIndex, trader, longBaseAssetQuantity, longPrice, salt, true, true) + + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(longOrder.AmmIndex.Int64()).Return(ammAddress).Times(1) + mockBibliophile.EXPECT().GetMinSizeRequirement(longOrder.AmmIndex.Int64()).Return(minSizeRequirement).Times(1) + orderHash, err := GetLimitOrderHashFromContractStruct(&longOrder) + if err != nil { + panic("error in getting longOrder hash") + } + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Invalid)).Times(1) + mockBibliophile.EXPECT().GetSize(ammAddress, &trader).Return(positionSize).Times(1) + mockBibliophile.EXPECT().GetReduceOnlyAmount(trader, longOrder.AmmIndex).Return(reduceOnlyAmount).Times(1) + mockBibliophile.EXPECT().GetLongOpenOrdersAmount(trader, longOrder.AmmIndex).Return(big.NewInt(0)).Times(1) + mockBibliophile.EXPECT().GetShortOpenOrdersAmount(trader, longOrder.AmmIndex).Return(big.NewInt(0)).Times(1) + mockBibliophile.EXPECT().GetAsksHead(ammAddress).Return(asksHead).Times(1) + mockBibliophile.EXPECT().GetBidsHead(ammAddress).Return(bidsHead).Times(1) + mockBibliophile.EXPECT().HasReferrer(trader).Return(true).Times(1) + mockBibliophile.EXPECT().GetPriceMultiplier(ammAddress).Return(big.NewInt(1)).Times(1) + output := ValidatePlaceLimitOrder(mockBibliophile, &ValidatePlaceLimitOrderInput{Order: longOrder, Sender: trader}) + assert.Equal(t, "", output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.Orderhash[:])) + assert.Equal(t, ammAddress, output.Res.Amm) + assert.Equal(t, big.NewInt(0), output.Res.ReserveAmount) + }) + t.Run("for a shortOrder it returns no error and 0 as reserveAmount", func(t *testing.T) { + positionSize := big.NewInt(0).Mul(shortBaseAssetQuantity, big.NewInt(-1)) + reduceOnlyAmount := big.NewInt(0) + + shortOrderPrice := big.NewInt(0).Add(bidsHead, big.NewInt(1)) + shortOrder := getOrder(ammIndex, trader, shortBaseAssetQuantity, shortOrderPrice, salt, true, true) + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(shortOrder.AmmIndex.Int64()).Return(ammAddress).Times(1) + mockBibliophile.EXPECT().GetMinSizeRequirement(shortOrder.AmmIndex.Int64()).Return(minSizeRequirement).Times(1) + orderHash, err := GetLimitOrderHashFromContractStruct(&shortOrder) + if err != nil { + panic("error in getting shortOrder hash") + } + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Invalid)).Times(1) + mockBibliophile.EXPECT().GetSize(ammAddress, &trader).Return(positionSize).Times(1) + mockBibliophile.EXPECT().GetReduceOnlyAmount(trader, shortOrder.AmmIndex).Return(reduceOnlyAmount).Times(1) + mockBibliophile.EXPECT().GetLongOpenOrdersAmount(trader, shortOrder.AmmIndex).Return(big.NewInt(0)).Times(1) + mockBibliophile.EXPECT().GetShortOpenOrdersAmount(trader, shortOrder.AmmIndex).Return(big.NewInt(0)).Times(1) + mockBibliophile.EXPECT().GetAsksHead(ammAddress).Return(asksHead).Times(1) + mockBibliophile.EXPECT().GetBidsHead(ammAddress).Return(bidsHead).Times(1) + mockBibliophile.EXPECT().HasReferrer(trader).Return(true).Times(1) + mockBibliophile.EXPECT().GetPriceMultiplier(ammAddress).Return(big.NewInt(1)).Times(1) + output := ValidatePlaceLimitOrder(mockBibliophile, &ValidatePlaceLimitOrderInput{Order: shortOrder, Sender: trader}) + assert.Equal(t, "", output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.Orderhash[:])) + assert.Equal(t, ammAddress, output.Res.Amm) + assert.Equal(t, big.NewInt(0), output.Res.ReserveAmount) + }) + }) + }) + }) + }) + }) + }) + t.Run("when order is not reduceOnly order", func(t *testing.T) { + t.Run("When order is in opposite direction of position and there are reduceOnly orders in orderbook", func(t *testing.T) { + t.Run("it returns error for a long Order", func(t *testing.T) { + longOrder := getOrder(ammIndex, trader, longBaseAssetQuantity, price, salt, false, postOnly) + positionSize := big.NewInt(0).Mul(longBaseAssetQuantity, big.NewInt(-3)) // short position + reduceOnlyAmount := big.NewInt(0).Div(longBaseAssetQuantity, big.NewInt(2)) + + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(longOrder.AmmIndex.Int64()).Return(ammAddress).Times(1) + mockBibliophile.EXPECT().GetMinSizeRequirement(longOrder.AmmIndex.Int64()).Return(minSizeRequirement).Times(1) + orderHash, err := GetLimitOrderHashFromContractStruct(&longOrder) + if err != nil { + panic("error in getting shortOrder hash") + } + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Invalid)).Times(1) + mockBibliophile.EXPECT().GetSize(ammAddress, &trader).Return(positionSize).Times(1) + mockBibliophile.EXPECT().GetReduceOnlyAmount(trader, longOrder.AmmIndex).Return(reduceOnlyAmount).Times(1) + + output := ValidatePlaceLimitOrder(mockBibliophile, &ValidatePlaceLimitOrderInput{Order: longOrder, Sender: trader}) + assert.Equal(t, ErrOpenReduceOnlyOrders.Error(), output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.Orderhash[:])) + assert.Equal(t, ammAddress, output.Res.Amm) + assert.Equal(t, big.NewInt(0), output.Res.ReserveAmount) + }) + t.Run("it returns error for a short Order", func(t *testing.T) { + shortOrder := getOrder(ammIndex, trader, shortBaseAssetQuantity, price, salt, false, postOnly) + positionSize := big.NewInt(0).Mul(shortBaseAssetQuantity, big.NewInt(-3)) // long position + reduceOnlyAmount := big.NewInt(0).Div(shortBaseAssetQuantity, big.NewInt(2)) + + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(shortOrder.AmmIndex.Int64()).Return(ammAddress).Times(1) + mockBibliophile.EXPECT().GetMinSizeRequirement(shortOrder.AmmIndex.Int64()).Return(minSizeRequirement).Times(1) + orderHash, err := GetLimitOrderHashFromContractStruct(&shortOrder) + if err != nil { + panic("error in getting shortOrder hash") + } + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Invalid)).Times(1) + mockBibliophile.EXPECT().GetSize(ammAddress, &trader).Return(positionSize).Times(1) + mockBibliophile.EXPECT().GetReduceOnlyAmount(trader, shortOrder.AmmIndex).Return(reduceOnlyAmount).Times(1) + + output := ValidatePlaceLimitOrder(mockBibliophile, &ValidatePlaceLimitOrderInput{Order: shortOrder, Sender: trader}) + assert.Equal(t, ErrOpenReduceOnlyOrders.Error(), output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.Orderhash[:])) + assert.Equal(t, ammAddress, output.Res.Amm) + assert.Equal(t, big.NewInt(0), output.Res.ReserveAmount) + }) + }) + //Using a bad description here. Not sure how to write it properly. I dont want to test so many branches + t.Run("when above is not true", func(t *testing.T) { + t.Run("when trader does not have available margin for order", func(t *testing.T) { + t.Run("it returns error for a long Order", func(t *testing.T) { + longOrder := getOrder(ammIndex, trader, longBaseAssetQuantity, price, salt, false, postOnly) + positionSize := big.NewInt(0).Mul(longBaseAssetQuantity, big.NewInt(-1)) // short position + reduceOnlyAmount := big.NewInt(0) + minAllowableMargin := big.NewInt(100000) + takerFee := big.NewInt(5000) + lowerBound := hu.Div(price, big.NewInt(2)) + upperBound := hu.Add(price, lowerBound) + + t.Run("when available margin is 0", func(t *testing.T) { + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(longOrder.AmmIndex.Int64()).Return(ammAddress).Times(1) + mockBibliophile.EXPECT().GetMinSizeRequirement(longOrder.AmmIndex.Int64()).Return(minSizeRequirement).Times(1) + orderHash, err := GetLimitOrderHashFromContractStruct(&longOrder) + if err != nil { + panic("error in getting longOrder hash") + } + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Invalid)).Times(1) + mockBibliophile.EXPECT().GetSize(ammAddress, &trader).Return(positionSize).Times(1) + mockBibliophile.EXPECT().GetReduceOnlyAmount(trader, longOrder.AmmIndex).Return(reduceOnlyAmount).Times(1) + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(longOrder.AmmIndex.Int64()).Return(upperBound, lowerBound).Times(1) + mockBibliophile.EXPECT().GetMinAllowableMargin().Return(minAllowableMargin).Times(1) + mockBibliophile.EXPECT().GetTakerFee().Return(takerFee).Times(1) + availableMargin := big.NewInt(0) + mockBibliophile.EXPECT().GetTimeStamp().Return(hu.V1ActivationTime).Times(1) + mockBibliophile.EXPECT().GetAvailableMargin(trader, hu.V1).Return(availableMargin).Times(1) + + output := ValidatePlaceLimitOrder(mockBibliophile, &ValidatePlaceLimitOrderInput{Order: longOrder, Sender: trader}) + assert.Equal(t, ErrInsufficientMargin.Error(), output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.Orderhash[:])) + assert.Equal(t, ammAddress, output.Res.Amm) + assert.Equal(t, big.NewInt(0), output.Res.ReserveAmount) + }) + t.Run("when available margin is one less than requiredMargin", func(t *testing.T) { + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(longOrder.AmmIndex.Int64()).Return(ammAddress).Times(1) + mockBibliophile.EXPECT().GetMinSizeRequirement(longOrder.AmmIndex.Int64()).Return(minSizeRequirement).Times(1) + orderHash, err := GetLimitOrderHashFromContractStruct(&longOrder) + if err != nil { + panic("error in getting longOrder hash") + } + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Invalid)).Times(1) + mockBibliophile.EXPECT().GetSize(ammAddress, &trader).Return(positionSize).Times(1) + mockBibliophile.EXPECT().GetReduceOnlyAmount(trader, longOrder.AmmIndex).Return(reduceOnlyAmount).Times(1) + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(longOrder.AmmIndex.Int64()).Return(upperBound, lowerBound).Times(1) + mockBibliophile.EXPECT().GetMinAllowableMargin().Return(minAllowableMargin).Times(1) + mockBibliophile.EXPECT().GetTakerFee().Return(takerFee).Times(1) + quoteAsset := big.NewInt(0).Abs(hu.Div(hu.Mul(longOrder.BaseAssetQuantity, longOrder.Price), big.NewInt(1e18))) + requiredMargin := hu.Div(hu.Mul(hu.Add(takerFee, minAllowableMargin), quoteAsset), big.NewInt(1e6)) + availableMargin := hu.Sub(requiredMargin, big.NewInt(1)) + mockBibliophile.EXPECT().GetTimeStamp().Return(hu.V1ActivationTime).Times(1) + mockBibliophile.EXPECT().GetAvailableMargin(trader, hu.V1).Return(availableMargin).Times(1) + + output := ValidatePlaceLimitOrder(mockBibliophile, &ValidatePlaceLimitOrderInput{Order: longOrder, Sender: trader}) + assert.Equal(t, ErrInsufficientMargin.Error(), output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.Orderhash[:])) + assert.Equal(t, ammAddress, output.Res.Amm) + assert.Equal(t, big.NewInt(0), output.Res.ReserveAmount) + }) + }) + t.Run("it returns error for a short Order", func(t *testing.T) { + shortOrder := getOrder(ammIndex, trader, shortBaseAssetQuantity, price, salt, false, postOnly) + positionSize := big.NewInt(0).Mul(shortBaseAssetQuantity, big.NewInt(-1)) // short position + reduceOnlyAmount := big.NewInt(0) + minAllowableMargin := big.NewInt(100000) + takerFee := big.NewInt(5000) + lowerBound := hu.Div(price, big.NewInt(2)) + upperBound := hu.Add(price, lowerBound) + + t.Run("when available margin is 0", func(t *testing.T) { + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(shortOrder.AmmIndex.Int64()).Return(ammAddress).Times(1) + mockBibliophile.EXPECT().GetMinSizeRequirement(shortOrder.AmmIndex.Int64()).Return(minSizeRequirement).Times(1) + orderHash, err := GetLimitOrderHashFromContractStruct(&shortOrder) + if err != nil { + panic("error in getting shortOrder hash") + } + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Invalid)).Times(1) + mockBibliophile.EXPECT().GetSize(ammAddress, &trader).Return(positionSize).Times(1) + mockBibliophile.EXPECT().GetReduceOnlyAmount(trader, shortOrder.AmmIndex).Return(reduceOnlyAmount).Times(1) + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(shortOrder.AmmIndex.Int64()).Return(upperBound, lowerBound).Times(1) + mockBibliophile.EXPECT().GetMinAllowableMargin().Return(minAllowableMargin).Times(1) + mockBibliophile.EXPECT().GetTakerFee().Return(takerFee).Times(1) + availableMargin := big.NewInt(0) + mockBibliophile.EXPECT().GetTimeStamp().Return(hu.V1ActivationTime).Times(1) + mockBibliophile.EXPECT().GetAvailableMargin(trader, hu.V1).Return(availableMargin).Times(1) + + output := ValidatePlaceLimitOrder(mockBibliophile, &ValidatePlaceLimitOrderInput{Order: shortOrder, Sender: trader}) + assert.Equal(t, ErrInsufficientMargin.Error(), output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.Orderhash[:])) + assert.Equal(t, ammAddress, output.Res.Amm) + assert.Equal(t, big.NewInt(0), output.Res.ReserveAmount) + }) + t.Run("when available margin is one less than requiredMargin", func(t *testing.T) { + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(shortOrder.AmmIndex.Int64()).Return(ammAddress).Times(1) + mockBibliophile.EXPECT().GetMinSizeRequirement(shortOrder.AmmIndex.Int64()).Return(minSizeRequirement).Times(1) + orderHash, err := GetLimitOrderHashFromContractStruct(&shortOrder) + if err != nil { + panic("error in getting shortOrder hash") + } + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Invalid)).Times(1) + mockBibliophile.EXPECT().GetSize(ammAddress, &trader).Return(positionSize).Times(1) + mockBibliophile.EXPECT().GetReduceOnlyAmount(trader, shortOrder.AmmIndex).Return(reduceOnlyAmount).Times(1) + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(shortOrder.AmmIndex.Int64()).Return(upperBound, lowerBound).Times(1) + mockBibliophile.EXPECT().GetMinAllowableMargin().Return(minAllowableMargin).Times(1) + mockBibliophile.EXPECT().GetTakerFee().Return(takerFee).Times(1) + // use upperBound as price to calculate quoteAsset for short + quoteAsset := big.NewInt(0).Abs(hu.Div(hu.Mul(shortOrder.BaseAssetQuantity, upperBound), big.NewInt(1e18))) + requiredMargin := hu.Div(hu.Mul(hu.Add(takerFee, minAllowableMargin), quoteAsset), big.NewInt(1e6)) + availableMargin := hu.Sub(requiredMargin, big.NewInt(1)) + mockBibliophile.EXPECT().GetTimeStamp().Return(hu.V1ActivationTime).Times(1) + mockBibliophile.EXPECT().GetAvailableMargin(trader, hu.V1).Return(availableMargin).Times(1) + + output := ValidatePlaceLimitOrder(mockBibliophile, &ValidatePlaceLimitOrderInput{Order: shortOrder, Sender: trader}) + assert.Equal(t, ErrInsufficientMargin.Error(), output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.Orderhash[:])) + assert.Equal(t, ammAddress, output.Res.Amm) + assert.Equal(t, big.NewInt(0), output.Res.ReserveAmount) + }) + }) + }) + t.Run("when trader has available margin for order", func(t *testing.T) { + t.Run("when order is not a postOnly order", func(t *testing.T) { + minAllowableMargin := big.NewInt(100000) + takerFee := big.NewInt(5000) + reduceOnlyAmount := big.NewInt(0) + t.Run("it returns nil error and reserverAmount when order is a long order", func(t *testing.T) { + longOrder := getOrder(ammIndex, trader, longBaseAssetQuantity, price, salt, false, false) + positionSize := big.NewInt(0).Mul(longBaseAssetQuantity, big.NewInt(-1)) // short position + quoteAsset := big.NewInt(0).Abs(hu.Div(hu.Mul(longOrder.BaseAssetQuantity, longOrder.Price), big.NewInt(1e18))) + requiredMargin := hu.Div(hu.Mul(hu.Add(takerFee, minAllowableMargin), quoteAsset), big.NewInt(1e6)) + availableMargin := hu.Add(requiredMargin, big.NewInt(1)) + lowerBound := hu.Div(price, big.NewInt(2)) + upperBound := hu.Add(price, lowerBound) + + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(longOrder.AmmIndex.Int64()).Return(ammAddress).Times(1) + mockBibliophile.EXPECT().GetMinSizeRequirement(longOrder.AmmIndex.Int64()).Return(minSizeRequirement).Times(1) + orderHash, err := GetLimitOrderHashFromContractStruct(&longOrder) + if err != nil { + panic("error in getting longOrder hash") + } + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Invalid)).Times(1) + mockBibliophile.EXPECT().GetSize(ammAddress, &trader).Return(positionSize).Times(1) + mockBibliophile.EXPECT().GetReduceOnlyAmount(trader, longOrder.AmmIndex).Return(reduceOnlyAmount).Times(1) + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(longOrder.AmmIndex.Int64()).Return(upperBound, lowerBound).Times(1) + mockBibliophile.EXPECT().GetMinAllowableMargin().Return(minAllowableMargin).Times(1) + mockBibliophile.EXPECT().GetTakerFee().Return(takerFee).Times(1) + mockBibliophile.EXPECT().GetTimeStamp().Return(hu.V1ActivationTime).Times(1) + mockBibliophile.EXPECT().GetAvailableMargin(trader, hu.V1).Return(availableMargin).Times(1) + mockBibliophile.EXPECT().HasReferrer(trader).Return(true).Times(1) + mockBibliophile.EXPECT().GetPriceMultiplier(ammAddress).Return(big.NewInt(1)).Times(1) + output := ValidatePlaceLimitOrder(mockBibliophile, &ValidatePlaceLimitOrderInput{Order: longOrder, Sender: trader}) + assert.Equal(t, "", output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.Orderhash[:])) + assert.Equal(t, ammAddress, output.Res.Amm) + assert.Equal(t, requiredMargin, output.Res.ReserveAmount) + }) + t.Run("it returns nil error and reserverAmount when order is a short order", func(t *testing.T) { + lowerBound := hu.Div(price, big.NewInt(2)) + upperBound := hu.Add(price, lowerBound) + shortOrder := getOrder(ammIndex, trader, shortBaseAssetQuantity, price, salt, false, false) + positionSize := big.NewInt(0).Mul(shortBaseAssetQuantity, big.NewInt(-1)) // long position + quoteAsset := big.NewInt(0).Abs(hu.Div(hu.Mul(shortOrder.BaseAssetQuantity, upperBound), big.NewInt(1e18))) + requiredMargin := hu.Div(hu.Mul(hu.Add(takerFee, minAllowableMargin), quoteAsset), big.NewInt(1e6)) + availableMargin := hu.Add(requiredMargin, big.NewInt(1)) + + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(shortOrder.AmmIndex.Int64()).Return(ammAddress).Times(1) + mockBibliophile.EXPECT().GetMinSizeRequirement(shortOrder.AmmIndex.Int64()).Return(minSizeRequirement).Times(1) + orderHash, err := GetLimitOrderHashFromContractStruct(&shortOrder) + if err != nil { + panic("error in getting shortOrder hash") + } + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Invalid)).Times(1) + mockBibliophile.EXPECT().GetSize(ammAddress, &trader).Return(positionSize).Times(1) + mockBibliophile.EXPECT().GetReduceOnlyAmount(trader, shortOrder.AmmIndex).Return(reduceOnlyAmount).Times(1) + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(shortOrder.AmmIndex.Int64()).Return(upperBound, lowerBound).Times(1) + mockBibliophile.EXPECT().GetMinAllowableMargin().Return(minAllowableMargin).Times(1) + mockBibliophile.EXPECT().GetTakerFee().Return(takerFee).Times(1) + mockBibliophile.EXPECT().GetTimeStamp().Return(hu.V1ActivationTime).Times(1) + mockBibliophile.EXPECT().GetAvailableMargin(trader, hu.V1).Return(availableMargin).Times(1) + mockBibliophile.EXPECT().HasReferrer(trader).Return(true).Times(1) + mockBibliophile.EXPECT().GetPriceMultiplier(ammAddress).Return(big.NewInt(1)).Times(1) + output := ValidatePlaceLimitOrder(mockBibliophile, &ValidatePlaceLimitOrderInput{Order: shortOrder, Sender: trader}) + assert.Equal(t, "", output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.Orderhash[:])) + assert.Equal(t, ammAddress, output.Res.Amm) + assert.Equal(t, requiredMargin, output.Res.ReserveAmount) + }) + }) + t.Run("when order is a postOnly order", func(t *testing.T) { + asksHead := big.NewInt(0).Add(price, big.NewInt(1)) + bidsHead := big.NewInt(0).Sub(price, big.NewInt(1)) + minAllowableMargin := big.NewInt(100000) + takerFee := big.NewInt(5000) + reduceOnlyAmount := big.NewInt(0) + + t.Run("when order crosses market", func(t *testing.T) { + t.Run("it returns error if longOrder's price >= asksHead", func(t *testing.T) { + t.Run("it returns error if longOrder's price = asksHead", func(t *testing.T) { + longPrice := big.NewInt(0).Set(asksHead) + longOrder := getOrder(ammIndex, trader, longBaseAssetQuantity, longPrice, salt, false, true) + positionSize := big.NewInt(0).Mul(longBaseAssetQuantity, big.NewInt(-1)) // short position + quoteAsset := big.NewInt(0).Abs(hu.Div(hu.Mul(longOrder.BaseAssetQuantity, longOrder.Price), big.NewInt(1e18))) + requiredMargin := hu.Add(hu.Div(hu.Mul(minAllowableMargin, quoteAsset), big.NewInt(1e6)), hu.Div(hu.Mul(takerFee, quoteAsset), big.NewInt(1e6))) + availableMargin := hu.Add(requiredMargin, big.NewInt(1)) + lowerBound := hu.Div(price, big.NewInt(2)) + upperBound := hu.Add(price, lowerBound) + + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(longOrder.AmmIndex.Int64()).Return(ammAddress).Times(1) + mockBibliophile.EXPECT().GetMinSizeRequirement(longOrder.AmmIndex.Int64()).Return(minSizeRequirement).Times(1) + orderHash, err := GetLimitOrderHashFromContractStruct(&longOrder) + if err != nil { + panic("error in getting longOrder hash") + } + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Invalid)).Times(1) + mockBibliophile.EXPECT().GetSize(ammAddress, &trader).Return(positionSize).Times(1) + mockBibliophile.EXPECT().GetReduceOnlyAmount(trader, longOrder.AmmIndex).Return(reduceOnlyAmount).Times(1) + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(longOrder.AmmIndex.Int64()).Return(upperBound, lowerBound).Times(1) + mockBibliophile.EXPECT().GetMinAllowableMargin().Return(minAllowableMargin).Times(1) + mockBibliophile.EXPECT().GetTakerFee().Return(takerFee).Times(1) + mockBibliophile.EXPECT().GetTimeStamp().Return(hu.V1ActivationTime).Times(1) + mockBibliophile.EXPECT().GetAvailableMargin(trader, hu.V1).Return(availableMargin).Times(1) + mockBibliophile.EXPECT().GetAsksHead(ammAddress).Return(asksHead).Times(1) + mockBibliophile.EXPECT().GetBidsHead(ammAddress).Return(bidsHead).Times(1) + output := ValidatePlaceLimitOrder(mockBibliophile, &ValidatePlaceLimitOrderInput{Order: longOrder, Sender: trader}) + assert.Equal(t, ErrCrossingMarket.Error(), output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.Orderhash[:])) + assert.Equal(t, ammAddress, output.Res.Amm) + assert.Equal(t, requiredMargin, output.Res.ReserveAmount) + }) + t.Run("it returns error if longOrder's price > asksHead", func(t *testing.T) { + longPrice := big.NewInt(0).Add(asksHead, big.NewInt(1)) + longOrder := getOrder(ammIndex, trader, longBaseAssetQuantity, longPrice, salt, false, true) + positionSize := big.NewInt(0).Mul(longBaseAssetQuantity, big.NewInt(-1)) // short position + quoteAsset := big.NewInt(0).Abs(hu.Div(hu.Mul(longOrder.BaseAssetQuantity, longOrder.Price), big.NewInt(1e18))) + requiredMargin := hu.Div(hu.Mul(hu.Add(takerFee, minAllowableMargin), quoteAsset), big.NewInt(1e6)) + availableMargin := hu.Add(requiredMargin, big.NewInt(1)) + lowerBound := hu.Div(price, big.NewInt(2)) + upperBound := hu.Add(price, lowerBound) + + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(longOrder.AmmIndex.Int64()).Return(ammAddress).Times(1) + mockBibliophile.EXPECT().GetMinSizeRequirement(longOrder.AmmIndex.Int64()).Return(minSizeRequirement).Times(1) + orderHash, err := GetLimitOrderHashFromContractStruct(&longOrder) + if err != nil { + panic("error in getting longOrder hash") + } + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Invalid)).Times(1) + mockBibliophile.EXPECT().GetSize(ammAddress, &trader).Return(positionSize).Times(1) + mockBibliophile.EXPECT().GetReduceOnlyAmount(trader, longOrder.AmmIndex).Return(reduceOnlyAmount).Times(1) + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(longOrder.AmmIndex.Int64()).Return(upperBound, lowerBound).Times(1) + mockBibliophile.EXPECT().GetMinAllowableMargin().Return(minAllowableMargin).Times(1) + mockBibliophile.EXPECT().GetTakerFee().Return(takerFee).Times(1) + mockBibliophile.EXPECT().GetTimeStamp().Return(hu.V1ActivationTime).Times(1) + mockBibliophile.EXPECT().GetAvailableMargin(trader, hu.V1).Return(availableMargin).Times(1) + mockBibliophile.EXPECT().GetAsksHead(ammAddress).Return(asksHead).Times(1) + mockBibliophile.EXPECT().GetBidsHead(ammAddress).Return(bidsHead).Times(1) + output := ValidatePlaceLimitOrder(mockBibliophile, &ValidatePlaceLimitOrderInput{Order: longOrder, Sender: trader}) + assert.Equal(t, ErrCrossingMarket.Error(), output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.Orderhash[:])) + assert.Equal(t, ammAddress, output.Res.Amm) + assert.Equal(t, requiredMargin, output.Res.ReserveAmount) + }) + }) + t.Run("it returns error if shortOrder's price <= bidsHead", func(t *testing.T) { + positionSize := big.NewInt(0).Mul(shortBaseAssetQuantity, big.NewInt(-1)) + + t.Run("it returns error if shortOrder price = asksHead", func(t *testing.T) { + shortOrderPrice := big.NewInt(0).Set(bidsHead) + lowerBound := hu.Div(price, big.NewInt(2)) + upperBound := hu.Add(price, lowerBound) + shortOrder := getOrder(ammIndex, trader, shortBaseAssetQuantity, shortOrderPrice, salt, false, true) + quoteAsset := big.NewInt(0).Abs(hu.Div(hu.Mul(shortOrder.BaseAssetQuantity, upperBound), big.NewInt(1e18))) + requiredMargin := hu.Add(hu.Div(hu.Mul(minAllowableMargin, quoteAsset), big.NewInt(1e6)), hu.Div(hu.Mul(takerFee, quoteAsset), big.NewInt(1e6))) + availableMargin := hu.Add(requiredMargin, big.NewInt(1)) + + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(shortOrder.AmmIndex.Int64()).Return(ammAddress).Times(1) + mockBibliophile.EXPECT().GetMinSizeRequirement(shortOrder.AmmIndex.Int64()).Return(minSizeRequirement).Times(1) + orderHash, err := GetLimitOrderHashFromContractStruct(&shortOrder) + if err != nil { + panic("error in getting shortOrder hash") + } + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Invalid)).Times(1) + mockBibliophile.EXPECT().GetSize(ammAddress, &trader).Return(positionSize).Times(1) + mockBibliophile.EXPECT().GetReduceOnlyAmount(trader, shortOrder.AmmIndex).Return(reduceOnlyAmount).Times(1) + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(shortOrder.AmmIndex.Int64()).Return(upperBound, lowerBound).Times(1) + mockBibliophile.EXPECT().GetMinAllowableMargin().Return(minAllowableMargin).Times(1) + mockBibliophile.EXPECT().GetTakerFee().Return(takerFee).Times(1) + mockBibliophile.EXPECT().GetTimeStamp().Return(hu.V1ActivationTime).Times(1) + mockBibliophile.EXPECT().GetAvailableMargin(trader, hu.V1).Return(availableMargin).Times(1) + mockBibliophile.EXPECT().GetAsksHead(ammAddress).Return(asksHead).Times(1) + mockBibliophile.EXPECT().GetBidsHead(ammAddress).Return(bidsHead).Times(1) + + output := ValidatePlaceLimitOrder(mockBibliophile, &ValidatePlaceLimitOrderInput{Order: shortOrder, Sender: trader}) + assert.Equal(t, ErrCrossingMarket.Error(), output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.Orderhash[:])) + assert.Equal(t, ammAddress, output.Res.Amm) + assert.Equal(t, requiredMargin, output.Res.ReserveAmount) + }) + t.Run("it returns error if shortOrder price < asksHead", func(t *testing.T) { + shortOrderPrice := big.NewInt(0).Sub(bidsHead, big.NewInt(1)) + lowerBound := hu.Div(price, big.NewInt(2)) + upperBound := hu.Add(price, lowerBound) + shortOrder := getOrder(ammIndex, trader, shortBaseAssetQuantity, shortOrderPrice, salt, false, true) + quoteAsset := big.NewInt(0).Abs(hu.Div(hu.Mul(shortOrder.BaseAssetQuantity, upperBound), big.NewInt(1e18))) + requiredMargin := hu.Add(hu.Div(hu.Mul(minAllowableMargin, quoteAsset), big.NewInt(1e6)), hu.Div(hu.Mul(takerFee, quoteAsset), big.NewInt(1e6))) + availableMargin := hu.Add(requiredMargin, big.NewInt(1)) + + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(shortOrder.AmmIndex.Int64()).Return(ammAddress).Times(1) + mockBibliophile.EXPECT().GetMinSizeRequirement(shortOrder.AmmIndex.Int64()).Return(minSizeRequirement).Times(1) + orderHash, err := GetLimitOrderHashFromContractStruct(&shortOrder) + if err != nil { + panic("error in getting shortOrder hash") + } + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Invalid)).Times(1) + mockBibliophile.EXPECT().GetSize(ammAddress, &trader).Return(positionSize).Times(1) + mockBibliophile.EXPECT().GetReduceOnlyAmount(trader, shortOrder.AmmIndex).Return(reduceOnlyAmount).Times(1) + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(shortOrder.AmmIndex.Int64()).Return(upperBound, lowerBound).Times(1) + mockBibliophile.EXPECT().GetMinAllowableMargin().Return(minAllowableMargin).Times(1) + mockBibliophile.EXPECT().GetTakerFee().Return(takerFee).Times(1) + mockBibliophile.EXPECT().GetTimeStamp().Return(hu.V1ActivationTime).Times(1) + mockBibliophile.EXPECT().GetAvailableMargin(trader, hu.V1).Return(availableMargin).Times(1) + mockBibliophile.EXPECT().GetAsksHead(ammAddress).Return(asksHead).Times(1) + mockBibliophile.EXPECT().GetBidsHead(ammAddress).Return(bidsHead).Times(1) + + output := ValidatePlaceLimitOrder(mockBibliophile, &ValidatePlaceLimitOrderInput{Order: shortOrder, Sender: trader}) + assert.Equal(t, ErrCrossingMarket.Error(), output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.Orderhash[:])) + assert.Equal(t, ammAddress, output.Res.Amm) + assert.Equal(t, requiredMargin, output.Res.ReserveAmount) + }) + }) + }) + t.Run("when order does not cross market", func(t *testing.T) { + t.Run("for a longOrder it returns no error and 0 as reserveAmount", func(t *testing.T) { + longOrder := getOrder(ammIndex, trader, longBaseAssetQuantity, price, salt, false, true) + positionSize := big.NewInt(0) + quoteAsset := big.NewInt(0).Abs(hu.Div(hu.Mul(longOrder.BaseAssetQuantity, longOrder.Price), big.NewInt(1e18))) + requiredMargin := hu.Add(hu.Div(hu.Mul(minAllowableMargin, quoteAsset), big.NewInt(1e6)), hu.Div(hu.Mul(takerFee, quoteAsset), big.NewInt(1e6))) + availableMargin := hu.Add(requiredMargin, big.NewInt(1)) + lowerBound := hu.Div(price, big.NewInt(2)) + upperBound := hu.Add(price, lowerBound) + + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(longOrder.AmmIndex.Int64()).Return(ammAddress).Times(1) + mockBibliophile.EXPECT().GetMinSizeRequirement(longOrder.AmmIndex.Int64()).Return(minSizeRequirement).Times(1) + orderHash, err := GetLimitOrderHashFromContractStruct(&longOrder) + if err != nil { + panic("error in getting longOrder hash") + } + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Invalid)).Times(1) + mockBibliophile.EXPECT().GetSize(ammAddress, &trader).Return(positionSize).Times(1) + mockBibliophile.EXPECT().GetReduceOnlyAmount(trader, longOrder.AmmIndex).Return(reduceOnlyAmount).Times(1) + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(longOrder.AmmIndex.Int64()).Return(upperBound, lowerBound).Times(1) + mockBibliophile.EXPECT().GetMinAllowableMargin().Return(minAllowableMargin).Times(1) + mockBibliophile.EXPECT().GetTakerFee().Return(takerFee).Times(1) + mockBibliophile.EXPECT().GetTimeStamp().Return(hu.V1ActivationTime).Times(1) + mockBibliophile.EXPECT().GetAvailableMargin(trader, hu.V1).Return(availableMargin).Times(1) + mockBibliophile.EXPECT().GetAsksHead(ammAddress).Return(asksHead).Times(1) + mockBibliophile.EXPECT().GetBidsHead(ammAddress).Return(bidsHead).Times(1) + mockBibliophile.EXPECT().HasReferrer(trader).Return(true).Times(1) + mockBibliophile.EXPECT().GetPriceMultiplier(ammAddress).Return(big.NewInt(1)).Times(1) + output := ValidatePlaceLimitOrder(mockBibliophile, &ValidatePlaceLimitOrderInput{Order: longOrder, Sender: trader}) + assert.Equal(t, "", output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.Orderhash[:])) + assert.Equal(t, ammAddress, output.Res.Amm) + assert.Equal(t, requiredMargin, output.Res.ReserveAmount) + }) + t.Run("for a shortOrder it returns no error and 0 as reserveAmount", func(t *testing.T) { + shortOrder := getOrder(ammIndex, trader, shortBaseAssetQuantity, price, salt, false, true) + positionSize := big.NewInt(0) + lowerBound := hu.Div(price, big.NewInt(2)) + upperBound := hu.Add(price, lowerBound) + quoteAsset := big.NewInt(0).Abs(hu.Div(hu.Mul(shortOrder.BaseAssetQuantity, upperBound), big.NewInt(1e18))) + requiredMargin := hu.Add(hu.Div(hu.Mul(minAllowableMargin, quoteAsset), big.NewInt(1e6)), hu.Div(hu.Mul(takerFee, quoteAsset), big.NewInt(1e6))) + availableMargin := hu.Add(requiredMargin, big.NewInt(1)) + + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(shortOrder.AmmIndex.Int64()).Return(ammAddress).Times(1) + mockBibliophile.EXPECT().GetMinSizeRequirement(shortOrder.AmmIndex.Int64()).Return(minSizeRequirement).Times(1) + orderHash, err := GetLimitOrderHashFromContractStruct(&shortOrder) + if err != nil { + panic("error in getting shortOrder hash") + } + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Invalid)).Times(1) + mockBibliophile.EXPECT().GetSize(ammAddress, &trader).Return(positionSize).Times(1) + mockBibliophile.EXPECT().GetReduceOnlyAmount(trader, shortOrder.AmmIndex).Return(reduceOnlyAmount).Times(1) + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(shortOrder.AmmIndex.Int64()).Return(upperBound, lowerBound).Times(1) + mockBibliophile.EXPECT().GetMinAllowableMargin().Return(minAllowableMargin).Times(1) + mockBibliophile.EXPECT().GetTakerFee().Return(takerFee).Times(1) + mockBibliophile.EXPECT().GetTimeStamp().Return(hu.V1ActivationTime).Times(1) + mockBibliophile.EXPECT().GetAvailableMargin(trader, hu.V1).Return(availableMargin).Times(1) + mockBibliophile.EXPECT().GetAsksHead(ammAddress).Return(asksHead).Times(1) + mockBibliophile.EXPECT().GetBidsHead(ammAddress).Return(bidsHead).Times(1) + mockBibliophile.EXPECT().HasReferrer(trader).Return(true).Times(1) + mockBibliophile.EXPECT().GetPriceMultiplier(ammAddress).Return(big.NewInt(1)).Times(1) + output := ValidatePlaceLimitOrder(mockBibliophile, &ValidatePlaceLimitOrderInput{Order: shortOrder, Sender: trader}) + assert.Equal(t, "", output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.Orderhash[:])) + assert.Equal(t, ammAddress, output.Res.Amm) + assert.Equal(t, requiredMargin, output.Res.ReserveAmount) + }) + }) + }) + }) + }) + }) + }) +} + +func TestValidateCancelLimitOrder(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockBibliophile := b.NewMockBibliophileClient(ctrl) + ammIndex := big.NewInt(0) + longBaseAssetQuantity := big.NewInt(5000000000000000000) + shortBaseAssetQuantity := big.NewInt(-5000000000000000000) + price := big.NewInt(100000000) + salt := big.NewInt(121) + reduceOnly := false + postOnly := false + trader := common.HexToAddress("0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC") + ammAddress := common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8") + assertLowMargin := false + + t.Run("when sender is not the trader and is not trading authority, it returns error", func(t *testing.T) { + sender := common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C9") + t.Run("it returns error for a long order", func(t *testing.T) { + order := getOrder(ammIndex, trader, longBaseAssetQuantity, price, salt, reduceOnly, postOnly) + input := getValidateCancelLimitOrderInput(order, sender, assertLowMargin) + mockBibliophile.EXPECT().IsTradingAuthority(order.Trader, sender).Return(false).Times(1) + output := ValidateCancelLimitOrder(mockBibliophile, &input) + assert.Equal(t, ErrNoTradingAuthority.Error(), output.Err) + }) + t.Run("it returns error for a short order", func(t *testing.T) { + order := getOrder(ammIndex, trader, shortBaseAssetQuantity, price, salt, reduceOnly, postOnly) + input := getValidateCancelLimitOrderInput(order, sender, assertLowMargin) + mockBibliophile.EXPECT().IsTradingAuthority(order.Trader, sender).Return(false).Times(1) + output := ValidateCancelLimitOrder(mockBibliophile, &input) + assert.Equal(t, ErrNoTradingAuthority.Error(), output.Err) + }) + }) + t.Run("when either sender is trader or a trading authority", func(t *testing.T) { + t.Run("When order status is not placed", func(t *testing.T) { + t.Run("when order status was never placed", func(t *testing.T) { + t.Run("it returns error for a longOrder", func(t *testing.T) { + longOrder := getOrder(ammIndex, trader, longBaseAssetQuantity, price, salt, reduceOnly, postOnly) + orderHash := getOrderHash(longOrder) + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Invalid)).Times(1) + input := getValidateCancelLimitOrderInput(longOrder, trader, assertLowMargin) + output := ValidateCancelLimitOrder(mockBibliophile, &input) + assert.Equal(t, "Invalid", output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.OrderHash[:])) + assert.Equal(t, common.Address{}, output.Res.Amm) + assert.Equal(t, big.NewInt(0), output.Res.UnfilledAmount) + }) + t.Run("it returns error for a shortOrder", func(t *testing.T) { + shortOrder := getOrder(ammIndex, trader, shortBaseAssetQuantity, price, salt, reduceOnly, postOnly) + orderHash := getOrderHash(shortOrder) + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Invalid)).Times(1) + input := getValidateCancelLimitOrderInput(shortOrder, trader, assertLowMargin) + output := ValidateCancelLimitOrder(mockBibliophile, &input) + assert.Equal(t, "Invalid", output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.OrderHash[:])) + assert.Equal(t, common.Address{}, output.Res.Amm) + assert.Equal(t, big.NewInt(0), output.Res.UnfilledAmount) + }) + }) + t.Run("when order status is cancelled", func(t *testing.T) { + t.Run("it returns error for a longOrder", func(t *testing.T) { + longOrder := getOrder(ammIndex, trader, longBaseAssetQuantity, price, salt, reduceOnly, postOnly) + orderHash := getOrderHash(longOrder) + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Cancelled)).Times(1) + input := getValidateCancelLimitOrderInput(longOrder, trader, assertLowMargin) + output := ValidateCancelLimitOrder(mockBibliophile, &input) + assert.Equal(t, "Cancelled", output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.OrderHash[:])) + assert.Equal(t, common.Address{}, output.Res.Amm) + assert.Equal(t, big.NewInt(0), output.Res.UnfilledAmount) + }) + t.Run("it returns error for a shortOrder", func(t *testing.T) { + shortOrder := getOrder(ammIndex, trader, shortBaseAssetQuantity, price, salt, reduceOnly, postOnly) + orderHash := getOrderHash(shortOrder) + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Cancelled)).Times(1) + input := getValidateCancelLimitOrderInput(shortOrder, trader, assertLowMargin) + output := ValidateCancelLimitOrder(mockBibliophile, &input) + assert.Equal(t, "Cancelled", output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.OrderHash[:])) + assert.Equal(t, common.Address{}, output.Res.Amm) + assert.Equal(t, big.NewInt(0), output.Res.UnfilledAmount) + }) + }) + t.Run("when order status is filled", func(t *testing.T) { + t.Run("it returns error for a longOrder", func(t *testing.T) { + longOrder := getOrder(ammIndex, trader, longBaseAssetQuantity, price, salt, reduceOnly, postOnly) + orderHash := getOrderHash(longOrder) + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Filled)).Times(1) + input := getValidateCancelLimitOrderInput(longOrder, trader, assertLowMargin) + output := ValidateCancelLimitOrder(mockBibliophile, &input) + assert.Equal(t, "Filled", output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.OrderHash[:])) + assert.Equal(t, common.Address{}, output.Res.Amm) + assert.Equal(t, big.NewInt(0), output.Res.UnfilledAmount) + }) + t.Run("it returns error for a shortOrder", func(t *testing.T) { + shortOrder := getOrder(ammIndex, trader, shortBaseAssetQuantity, price, salt, reduceOnly, postOnly) + orderHash := getOrderHash(shortOrder) + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Filled)).Times(1) + input := getValidateCancelLimitOrderInput(shortOrder, trader, assertLowMargin) + output := ValidateCancelLimitOrder(mockBibliophile, &input) + assert.Equal(t, "Filled", output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.OrderHash[:])) + assert.Equal(t, common.Address{}, output.Res.Amm) + assert.Equal(t, big.NewInt(0), output.Res.UnfilledAmount) + }) + }) + }) + t.Run("When order status is placed", func(t *testing.T) { + t.Run("when assertLowMargin is true", func(t *testing.T) { + assertLowMargin := true + t.Run("when availableMargin >= zero", func(t *testing.T) { + t.Run("when availableMargin == 0 ", func(t *testing.T) { + t.Run("it returns error for a longOrder", func(t *testing.T) { + longOrder := getOrder(ammIndex, trader, longBaseAssetQuantity, price, salt, reduceOnly, postOnly) + orderHash := getOrderHash(longOrder) + + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Placed)).Times(1) + mockBibliophile.EXPECT().GetTimeStamp().Return(hu.V1ActivationTime).Times(1) + mockBibliophile.EXPECT().GetAvailableMargin(longOrder.Trader, hu.V1).Return(big.NewInt(0)).Times(1) + mockBibliophile.EXPECT().IsValidator(longOrder.Trader).Return(true).Times(1) + input := getValidateCancelLimitOrderInput(longOrder, trader, assertLowMargin) + output := ValidateCancelLimitOrder(mockBibliophile, &input) + assert.Equal(t, "Not Low Margin", output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.OrderHash[:])) + assert.Equal(t, common.Address{}, output.Res.Amm) + assert.Equal(t, big.NewInt(0), output.Res.UnfilledAmount) + }) + t.Run("it returns error for a shortOrder", func(t *testing.T) { + shortOrder := getOrder(ammIndex, trader, shortBaseAssetQuantity, price, salt, reduceOnly, postOnly) + orderHash := getOrderHash(shortOrder) + + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Placed)).Times(1) + mockBibliophile.EXPECT().GetTimeStamp().Return(hu.V1ActivationTime).Times(1) + mockBibliophile.EXPECT().GetAvailableMargin(shortOrder.Trader, hu.V1).Return(big.NewInt(0)).Times(1) + mockBibliophile.EXPECT().IsValidator(shortOrder.Trader).Return(true).Times(1) + input := getValidateCancelLimitOrderInput(shortOrder, trader, assertLowMargin) + output := ValidateCancelLimitOrder(mockBibliophile, &input) + + assert.Equal(t, "Not Low Margin", output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.OrderHash[:])) + assert.Equal(t, common.Address{}, output.Res.Amm) + assert.Equal(t, big.NewInt(0), output.Res.UnfilledAmount) + }) + }) + t.Run("when availableMargin > 0 ", func(t *testing.T) { + newMargin := hu.Mul(price, longBaseAssetQuantity) + t.Run("it returns error for a longOrder", func(t *testing.T) { + longOrder := getOrder(ammIndex, trader, longBaseAssetQuantity, price, salt, reduceOnly, postOnly) + orderHash := getOrderHash(longOrder) + + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Placed)).Times(1) + mockBibliophile.EXPECT().GetTimeStamp().Return(hu.V1ActivationTime).Times(1) + mockBibliophile.EXPECT().GetAvailableMargin(longOrder.Trader, hu.V1).Return(newMargin).Times(1) + mockBibliophile.EXPECT().IsValidator(longOrder.Trader).Return(true).Times(1) + input := getValidateCancelLimitOrderInput(longOrder, trader, assertLowMargin) + output := ValidateCancelLimitOrder(mockBibliophile, &input) + assert.Equal(t, "Not Low Margin", output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.OrderHash[:])) + assert.Equal(t, common.Address{}, output.Res.Amm) + assert.Equal(t, big.NewInt(0), output.Res.UnfilledAmount) + }) + t.Run("it returns error for a shortOrder", func(t *testing.T) { + shortOrder := getOrder(ammIndex, trader, shortBaseAssetQuantity, price, salt, reduceOnly, postOnly) + orderHash := getOrderHash(shortOrder) + + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Placed)).Times(1) + mockBibliophile.EXPECT().GetTimeStamp().Return(hu.V1ActivationTime).Times(1) + mockBibliophile.EXPECT().GetAvailableMargin(shortOrder.Trader, hu.V1).Return(newMargin).Times(1) + mockBibliophile.EXPECT().IsValidator(shortOrder.Trader).Return(true).Times(1) + input := getValidateCancelLimitOrderInput(shortOrder, trader, assertLowMargin) + output := ValidateCancelLimitOrder(mockBibliophile, &input) + assert.Equal(t, "Not Low Margin", output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.OrderHash[:])) + assert.Equal(t, common.Address{}, output.Res.Amm) + assert.Equal(t, big.NewInt(0), output.Res.UnfilledAmount) + }) + }) + }) + t.Run("when availableMargin < zero", func(t *testing.T) { + t.Run("for an unfilled Order", func(t *testing.T) { + t.Run("for a longOrder it returns err = nil, with ammAddress and unfilled amount of cancelled Order", func(t *testing.T) { + longOrder := getOrder(ammIndex, trader, longBaseAssetQuantity, price, salt, reduceOnly, postOnly) + orderHash := getOrderHash(longOrder) + + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Placed)).Times(1) + mockBibliophile.EXPECT().GetTimeStamp().Return(hu.V1ActivationTime).Times(1) + mockBibliophile.EXPECT().GetAvailableMargin(longOrder.Trader, hu.V1).Return(big.NewInt(-1)).Times(1) + mockBibliophile.EXPECT().GetOrderFilledAmount(orderHash).Return(big.NewInt(0)).Times(1) + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(longOrder.AmmIndex.Int64()).Return(ammAddress).Times(1) + mockBibliophile.EXPECT().IsValidator(longOrder.Trader).Return(true).Times(1) + + input := getValidateCancelLimitOrderInput(longOrder, trader, assertLowMargin) + output := ValidateCancelLimitOrder(mockBibliophile, &input) + assert.Equal(t, "", output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.OrderHash[:])) + assert.Equal(t, ammAddress, output.Res.Amm) + assert.Equal(t, longOrder.BaseAssetQuantity, output.Res.UnfilledAmount) + }) + t.Run("for a shortOrder it returns err = nil, with ammAddress and unfilled amount of cancelled Order", func(t *testing.T) { + shortOrder := getOrder(ammIndex, trader, shortBaseAssetQuantity, price, salt, reduceOnly, postOnly) + orderHash := getOrderHash(shortOrder) + + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Placed)).Times(1) + mockBibliophile.EXPECT().GetTimeStamp().Return(hu.V1ActivationTime).Times(1) + mockBibliophile.EXPECT().GetAvailableMargin(shortOrder.Trader, hu.V1).Return(big.NewInt(-1)).Times(1) + mockBibliophile.EXPECT().GetOrderFilledAmount(orderHash).Return(big.NewInt(0)).Times(1) + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(shortOrder.AmmIndex.Int64()).Return(ammAddress).Times(1) + mockBibliophile.EXPECT().IsValidator(shortOrder.Trader).Return(true).Times(1) + + input := getValidateCancelLimitOrderInput(shortOrder, trader, assertLowMargin) + output := ValidateCancelLimitOrder(mockBibliophile, &input) + assert.Equal(t, "", output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.OrderHash[:])) + assert.Equal(t, ammAddress, output.Res.Amm) + assert.Equal(t, shortOrder.BaseAssetQuantity, output.Res.UnfilledAmount) + }) + }) + t.Run("for a partially filled Order", func(t *testing.T) { + t.Run("for a longOrder it returns err = nil, with ammAddress and unfilled amount of cancelled Order", func(t *testing.T) { + longOrder := getOrder(ammIndex, trader, longBaseAssetQuantity, price, salt, reduceOnly, postOnly) + orderHash := getOrderHash(longOrder) + filledAmount := hu.Div(longOrder.BaseAssetQuantity, big.NewInt(2)) + + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Placed)).Times(1) + mockBibliophile.EXPECT().GetTimeStamp().Return(hu.V1ActivationTime).Times(1) + mockBibliophile.EXPECT().GetAvailableMargin(longOrder.Trader, hu.V1).Return(big.NewInt(-1)).Times(1) + mockBibliophile.EXPECT().GetOrderFilledAmount(orderHash).Return(filledAmount).Times(1) + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(longOrder.AmmIndex.Int64()).Return(ammAddress).Times(1) + mockBibliophile.EXPECT().IsValidator(longOrder.Trader).Return(true).Times(1) + + input := getValidateCancelLimitOrderInput(longOrder, trader, assertLowMargin) + output := ValidateCancelLimitOrder(mockBibliophile, &input) + assert.Equal(t, "", output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.OrderHash[:])) + assert.Equal(t, ammAddress, output.Res.Amm) + expectedUnfilleAmount := hu.Sub(longOrder.BaseAssetQuantity, filledAmount) + assert.Equal(t, expectedUnfilleAmount, output.Res.UnfilledAmount) + }) + t.Run("for a shortOrder it returns err = nil, with ammAddress and unfilled amount of cancelled Order", func(t *testing.T) { + shortOrder := getOrder(ammIndex, trader, shortBaseAssetQuantity, price, salt, reduceOnly, postOnly) + orderHash := getOrderHash(shortOrder) + filledAmount := hu.Div(shortOrder.BaseAssetQuantity, big.NewInt(2)) + + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Placed)).Times(1) + mockBibliophile.EXPECT().GetTimeStamp().Return(hu.V1ActivationTime).Times(1) + mockBibliophile.EXPECT().GetAvailableMargin(shortOrder.Trader, hu.V1).Return(big.NewInt(-1)).Times(1) + mockBibliophile.EXPECT().GetOrderFilledAmount(orderHash).Return(filledAmount).Times(1) + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(shortOrder.AmmIndex.Int64()).Return(ammAddress).Times(1) + mockBibliophile.EXPECT().IsValidator(shortOrder.Trader).Return(true).Times(1) + + input := getValidateCancelLimitOrderInput(shortOrder, trader, assertLowMargin) + output := ValidateCancelLimitOrder(mockBibliophile, &input) + assert.Equal(t, "", output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.OrderHash[:])) + assert.Equal(t, ammAddress, output.Res.Amm) + expectedUnfilleAmount := hu.Sub(shortOrder.BaseAssetQuantity, filledAmount) + assert.Equal(t, expectedUnfilleAmount, output.Res.UnfilledAmount) + }) + }) + }) + }) + t.Run("when assertLowMargin is false", func(t *testing.T) { + assertLowMargin := false + t.Run("for an unfilled Order", func(t *testing.T) { + t.Run("for a longOrder it returns err = nil, with ammAddress and unfilled amount of cancelled Order", func(t *testing.T) { + longOrder := getOrder(ammIndex, trader, longBaseAssetQuantity, price, salt, reduceOnly, postOnly) + orderHash := getOrderHash(longOrder) + + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Placed)).Times(1) + mockBibliophile.EXPECT().GetOrderFilledAmount(orderHash).Return(big.NewInt(0)).Times(1) + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(longOrder.AmmIndex.Int64()).Return(ammAddress).Times(1) + + input := getValidateCancelLimitOrderInput(longOrder, trader, assertLowMargin) + output := ValidateCancelLimitOrder(mockBibliophile, &input) + assert.Equal(t, "", output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.OrderHash[:])) + assert.Equal(t, ammAddress, output.Res.Amm) + assert.Equal(t, longOrder.BaseAssetQuantity, output.Res.UnfilledAmount) + }) + t.Run("for a shortOrder it returns err = nil, with ammAddress and unfilled amount of cancelled Order", func(t *testing.T) { + shortOrder := getOrder(ammIndex, trader, shortBaseAssetQuantity, price, salt, reduceOnly, postOnly) + orderHash := getOrderHash(shortOrder) + + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Placed)).Times(1) + mockBibliophile.EXPECT().GetOrderFilledAmount(orderHash).Return(big.NewInt(0)).Times(1) + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(shortOrder.AmmIndex.Int64()).Return(ammAddress).Times(1) + + input := getValidateCancelLimitOrderInput(shortOrder, trader, assertLowMargin) + output := ValidateCancelLimitOrder(mockBibliophile, &input) + assert.Equal(t, "", output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.OrderHash[:])) + assert.Equal(t, ammAddress, output.Res.Amm) + assert.Equal(t, shortOrder.BaseAssetQuantity, output.Res.UnfilledAmount) + }) + }) + t.Run("for a partially filled Order", func(t *testing.T) { + t.Run("for a longOrder it returns err = nil, with ammAddress and unfilled amount of cancelled Order", func(t *testing.T) { + longOrder := getOrder(ammIndex, trader, longBaseAssetQuantity, price, salt, reduceOnly, postOnly) + orderHash := getOrderHash(longOrder) + filledAmount := hu.Div(longOrder.BaseAssetQuantity, big.NewInt(2)) + + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Placed)).Times(1) + mockBibliophile.EXPECT().GetOrderFilledAmount(orderHash).Return(filledAmount).Times(1) + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(longOrder.AmmIndex.Int64()).Return(ammAddress).Times(1) + + input := getValidateCancelLimitOrderInput(longOrder, trader, assertLowMargin) + output := ValidateCancelLimitOrder(mockBibliophile, &input) + assert.Equal(t, "", output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.OrderHash[:])) + assert.Equal(t, ammAddress, output.Res.Amm) + expectedUnfilleAmount := hu.Sub(longOrder.BaseAssetQuantity, filledAmount) + assert.Equal(t, expectedUnfilleAmount, output.Res.UnfilledAmount) + }) + t.Run("for a shortOrder it returns err = nil, with ammAddress and unfilled amount of cancelled Order", func(t *testing.T) { + shortOrder := getOrder(ammIndex, trader, shortBaseAssetQuantity, price, salt, reduceOnly, postOnly) + orderHash := getOrderHash(shortOrder) + filledAmount := hu.Div(shortOrder.BaseAssetQuantity, big.NewInt(2)) + + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(Placed)).Times(1) + mockBibliophile.EXPECT().GetOrderFilledAmount(orderHash).Return(filledAmount).Times(1) + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(shortOrder.AmmIndex.Int64()).Return(ammAddress).Times(1) + + input := getValidateCancelLimitOrderInput(shortOrder, trader, assertLowMargin) + output := ValidateCancelLimitOrder(mockBibliophile, &input) + assert.Equal(t, "", output.Err) + assert.Equal(t, orderHash, common.BytesToHash(output.OrderHash[:])) + assert.Equal(t, ammAddress, output.Res.Amm) + expectedUnfilleAmount := hu.Sub(shortOrder.BaseAssetQuantity, filledAmount) + assert.Equal(t, expectedUnfilleAmount, output.Res.UnfilledAmount) + }) + }) + }) + }) + }) +} + +func getValidateCancelLimitOrderInput(order ILimitOrderBookOrder, sender common.Address, assertLowMargin bool) ValidateCancelLimitOrderInput { + return ValidateCancelLimitOrderInput{ + Order: order, + Sender: sender, + AssertLowMargin: assertLowMargin, + } +} + +func getOrder(ammIndex *big.Int, trader common.Address, baseAssetQuantity *big.Int, price *big.Int, salt *big.Int, reduceOnly bool, postOnly bool) ILimitOrderBookOrder { + return ILimitOrderBookOrder{ + AmmIndex: ammIndex, + BaseAssetQuantity: baseAssetQuantity, + Trader: trader, + Price: price, + Salt: salt, + ReduceOnly: reduceOnly, + PostOnly: postOnly, + } +} + +func getOrderHash(order ILimitOrderBookOrder) common.Hash { + orderHash, err := GetLimitOrderHashFromContractStruct(&order) + if err != nil { + panic("error in getting order hash") + } + return orderHash +} diff --git a/precompile/contracts/juror/matching_validation.go b/precompile/contracts/juror/matching_validation.go new file mode 100644 index 0000000000..3a26152033 --- /dev/null +++ b/precompile/contracts/juror/matching_validation.go @@ -0,0 +1,525 @@ +package juror + +import ( + "errors" + "math/big" + + hu "github.com/ava-labs/subnet-evm/plugin/evm/orderbook/hubbleutils" + b "github.com/ava-labs/subnet-evm/precompile/contracts/bibliophile" + "github.com/ava-labs/subnet-evm/utils" + "github.com/ethereum/go-ethereum/common" +) + +type Metadata struct { + AmmIndex *big.Int + Trader common.Address + BaseAssetQuantity *big.Int + Price *big.Int + BlockPlaced *big.Int + OrderHash common.Hash + OrderType hu.OrderType + PostOnly bool +} + +type Side uint8 + +const ( + Long Side = iota + Short + Liquidation +) + +type OrderStatus uint8 + +// has to be exact same as IOrderHandler +const ( + Invalid OrderStatus = iota + Placed + Filled + Cancelled +) + +var ( + ErrTwoOrders = errors.New("need 2 orders") + ErrInvalidFillAmount = errors.New("invalid fillAmount") + ErrNotLongOrder = errors.New("not long") + ErrNotShortOrder = errors.New("not short") + ErrNotSameAMM = errors.New("OB_orders_for_different_amms") + ErrNoMatch = errors.New("OB_orders_do_not_match") + ErrNotMultiple = errors.New("not multiple") + + ErrInvalidOrder = errors.New("invalid order") + ErrNotIOCOrder = errors.New("not_ioc_order") + ErrInvalidPrice = errors.New("invalid price") + ErrPricePrecision = errors.New("invalid price precision") + ErrInvalidMarket = errors.New("invalid market") + ErrCancelledOrder = errors.New("cancelled order") + ErrFilledOrder = errors.New("filled order") + ErrOrderAlreadyExists = errors.New("order already exists") + ErrTooLow = errors.New("long price below lower bound") + ErrTooHigh = errors.New("short price above upper bound") + ErrOverFill = errors.New("overfill") + ErrReduceOnlyAmountExceeded = errors.New("not reducing pos") + ErrBaseAssetQuantityZero = errors.New("baseAssetQuantity is zero") + ErrReduceOnlyBaseAssetQuantityInvalid = errors.New("reduce only order must reduce position") + ErrNetReduceOnlyAmountExceeded = errors.New("net reduce only amount exceeded") + ErrStaleReduceOnlyOrders = errors.New("cancel stale reduce only orders") + ErrInsufficientMargin = errors.New("insufficient margin") + ErrCrossingMarket = errors.New("crossing market") + ErrIOCOrderExpired = errors.New("IOC order expired") + ErrOpenOrders = errors.New("open orders") + ErrOpenReduceOnlyOrders = errors.New("open reduce only orders") + ErrNoTradingAuthority = errors.New("no trading authority") + ErrNoReferrer = errors.New("no referrer") +) + +type BadElement uint8 + +// DO NOT change this ordering because it is critical for the orderbook to determine the problematic order +const ( + Order0 BadElement = iota + Order1 + Generic + NoError +) + +// Business Logic +func ValidateOrdersAndDetermineFillPrice(bibliophile b.BibliophileClient, inputStruct *ValidateOrdersAndDetermineFillPriceInput) ValidateOrdersAndDetermineFillPriceOutput { + if len(inputStruct.Data) != 2 { + return getValidateOrdersAndDetermineFillPriceErrorOutput(ErrTwoOrders, Generic, common.Hash{}) + } + + if inputStruct.FillAmount.Sign() <= 0 { + return getValidateOrdersAndDetermineFillPriceErrorOutput(ErrInvalidFillAmount, Generic, common.Hash{}) + } + + decodeStep0, err := hu.DecodeTypeAndEncodedOrder(inputStruct.Data[0]) + if err != nil { + return getValidateOrdersAndDetermineFillPriceErrorOutput(err, Order0, common.Hash{}) + } + m0, err := validateOrder(bibliophile, decodeStep0.OrderType, decodeStep0.EncodedOrder, Long, inputStruct.FillAmount) + if err != nil { + return getValidateOrdersAndDetermineFillPriceErrorOutput(err, Order0, m0.OrderHash) + } + + decodeStep1, err := hu.DecodeTypeAndEncodedOrder(inputStruct.Data[1]) + if err != nil { + return getValidateOrdersAndDetermineFillPriceErrorOutput(err, Order1, common.Hash{}) + } + m1, err := validateOrder(bibliophile, decodeStep1.OrderType, decodeStep1.EncodedOrder, Short, new(big.Int).Neg(inputStruct.FillAmount)) + if err != nil { + return getValidateOrdersAndDetermineFillPriceErrorOutput(err, Order1, m1.OrderHash) + } + + if m0.AmmIndex.Cmp(m1.AmmIndex) != 0 { + return getValidateOrdersAndDetermineFillPriceErrorOutput(ErrNotSameAMM, Generic, common.Hash{}) + } + + if m0.Price.Cmp(m1.Price) < 0 { + return getValidateOrdersAndDetermineFillPriceErrorOutput(ErrNoMatch, Generic, common.Hash{}) + } + + minSize := bibliophile.GetMinSizeRequirement(m0.AmmIndex.Int64()) + if new(big.Int).Mod(inputStruct.FillAmount, minSize).Cmp(big.NewInt(0)) != 0 { + return getValidateOrdersAndDetermineFillPriceErrorOutput(ErrNotMultiple, Generic, common.Hash{}) + } + + fillPriceAndModes, err, element := determineFillPrice(bibliophile, m0, m1) + if err != nil { + orderHash := common.Hash{} + if element == Order0 { + orderHash = m0.OrderHash + } else if element == Order1 { + orderHash = m1.OrderHash + } + return getValidateOrdersAndDetermineFillPriceErrorOutput(err, element, orderHash) + } + + return ValidateOrdersAndDetermineFillPriceOutput{ + Err: "", + Element: uint8(NoError), + Res: IOrderHandlerMatchingValidationRes{ + Instructions: [2]IClearingHouseInstruction{ + IClearingHouseInstruction{ + AmmIndex: m0.AmmIndex, + Trader: m0.Trader, + OrderHash: m0.OrderHash, + Mode: uint8(fillPriceAndModes.Mode0), + }, + IClearingHouseInstruction{ + AmmIndex: m1.AmmIndex, + Trader: m1.Trader, + OrderHash: m1.OrderHash, + Mode: uint8(fillPriceAndModes.Mode1), + }, + }, + OrderTypes: [2]uint8{uint8(decodeStep0.OrderType), uint8(decodeStep1.OrderType)}, + EncodedOrders: [2][]byte{ + decodeStep0.EncodedOrder, + decodeStep1.EncodedOrder, + }, + FillPrice: fillPriceAndModes.FillPrice, + }, + } +} + +type executionMode uint8 + +// DO NOT change this ordering because it is critical for the clearing house to determine the correct fill mode +const ( + Taker executionMode = iota + Maker +) + +type FillPriceAndModes struct { + FillPrice *big.Int + Mode0 executionMode + Mode1 executionMode +} + +func determineFillPrice(bibliophile b.BibliophileClient, m0, m1 *Metadata) (*FillPriceAndModes, error, BadElement) { + output := FillPriceAndModes{} + upperBound, lowerBound := bibliophile.GetUpperAndLowerBoundForMarket(m0.AmmIndex.Int64()) + if m0.Price.Cmp(lowerBound) == -1 { + return nil, ErrTooLow, Order0 + } + if m1.Price.Cmp(upperBound) == 1 { + return nil, ErrTooHigh, Order1 + } + + blockDiff := m0.BlockPlaced.Cmp(m1.BlockPlaced) + if blockDiff == -1 { + // order0 came first, can't be IOC order + if m0.OrderType == hu.IOC { + return nil, ErrIOCOrderExpired, Order0 + } + // order1 came second, can't be post only order + if m1.OrderType == hu.Limit && m1.PostOnly { + return nil, ErrCrossingMarket, Order1 + } + output.Mode0 = Maker + output.Mode1 = Taker + } else if blockDiff == 1 { + // order1 came first, can't be IOC order + if m1.OrderType == hu.IOC { + return nil, ErrIOCOrderExpired, Order1 + } + // order0 came second, can't be post only order + if m0.OrderType == hu.Limit && m0.PostOnly { + return nil, ErrCrossingMarket, Order0 + } + output.Mode0 = Taker + output.Mode1 = Maker + } else { + // both orders were placed in same block + if m1.OrderType == hu.IOC { + // order1 is IOC, order0 is Limit or post only + output.Mode0 = Maker + output.Mode1 = Taker + } else { + // scenarios: + // 1. order0 is IOC, order1 is Limit or post only + // 2. both order0 and order1 are Limit or post only (in that scenario we default to long being the taker order, which can sometimes result in a better execution price for them) + output.Mode0 = Taker + output.Mode1 = Maker + } + } + + if output.Mode0 == Maker { + output.FillPrice = utils.BigIntMin(m0.Price, upperBound) + } else { + output.FillPrice = utils.BigIntMax(m1.Price, lowerBound) + } + return &output, nil, NoError +} + +func ValidateLiquidationOrderAndDetermineFillPrice(bibliophile b.BibliophileClient, inputStruct *ValidateLiquidationOrderAndDetermineFillPriceInput) ValidateLiquidationOrderAndDetermineFillPriceOutput { + fillAmount := new(big.Int).Set(inputStruct.LiquidationAmount) + if fillAmount.Sign() <= 0 { + return getValidateLiquidationOrderAndDetermineFillPriceErrorOutput(ErrInvalidFillAmount, Generic, common.Hash{}) + } + + decodeStep0, err := hu.DecodeTypeAndEncodedOrder(inputStruct.Data) + if err != nil { + return getValidateLiquidationOrderAndDetermineFillPriceErrorOutput(err, Order0, common.Hash{}) + } + m0, err := validateOrder(bibliophile, decodeStep0.OrderType, decodeStep0.EncodedOrder, Liquidation, fillAmount) + if err != nil { + return getValidateLiquidationOrderAndDetermineFillPriceErrorOutput(err, Order0, m0.OrderHash) + } + + if m0.BaseAssetQuantity.Sign() < 0 { + fillAmount = new(big.Int).Neg(fillAmount) + } + + minSize := bibliophile.GetMinSizeRequirement(m0.AmmIndex.Int64()) + if new(big.Int).Mod(fillAmount, minSize).Cmp(big.NewInt(0)) != 0 { + return getValidateLiquidationOrderAndDetermineFillPriceErrorOutput(ErrNotMultiple, Generic, common.Hash{}) + } + + fillPrice, err := determineLiquidationFillPrice(bibliophile, m0) + if err != nil { + return getValidateLiquidationOrderAndDetermineFillPriceErrorOutput(err, Order0, m0.OrderHash) + } + + return ValidateLiquidationOrderAndDetermineFillPriceOutput{ + Err: "", + Element: uint8(NoError), + Res: IOrderHandlerLiquidationMatchingValidationRes{ + Instruction: IClearingHouseInstruction{ + AmmIndex: m0.AmmIndex, + Trader: m0.Trader, + OrderHash: m0.OrderHash, + Mode: uint8(Maker), + }, + OrderType: uint8(decodeStep0.OrderType), + EncodedOrder: decodeStep0.EncodedOrder, + FillPrice: fillPrice, + FillAmount: fillAmount, + }, + } +} + +func determineLiquidationFillPrice(bibliophile b.BibliophileClient, m0 *Metadata) (*big.Int, error) { + liqUpperBound, liqLowerBound := bibliophile.GetAcceptableBoundsForLiquidation(m0.AmmIndex.Int64()) + upperBound, lowerBound := bibliophile.GetUpperAndLowerBoundForMarket(m0.AmmIndex.Int64()) + if m0.BaseAssetQuantity.Sign() > 0 { + // we are liquidating a long position + // do not allow liquidation if order.Price < liqLowerBound, because that gives scope for malicious activity to a validator + if m0.Price.Cmp(liqLowerBound) == -1 { + return nil, ErrTooLow + } + return utils.BigIntMin(m0.Price, upperBound /* oracle spread upper bound */), nil + } + + // we are liquidating a short position + if m0.Price.Cmp(liqUpperBound) == 1 { + return nil, ErrTooHigh + } + return utils.BigIntMax(m0.Price, lowerBound /* oracle spread lower bound */), nil +} + +func validateOrder(bibliophile b.BibliophileClient, orderType hu.OrderType, encodedOrder []byte, side Side, fillAmount *big.Int) (metadata *Metadata, err error) { + if orderType == hu.Limit { + order, err := hu.DecodeLimitOrder(encodedOrder) + if err != nil { + return nil, err + } + return validateExecuteLimitOrder(bibliophile, order, side, fillAmount) + } + if orderType == hu.IOC { + order, err := hu.DecodeIOCOrder(encodedOrder) + if err != nil { + return nil, err + } + return validateExecuteIOCOrder(bibliophile, order, side, fillAmount) + } + return nil, errors.New("invalid order type") +} + +func validateExecuteLimitOrder(bibliophile b.BibliophileClient, order *hu.LimitOrder, side Side, fillAmount *big.Int) (metadata *Metadata, err error) { + orderHash, err := order.Hash() + if err != nil { + return nil, err + } + if err := validateLimitOrderLike(bibliophile, &order.BaseOrder, bibliophile.GetOrderFilledAmount(orderHash), OrderStatus(bibliophile.GetOrderStatus(orderHash)), side, fillAmount); err != nil { + return &Metadata{OrderHash: orderHash}, err + } + return &Metadata{ + AmmIndex: order.AmmIndex, + Trader: order.Trader, + BaseAssetQuantity: order.BaseAssetQuantity, + BlockPlaced: bibliophile.GetBlockPlaced(orderHash), + Price: order.Price, + OrderHash: orderHash, + OrderType: hu.Limit, + PostOnly: order.PostOnly, + }, nil +} + +func validateExecuteIOCOrder(bibliophile b.BibliophileClient, order *hu.IOCOrder, side Side, fillAmount *big.Int) (metadata *Metadata, err error) { + orderHash, err := order.Hash() + if err != nil { + return nil, err + } + if hu.OrderType(order.OrderType) != hu.IOC { + return &Metadata{OrderHash: orderHash}, errors.New("not ioc order") + } + if order.ExpireAt.Uint64() < bibliophile.GetTimeStamp() { + return &Metadata{OrderHash: orderHash}, errors.New("ioc expired") + } + if err := validateLimitOrderLike(bibliophile, &order.BaseOrder, bibliophile.IOC_GetOrderFilledAmount(orderHash), OrderStatus(bibliophile.IOC_GetOrderStatus(orderHash)), side, fillAmount); err != nil { + return &Metadata{OrderHash: orderHash}, err + } + return &Metadata{ + AmmIndex: order.AmmIndex, + Trader: order.Trader, + BaseAssetQuantity: order.BaseAssetQuantity, + BlockPlaced: bibliophile.IOC_GetBlockPlaced(orderHash), + Price: order.Price, + OrderHash: orderHash, + OrderType: hu.IOC, + PostOnly: false, + }, nil +} + +func validateLimitOrderLike(bibliophile b.BibliophileClient, order *hu.BaseOrder, filledAmount *big.Int, status OrderStatus, side Side, fillAmount *big.Int) error { + if status != Placed { + return ErrInvalidOrder + } + + // in case of liquidations, side of the order is determined by the sign of the base asset quantity, so basically base asset quantity check is redundant + if side == Liquidation { + if order.BaseAssetQuantity.Sign() > 0 { + side = Long + } else if order.BaseAssetQuantity.Sign() < 0 { + side = Short + fillAmount = new(big.Int).Neg(fillAmount) + } + } + + market := bibliophile.GetMarketAddressFromMarketID(order.AmmIndex.Int64()) + if side == Long { + if order.BaseAssetQuantity.Sign() <= 0 { + return ErrNotLongOrder + } + if fillAmount.Sign() <= 0 { + return ErrInvalidFillAmount + } + if new(big.Int).Add(filledAmount, fillAmount).Cmp(order.BaseAssetQuantity) > 0 { + return ErrOverFill + } + if order.ReduceOnly { + posSize := bibliophile.GetSize(market, &order.Trader) + // posSize should be closed to continue to be Short + // this also returns err if posSize >= 0, which should not happen because we are executing a long reduceOnly order on this account + if new(big.Int).Add(posSize, fillAmount).Sign() > 0 { + return ErrReduceOnlyAmountExceeded + } + } + } else if side == Short { + if order.BaseAssetQuantity.Sign() >= 0 { + return ErrNotShortOrder + } + if fillAmount.Sign() >= 0 { + return ErrInvalidFillAmount + } + if new(big.Int).Add(filledAmount, fillAmount).Cmp(order.BaseAssetQuantity) < 0 { // all quantities are -ve + return ErrOverFill + } + if order.ReduceOnly { + posSize := bibliophile.GetSize(market, &order.Trader) + // posSize should continue to be Long + // this also returns is posSize <= 0, which should not happen because we are executing a short reduceOnly order on this account + if new(big.Int).Add(posSize, fillAmount).Sign() < 0 { + return ErrReduceOnlyAmountExceeded + } + } + } else { + return errors.New("invalid side") + } + return nil +} + +// Common + +func reducesPosition(positionSize *big.Int, baseAssetQuantity *big.Int) bool { + if positionSize.Sign() == 1 && baseAssetQuantity.Sign() == -1 && big.NewInt(0).Add(positionSize, baseAssetQuantity).Sign() != -1 { + return true + } + if positionSize.Sign() == -1 && baseAssetQuantity.Sign() == 1 && big.NewInt(0).Add(positionSize, baseAssetQuantity).Sign() != 1 { + return true + } + return false +} + +func getRequiredMargin(bibliophile b.BibliophileClient, order ILimitOrderBookOrder) *big.Int { + price := order.Price + upperBound, _ := bibliophile.GetUpperAndLowerBoundForMarket(order.AmmIndex.Int64()) + if order.BaseAssetQuantity.Sign() == -1 && order.Price.Cmp(upperBound) == -1 { + price = upperBound + } + quoteAsset := big.NewInt(0).Abs(big.NewInt(0).Div(new(big.Int).Mul(order.BaseAssetQuantity, price), big.NewInt(1e18))) + requiredMargin := big.NewInt(0).Div(big.NewInt(0).Mul(bibliophile.GetMinAllowableMargin(), quoteAsset), big.NewInt(1e6)) + takerFee := big.NewInt(0).Div(big.NewInt(0).Mul(quoteAsset, bibliophile.GetTakerFee()), big.NewInt(1e6)) + requiredMargin.Add(requiredMargin, takerFee) + return requiredMargin +} + +func formatOrder(orderBytes []byte) interface{} { + decodeStep0, err := hu.DecodeTypeAndEncodedOrder(orderBytes) + if err != nil { + return orderBytes + } + + if decodeStep0.OrderType == hu.Limit { + order, err := hu.DecodeLimitOrder(decodeStep0.EncodedOrder) + if err != nil { + return decodeStep0 + } + orderJson := order.Map() + orderHash, err := order.Hash() + if err != nil { + return orderJson + } + orderJson["hash"] = orderHash.String() + return orderJson + } + if decodeStep0.OrderType == hu.IOC { + order, err := hu.DecodeIOCOrder(decodeStep0.EncodedOrder) + if err != nil { + return decodeStep0 + } + orderJson := order.Map() + orderHash, err := order.Hash() + if err != nil { + return orderJson + } + orderJson["hash"] = orderHash.String() + return orderJson + } + return nil +} + +func getValidateOrdersAndDetermineFillPriceErrorOutput(err error, element BadElement, orderHash common.Hash) ValidateOrdersAndDetermineFillPriceOutput { + // need to provide an empty res because PackValidateOrdersAndDetermineFillPriceOutput fails if FillPrice is nil, and if res.Instructions[0].AmmIndex is nil + emptyRes := IOrderHandlerMatchingValidationRes{ + Instructions: [2]IClearingHouseInstruction{ + IClearingHouseInstruction{AmmIndex: big.NewInt(0)}, + IClearingHouseInstruction{AmmIndex: big.NewInt(0)}, + }, + OrderTypes: [2]uint8{}, + EncodedOrders: [2][]byte{}, + FillPrice: big.NewInt(0), + } + + var errorString string + if err != nil { + // should always be true + errorString = err.Error() + } + if (element == Order0 || element == Order1) && orderHash != (common.Hash{}) { + emptyRes.Instructions[element].OrderHash = orderHash + } + return ValidateOrdersAndDetermineFillPriceOutput{Err: errorString, Element: uint8(element), Res: emptyRes} +} + +func getValidateLiquidationOrderAndDetermineFillPriceErrorOutput(err error, element BadElement, orderHash common.Hash) ValidateLiquidationOrderAndDetermineFillPriceOutput { + emptyRes := IOrderHandlerLiquidationMatchingValidationRes{ + Instruction: IClearingHouseInstruction{AmmIndex: big.NewInt(0)}, + OrderType: 0, + EncodedOrder: []byte{}, + FillPrice: big.NewInt(0), + FillAmount: big.NewInt(0), + } + + var errorString string + if err != nil { + // should always be true + errorString = err.Error() + } + if element == Order0 && orderHash != (common.Hash{}) { + emptyRes.Instruction.OrderHash = orderHash + } + return ValidateLiquidationOrderAndDetermineFillPriceOutput{Err: errorString, Element: uint8(element), Res: emptyRes} +} diff --git a/precompile/contracts/juror/matching_validation_test.go b/precompile/contracts/juror/matching_validation_test.go new file mode 100644 index 0000000000..3abd025bd0 --- /dev/null +++ b/precompile/contracts/juror/matching_validation_test.go @@ -0,0 +1,1147 @@ +// Code generated +// This file is a generated precompile contract test with the skeleton of test functions. +// The file is generated by a template. Please inspect every code and comment in this file before use. + +package juror + +import ( + "fmt" + "math/big" + + "testing" + + ob "github.com/ava-labs/subnet-evm/plugin/evm/orderbook" + hu "github.com/ava-labs/subnet-evm/plugin/evm/orderbook/hubbleutils" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/assert" + + b "github.com/ava-labs/subnet-evm/precompile/contracts/bibliophile" + gomock "github.com/golang/mock/gomock" +) + +func TestValidateLimitOrderLike(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockBibliophile := b.NewMockBibliophileClient(ctrl) + + trader := common.HexToAddress("0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC") + order := &hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(10), + Price: big.NewInt(20), + Salt: big.NewInt(1), + ReduceOnly: false, + } + filledAmount := big.NewInt(5) + fillAmount := big.NewInt(5) + + t.Run("Side=Long", func(t *testing.T) { + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(gomock.Any()).Return(common.Address{}).AnyTimes() + t.Run("OrderStatus != Placed will throw error", func(t *testing.T) { + err := validateLimitOrderLike(mockBibliophile, order, filledAmount, Invalid, Long, fillAmount) + assert.EqualError(t, err, ErrInvalidOrder.Error()) + + err = validateLimitOrderLike(mockBibliophile, order, filledAmount, Filled, Long, fillAmount) + assert.EqualError(t, err, ErrInvalidOrder.Error()) + + err = validateLimitOrderLike(mockBibliophile, order, filledAmount, Cancelled, Long, fillAmount) + assert.EqualError(t, err, ErrInvalidOrder.Error()) + }) + + t.Run("base asset quantity <= 0", func(t *testing.T) { + badOrder := *order + badOrder.BaseAssetQuantity = big.NewInt(-23) + + err := validateLimitOrderLike(mockBibliophile, &badOrder, filledAmount, Placed, Long, fillAmount) + assert.EqualError(t, err, ErrNotLongOrder.Error()) + + badOrder.BaseAssetQuantity = big.NewInt(0) + err = validateLimitOrderLike(mockBibliophile, &badOrder, filledAmount, Placed, Long, fillAmount) + assert.EqualError(t, err, ErrNotLongOrder.Error()) + }) + + t.Run("ErrOverFill", func(t *testing.T) { + fillAmount := big.NewInt(6) + + err := validateLimitOrderLike(mockBibliophile, order, filledAmount, Placed, Long, fillAmount) + assert.EqualError(t, err, ErrOverFill.Error()) + }) + + t.Run("negative fillAmount", func(t *testing.T) { + fillAmount := big.NewInt(-6) + + err := validateLimitOrderLike(mockBibliophile, order, filledAmount, Placed, Long, fillAmount) + assert.EqualError(t, err, ErrInvalidFillAmount.Error()) + }) + + t.Run("ErrReduceOnlyAmountExceeded", func(t *testing.T) { + badOrder := *order + badOrder.ReduceOnly = true + + for i := int64(10); /* any +ve # */ i > new(big.Int).Neg(fillAmount).Int64(); i-- { + mockBibliophile.EXPECT().GetSize(gomock.Any(), gomock.Any()).Return(big.NewInt(i)).Times(1) + err := validateLimitOrderLike(mockBibliophile, &badOrder, filledAmount, Placed, Long, fillAmount) + assert.EqualError(t, err, ErrReduceOnlyAmountExceeded.Error()) + } + }) + + t.Run("all conditions met for reduceOnly order", func(t *testing.T) { + badOrder := *order + badOrder.ReduceOnly = true + + start := new(big.Int).Neg(fillAmount).Int64() + for i := start; i > start-5; i-- { + mockBibliophile.EXPECT().GetSize(gomock.Any(), gomock.Any()).Return(big.NewInt(i)).Times(1) + err := validateLimitOrderLike(mockBibliophile, &badOrder, filledAmount, Placed, Long, fillAmount) + assert.Nil(t, err) + } + }) + + t.Run("all conditions met", func(t *testing.T) { + err := validateLimitOrderLike(mockBibliophile, order, filledAmount, Placed, Long, fillAmount) + assert.Nil(t, err) + }) + }) + + t.Run("Side=Short", func(t *testing.T) { + order := &hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(-10), + Price: big.NewInt(20), + Salt: big.NewInt(1), + ReduceOnly: false, + } + filledAmount := big.NewInt(-5) + fillAmount := big.NewInt(-5) + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(gomock.Any()).Return(common.Address{}).AnyTimes() + t.Run("OrderStatus != Placed will throw error", func(t *testing.T) { + err := validateLimitOrderLike(mockBibliophile, order, filledAmount, Invalid, Short, fillAmount) + assert.EqualError(t, err, ErrInvalidOrder.Error()) + + err = validateLimitOrderLike(mockBibliophile, order, filledAmount, Filled, Short, fillAmount) + assert.EqualError(t, err, ErrInvalidOrder.Error()) + + err = validateLimitOrderLike(mockBibliophile, order, filledAmount, Cancelled, Short, fillAmount) + assert.EqualError(t, err, ErrInvalidOrder.Error()) + }) + + t.Run("base asset quantity >= 0", func(t *testing.T) { + badOrder := *order + badOrder.BaseAssetQuantity = big.NewInt(23) + + err := validateLimitOrderLike(mockBibliophile, &badOrder, filledAmount, Placed, Short, fillAmount) + assert.EqualError(t, err, ErrNotShortOrder.Error()) + + badOrder.BaseAssetQuantity = big.NewInt(0) + err = validateLimitOrderLike(mockBibliophile, &badOrder, filledAmount, Placed, Short, fillAmount) + assert.EqualError(t, err, ErrNotShortOrder.Error()) + }) + + t.Run("positive fillAmount", func(t *testing.T) { + fillAmount := big.NewInt(6) + + err := validateLimitOrderLike(mockBibliophile, order, filledAmount, Placed, Short, fillAmount) + assert.EqualError(t, err, ErrInvalidFillAmount.Error()) + }) + + t.Run("ErrOverFill", func(t *testing.T) { + fillAmount := big.NewInt(-6) + + err := validateLimitOrderLike(mockBibliophile, order, filledAmount, Placed, Short, fillAmount) + assert.EqualError(t, err, ErrOverFill.Error()) + }) + + t.Run("ErrReduceOnlyAmountExceeded", func(t *testing.T) { + badOrder := *order + badOrder.ReduceOnly = true + + for i := int64(-10); /* any -ve # */ i < new(big.Int).Abs(fillAmount).Int64(); i++ { + mockBibliophile.EXPECT().GetSize(gomock.Any(), gomock.Any()).Return(big.NewInt(i)).Times(1) + err := validateLimitOrderLike(mockBibliophile, &badOrder, filledAmount, Placed, Short, fillAmount) + assert.EqualError(t, err, ErrReduceOnlyAmountExceeded.Error()) + } + }) + + t.Run("all conditions met for reduceOnly order", func(t *testing.T) { + badOrder := *order + badOrder.ReduceOnly = true + + start := new(big.Int).Abs(fillAmount).Int64() + for i := start; i < start+5; i++ { + mockBibliophile.EXPECT().GetSize(gomock.Any(), gomock.Any()).Return(big.NewInt(i)).Times(1) + err := validateLimitOrderLike(mockBibliophile, &badOrder, filledAmount, Placed, Short, fillAmount) + assert.Nil(t, err) + } + }) + + t.Run("all conditions met", func(t *testing.T) { + err := validateLimitOrderLike(mockBibliophile, order, filledAmount, Placed, Short, fillAmount) + assert.Nil(t, err) + }) + }) + + t.Run("invalid side", func(t *testing.T) { + order := &hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(10), + Price: big.NewInt(20), + Salt: big.NewInt(1), + ReduceOnly: false, + } + filledAmount := big.NewInt(0) + fillAmount := big.NewInt(5) + + err := validateLimitOrderLike(mockBibliophile, order, filledAmount, Placed, Side(4), fillAmount) // assuming 4 is an invalid Side value + assert.EqualError(t, err, "invalid side") + }) +} + +func TestValidateExecuteLimitOrder(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockBibliophile := b.NewMockBibliophileClient(ctrl) + marketAddress := common.HexToAddress("0xa72b463C21dA61cCc86069cFab82e9e8491152a0") + trader := common.HexToAddress("0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC") + + order := &hu.LimitOrder{ + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(534), + Trader: trader, + BaseAssetQuantity: big.NewInt(10), + Price: big.NewInt(20), + Salt: big.NewInt(1), + ReduceOnly: false, + }, + PostOnly: false, + } + filledAmount := big.NewInt(5) + fillAmount := big.NewInt(5) + + t.Run("validateExecuteLimitOrder", func(t *testing.T) { + orderHash, err := order.Hash() + assert.Nil(t, err) + + blockPlaced := big.NewInt(42) + mockBibliophile.EXPECT().GetOrderFilledAmount(orderHash).Return(filledAmount).Times(1) + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(1)).Times(1) // placed + mockBibliophile.EXPECT().GetBlockPlaced(orderHash).Return(blockPlaced).Times(1) // placed + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(order.AmmIndex.Int64()).Return(marketAddress).Times(1) // placed + + m, err := validateExecuteLimitOrder(mockBibliophile, order, Long, fillAmount) + assert.Nil(t, err) + assertMetadataEquality(t, &Metadata{ + AmmIndex: new(big.Int).Set(order.AmmIndex), + Trader: trader, + BaseAssetQuantity: new(big.Int).Set(order.BaseAssetQuantity), + BlockPlaced: blockPlaced, + Price: new(big.Int).Set(order.Price), + OrderHash: orderHash, + }, m) + }) + + t.Run("validateExecuteLimitOrder returns orderHash even when validation fails", func(t *testing.T) { + orderHash, err := order.Hash() + assert.Nil(t, err) + + mockBibliophile.EXPECT().GetOrderFilledAmount(orderHash).Return(filledAmount).Times(1) + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(2)).Times(1) // Filled + + m, err := validateExecuteLimitOrder(mockBibliophile, order, Long, fillAmount) + assert.EqualError(t, err, ErrInvalidOrder.Error()) + assert.Equal(t, orderHash, m.OrderHash) + }) +} + +func assertMetadataEquality(t *testing.T, expected, actual *Metadata) { + assert.Equal(t, expected.AmmIndex.Int64(), actual.AmmIndex.Int64()) + assert.Equal(t, expected.Trader, actual.Trader) + assert.Equal(t, expected.BaseAssetQuantity, actual.BaseAssetQuantity) + assert.Equal(t, expected.BlockPlaced, actual.BlockPlaced) + assert.Equal(t, expected.Price, actual.Price) + assert.Equal(t, expected.OrderHash, actual.OrderHash) +} + +func TestDetermineFillPrice(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockBibliophile := b.NewMockBibliophileClient(ctrl) + + oraclePrice := hu.Mul1e6(big.NewInt(20)) // $10 + spreadLimit := new(big.Int).Mul(big.NewInt(50), big.NewInt(1e4)) // 50% + upperbound := hu.Div1e6(new(big.Int).Mul(oraclePrice, new(big.Int).Add(big.NewInt(1e6), spreadLimit))) // $10 + lowerbound := hu.Div1e6(new(big.Int).Mul(oraclePrice, new(big.Int).Sub(big.NewInt(1e6), spreadLimit))) // $30 + market := int64(5) + + t.Run("long order came first", func(t *testing.T) { + blockPlaced0 := big.NewInt(69) + blockPlaced1 := big.NewInt(70) + t.Run("long price < lower bound", func(t *testing.T) { + t.Run("short price < long price", func(t *testing.T) { + m0 := &Metadata{ + Price: hu.Mul1e6(big.NewInt(9)), + AmmIndex: big.NewInt(market), + BlockPlaced: blockPlaced0, + } + m1 := &Metadata{ + Price: hu.Mul1e6(big.NewInt(8)), + BlockPlaced: blockPlaced1, + } + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(market).Return(upperbound, lowerbound).Times(1) + output, err, _ := determineFillPrice(mockBibliophile, m0, m1) + assert.Nil(t, output) + assert.Equal(t, ErrTooLow, err) + }) + + t.Run("short price == long price", func(t *testing.T) { + m0 := &Metadata{ + Price: hu.Mul1e6(big.NewInt(7)), + AmmIndex: big.NewInt(market), + BlockPlaced: blockPlaced0, + } + m1 := &Metadata{ + Price: hu.Mul1e6(big.NewInt(7)), + BlockPlaced: blockPlaced1, + } + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(market).Return(upperbound, lowerbound).Times(1) + output, err, _ := determineFillPrice(mockBibliophile, m0, m1) + assert.Nil(t, output) + assert.Equal(t, ErrTooLow, err) + }) + }) + + t.Run("long price == lower bound", func(t *testing.T) { + longPrice := lowerbound + t.Run("short price < long price", func(t *testing.T) { + m0 := &Metadata{ + Price: longPrice, + AmmIndex: big.NewInt(market), + BlockPlaced: blockPlaced0, + } + m1 := &Metadata{ + Price: new(big.Int).Sub(longPrice, big.NewInt(1)), + BlockPlaced: blockPlaced1, + } + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(market).Return(upperbound, lowerbound).Times(1) + output, err, _ := determineFillPrice(mockBibliophile, m0, m1) + assert.Nil(t, err) + assert.Equal(t, FillPriceAndModes{longPrice, Maker, Taker}, *output) + }) + + t.Run("short price == long price", func(t *testing.T) { + m0 := &Metadata{ + Price: longPrice, + AmmIndex: big.NewInt(market), + BlockPlaced: blockPlaced0, + } + m1 := &Metadata{ + Price: longPrice, + BlockPlaced: blockPlaced1, + } + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(market).Return(upperbound, lowerbound).Times(1) + output, err, _ := determineFillPrice(mockBibliophile, m0, m1) + assert.Nil(t, err) + assert.Equal(t, FillPriceAndModes{longPrice, Maker, Taker}, *output) + }) + }) + + t.Run("lowerbound < long price < oracle", func(t *testing.T) { + longPrice := hu.Mul1e6(big.NewInt(15)) + t.Run("short price < long price", func(t *testing.T) { + m0 := &Metadata{ + Price: longPrice, + AmmIndex: big.NewInt(market), + BlockPlaced: blockPlaced0, + } + m1 := &Metadata{ + Price: new(big.Int).Sub(longPrice, big.NewInt(1)), + BlockPlaced: blockPlaced1, + } + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(market).Return(upperbound, lowerbound).Times(1) + output, err, _ := determineFillPrice(mockBibliophile, m0, m1) + assert.Nil(t, err) + assert.Equal(t, FillPriceAndModes{longPrice, Maker, Taker}, *output) + }) + + t.Run("short price == long price", func(t *testing.T) { + m0 := &Metadata{ + Price: longPrice, + AmmIndex: big.NewInt(market), + BlockPlaced: blockPlaced0, + } + m1 := &Metadata{ + Price: longPrice, + BlockPlaced: blockPlaced1, + } + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(market).Return(upperbound, lowerbound).Times(1) + output, err, _ := determineFillPrice(mockBibliophile, m0, m1) + assert.Nil(t, err) + assert.Equal(t, FillPriceAndModes{longPrice, Maker, Taker}, *output) + }) + }) + + t.Run("long price == oracle", func(t *testing.T) { + longPrice := oraclePrice + t.Run("short price < long price", func(t *testing.T) { + m0 := &Metadata{ + Price: longPrice, + AmmIndex: big.NewInt(market), + BlockPlaced: blockPlaced0, + } + m1 := &Metadata{ + Price: new(big.Int).Sub(longPrice, big.NewInt(1)), + BlockPlaced: blockPlaced1, + } + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(market).Return(upperbound, lowerbound).Times(1) + output, err, _ := determineFillPrice(mockBibliophile, m0, m1) + assert.Nil(t, err) + assert.Equal(t, FillPriceAndModes{longPrice, Maker, Taker}, *output) + }) + + t.Run("short price == long price", func(t *testing.T) { + m0 := &Metadata{ + Price: longPrice, + AmmIndex: big.NewInt(market), + BlockPlaced: blockPlaced0, + } + m1 := &Metadata{ + Price: longPrice, + BlockPlaced: blockPlaced1, + } + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(market).Return(upperbound, lowerbound).Times(1) + output, err, _ := determineFillPrice(mockBibliophile, m0, m1) + assert.Nil(t, err) + assert.Equal(t, FillPriceAndModes{longPrice, Maker, Taker}, *output) + }) + }) + + t.Run("oracle < long price < upper bound", func(t *testing.T) { + longPrice := hu.Mul1e6(big.NewInt(25)) + t.Run("short price < long price", func(t *testing.T) { + m0 := &Metadata{ + Price: longPrice, + AmmIndex: big.NewInt(market), + BlockPlaced: blockPlaced0, + } + m1 := &Metadata{ + Price: new(big.Int).Sub(longPrice, big.NewInt(1)), + BlockPlaced: blockPlaced1, + } + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(market).Return(upperbound, lowerbound).Times(1) + output, err, _ := determineFillPrice(mockBibliophile, m0, m1) + assert.Nil(t, err) + assert.Equal(t, FillPriceAndModes{longPrice, Maker, Taker}, *output) + }) + + t.Run("short price == long price", func(t *testing.T) { + m0 := &Metadata{ + Price: longPrice, + AmmIndex: big.NewInt(market), + BlockPlaced: blockPlaced0, + } + m1 := &Metadata{ + Price: longPrice, + BlockPlaced: blockPlaced1, + } + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(market).Return(upperbound, lowerbound).Times(1) + output, err, _ := determineFillPrice(mockBibliophile, m0, m1) + assert.Nil(t, err) + assert.Equal(t, FillPriceAndModes{longPrice, Maker, Taker}, *output) + }) + }) + + t.Run("long price == upper bound", func(t *testing.T) { + longPrice := upperbound + t.Run("short price < long price", func(t *testing.T) { + m0 := &Metadata{ + Price: longPrice, + AmmIndex: big.NewInt(market), + BlockPlaced: blockPlaced0, + } + m1 := &Metadata{ + Price: new(big.Int).Sub(longPrice, big.NewInt(1)), + BlockPlaced: blockPlaced1, + } + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(market).Return(upperbound, lowerbound).Times(1) + output, err, _ := determineFillPrice(mockBibliophile, m0, m1) + assert.Nil(t, err) + assert.Equal(t, FillPriceAndModes{longPrice, Maker, Taker}, *output) + }) + + t.Run("short price == long price", func(t *testing.T) { + m0 := &Metadata{ + Price: longPrice, + AmmIndex: big.NewInt(market), + BlockPlaced: blockPlaced0, + } + m1 := &Metadata{ + Price: longPrice, + BlockPlaced: blockPlaced1, + } + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(market).Return(upperbound, lowerbound).Times(1) + output, err, _ := determineFillPrice(mockBibliophile, m0, m1) + assert.Nil(t, err) + assert.Equal(t, FillPriceAndModes{longPrice, Maker, Taker}, *output) + }) + }) + + t.Run("upper bound < long price", func(t *testing.T) { + longPrice := new(big.Int).Add(upperbound, big.NewInt(42)) + t.Run("upper < short price < long price", func(t *testing.T) { + m0 := &Metadata{ + Price: longPrice, + AmmIndex: big.NewInt(market), + BlockPlaced: blockPlaced0, + } + m1 := &Metadata{ + Price: new(big.Int).Add(upperbound, big.NewInt(1)), + BlockPlaced: blockPlaced1, + } + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(market).Return(upperbound, lowerbound).Times(1) + output, err, _ := determineFillPrice(mockBibliophile, m0, m1) + assert.Nil(t, output) + assert.Equal(t, ErrTooHigh, err) + }) + + t.Run("upper == short price < long price", func(t *testing.T) { + m0 := &Metadata{ + Price: longPrice, + AmmIndex: big.NewInt(market), + BlockPlaced: blockPlaced0, + } + m1 := &Metadata{ + Price: upperbound, + BlockPlaced: blockPlaced1, + } + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(market).Return(upperbound, lowerbound).Times(1) + output, err, _ := determineFillPrice(mockBibliophile, m0, m1) + assert.Nil(t, err) + assert.Equal(t, FillPriceAndModes{upperbound, Maker, Taker}, *output) + }) + + t.Run("short price < upper", func(t *testing.T) { + m0 := &Metadata{ + Price: longPrice, + AmmIndex: big.NewInt(market), + BlockPlaced: blockPlaced0, + } + m1 := &Metadata{ + Price: new(big.Int).Sub(upperbound, big.NewInt(1)), + BlockPlaced: blockPlaced1, + } + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(market).Return(upperbound, lowerbound).Times(1) + output, err, _ := determineFillPrice(mockBibliophile, m0, m1) + assert.Nil(t, err) + assert.Equal(t, FillPriceAndModes{upperbound, Maker, Taker}, *output) + }) + + t.Run("short price < lower", func(t *testing.T) { + m0 := &Metadata{ + Price: longPrice, + AmmIndex: big.NewInt(market), + BlockPlaced: blockPlaced0, + } + m1 := &Metadata{ + Price: new(big.Int).Sub(lowerbound, big.NewInt(1)), + BlockPlaced: blockPlaced1, + } + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(market).Return(upperbound, lowerbound).Times(1) + output, err, _ := determineFillPrice(mockBibliophile, m0, m1) + assert.Nil(t, err) + assert.Equal(t, FillPriceAndModes{upperbound, Maker, Taker}, *output) + }) + }) + }) +} + +func TestDetermineLiquidationFillPrice(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockBibliophile := b.NewMockBibliophileClient(ctrl) + + liqUpperBound, liqLowerBound := hu.Mul1e6(big.NewInt(22)), hu.Mul1e6(big.NewInt(18)) + + upperbound := hu.Mul1e6(big.NewInt(30)) // $30 + lowerbound := hu.Mul1e6(big.NewInt(10)) // $10 + market := int64(7) + + t.Run("long position is being liquidated", func(t *testing.T) { + t.Run("order price < liqLowerBound", func(t *testing.T) { + m0 := &Metadata{ + Price: new(big.Int).Sub(liqLowerBound, big.NewInt(1)), + BaseAssetQuantity: big.NewInt(5), + AmmIndex: big.NewInt(market), + } + mockBibliophile.EXPECT().GetAcceptableBoundsForLiquidation(market).Return(liqUpperBound, liqLowerBound).Times(1) + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(market).Return(upperbound, lowerbound).Times(1) + output, err := determineLiquidationFillPrice(mockBibliophile, m0) + assert.Nil(t, output) + assert.Equal(t, ErrTooLow, err) + }) + t.Run("order price == liqLowerBound", func(t *testing.T) { + m0 := &Metadata{ + Price: liqLowerBound, + BaseAssetQuantity: big.NewInt(5), + AmmIndex: big.NewInt(market), + } + mockBibliophile.EXPECT().GetAcceptableBoundsForLiquidation(market).Return(liqUpperBound, liqLowerBound).Times(1) + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(market).Return(upperbound, lowerbound).Times(1) + output, err := determineLiquidationFillPrice(mockBibliophile, m0) + assert.Nil(t, err) + assert.Equal(t, liqLowerBound, output) + }) + + t.Run("liqLowerBound < order price < upper bound", func(t *testing.T) { + m0 := &Metadata{ + Price: new(big.Int).Add(liqLowerBound, big.NewInt(99)), + BaseAssetQuantity: big.NewInt(5), + AmmIndex: big.NewInt(market), + } + mockBibliophile.EXPECT().GetAcceptableBoundsForLiquidation(market).Return(liqUpperBound, liqLowerBound).Times(1) + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(market).Return(upperbound, lowerbound).Times(1) + output, err := determineLiquidationFillPrice(mockBibliophile, m0) + assert.Nil(t, err) + assert.Equal(t, m0.Price, output) + }) + + t.Run("order price == upper bound", func(t *testing.T) { + m0 := &Metadata{ + Price: upperbound, + BaseAssetQuantity: big.NewInt(5), + AmmIndex: big.NewInt(market), + } + mockBibliophile.EXPECT().GetAcceptableBoundsForLiquidation(market).Return(liqUpperBound, liqLowerBound).Times(1) + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(market).Return(upperbound, lowerbound).Times(1) + output, err := determineLiquidationFillPrice(mockBibliophile, m0) + assert.Nil(t, err) + assert.Equal(t, upperbound, output) + }) + + t.Run("order price > upper bound", func(t *testing.T) { + m0 := &Metadata{ + Price: new(big.Int).Add(upperbound, big.NewInt(99)), + BaseAssetQuantity: big.NewInt(5), + AmmIndex: big.NewInt(market), + } + mockBibliophile.EXPECT().GetAcceptableBoundsForLiquidation(market).Return(liqUpperBound, liqLowerBound).Times(1) + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(market).Return(upperbound, lowerbound).Times(1) + output, err := determineLiquidationFillPrice(mockBibliophile, m0) + assert.Nil(t, err) + assert.Equal(t, upperbound, output) + }) + }) +} + +type ValidateOrdersAndDetermineFillPriceTestCase struct { + Order0, Order1 ob.ContractOrder + FillAmount *big.Int + Err error + BadElement BadElement +} + +func testValidateOrdersAndDetermineFillPriceTestCase(t *testing.T, mockBibliophile *b.MockBibliophileClient, testCase ValidateOrdersAndDetermineFillPriceTestCase) ValidateOrdersAndDetermineFillPriceOutput { + order0Bytes, err := testCase.Order0.EncodeToABI() + if err != nil { + t.Fatal(err) + } + order1Bytes, err := testCase.Order1.EncodeToABI() + if err != nil { + t.Fatal(err) + } + resp := ValidateOrdersAndDetermineFillPrice(mockBibliophile, &ValidateOrdersAndDetermineFillPriceInput{ + Data: [2][]byte{order0Bytes, order1Bytes}, + FillAmount: testCase.FillAmount, + }) + + // verify results + if testCase.Err == nil && resp.Err != "" { + t.Fatalf("expected no error, got %v", resp.Err) + } + if testCase.Err != nil { + if resp.Err != testCase.Err.Error() { + t.Fatalf("expected %v, got %v", testCase.Err, testCase.Err) + } + + if resp.Element != uint8(testCase.BadElement) { + t.Fatalf("expected %v, got %v", testCase.BadElement, resp.Element) + } + } + return resp +} + +func TestValidateOrdersAndDetermineFillPrice(t *testing.T) { + // create a mock BibliophileClient + trader := common.HexToAddress("0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC") + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + t.Run("invalid fillAmount", func(t *testing.T) { + order0 := &hu.LimitOrder{ + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(10), + Price: big.NewInt(100), + Salt: big.NewInt(1), + ReduceOnly: false, + }, + PostOnly: false, + } + order1 := &hu.LimitOrder{ + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(-10), + Price: big.NewInt(100), + Salt: big.NewInt(2), + ReduceOnly: false, + }, + PostOnly: false, + } + fillAmount := big.NewInt(0) + + mockBibliophile := b.NewMockBibliophileClient(ctrl) + + testCase := ValidateOrdersAndDetermineFillPriceTestCase{ + Order0: order0, + Order1: order1, + FillAmount: fillAmount, + Err: ErrInvalidFillAmount, + BadElement: Generic, + } + + testValidateOrdersAndDetermineFillPriceTestCase(t, mockBibliophile, testCase) + }) + + t.Run("different amm", func(t *testing.T) { + order0 := &hu.LimitOrder{ + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(10), + Price: big.NewInt(100), + Salt: big.NewInt(1), + ReduceOnly: false, + }, + PostOnly: false, + } + order0Hash, _ := order0.Hash() + order1 := &hu.LimitOrder{ + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(1), + Trader: trader, + BaseAssetQuantity: big.NewInt(-10), + Price: big.NewInt(100), + Salt: big.NewInt(2), + ReduceOnly: false, + }, + PostOnly: false, + } + order1Hash, _ := order1.Hash() + fillAmount := big.NewInt(2) + + mockBibliophile := b.NewMockBibliophileClient(ctrl) + mockBibliophile.EXPECT().GetOrderFilledAmount(order0Hash).Return(big.NewInt(0)) + mockBibliophile.EXPECT().GetOrderStatus(order0Hash).Return(int64(1)) // placed + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(order0.AmmIndex.Int64()).Return(common.Address{101}) + mockBibliophile.EXPECT().GetBlockPlaced(order0Hash).Return(big.NewInt(10)) + + mockBibliophile.EXPECT().GetOrderFilledAmount(order1Hash).Return(big.NewInt(0)) + mockBibliophile.EXPECT().GetOrderStatus(order1Hash).Return(int64(1)) // placed + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(order1.AmmIndex.Int64()).Return(common.Address{102}) + mockBibliophile.EXPECT().GetBlockPlaced(order1Hash).Return(big.NewInt(12)) + testCase := ValidateOrdersAndDetermineFillPriceTestCase{ + Order0: order0, + Order1: order1, + FillAmount: fillAmount, + Err: ErrNotSameAMM, + BadElement: Generic, + } + + testValidateOrdersAndDetermineFillPriceTestCase(t, mockBibliophile, testCase) + }) + + t.Run("price mismatch", func(t *testing.T) { + order0 := &hu.LimitOrder{ + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(10), + Price: big.NewInt(99), + Salt: big.NewInt(1), + ReduceOnly: false, + }, + PostOnly: false, + } + order0Hash, _ := order0.Hash() + order1 := &hu.LimitOrder{ + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(-10), + Price: big.NewInt(100), + Salt: big.NewInt(2), + ReduceOnly: false, + }, + PostOnly: false, + } + order1Hash, _ := order1.Hash() + fillAmount := big.NewInt(2) + + mockBibliophile := b.NewMockBibliophileClient(ctrl) + mockBibliophile.EXPECT().GetOrderFilledAmount(order0Hash).Return(big.NewInt(0)) + mockBibliophile.EXPECT().GetOrderStatus(order0Hash).Return(int64(1)) // placed + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(order0.AmmIndex.Int64()).Return(common.Address{101}) + mockBibliophile.EXPECT().GetBlockPlaced(order0Hash).Return(big.NewInt(10)) + + mockBibliophile.EXPECT().GetOrderFilledAmount(order1Hash).Return(big.NewInt(0)) + mockBibliophile.EXPECT().GetOrderStatus(order1Hash).Return(int64(1)) // placed + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(order1.AmmIndex.Int64()).Return(common.Address{101}) + mockBibliophile.EXPECT().GetBlockPlaced(order1Hash).Return(big.NewInt(12)) + testCase := ValidateOrdersAndDetermineFillPriceTestCase{ + Order0: order0, + Order1: order1, + FillAmount: fillAmount, + Err: ErrNoMatch, + BadElement: Generic, + } + + testValidateOrdersAndDetermineFillPriceTestCase(t, mockBibliophile, testCase) + }) + + t.Run("fillAmount not multiple", func(t *testing.T) { + order0 := &hu.LimitOrder{ + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(10), + Price: big.NewInt(100), + Salt: big.NewInt(1), + ReduceOnly: false, + }, + PostOnly: false, + } + order0Hash, _ := order0.Hash() + order1 := &hu.LimitOrder{ + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(-10), + Price: big.NewInt(100), + Salt: big.NewInt(2), + ReduceOnly: false, + }, + PostOnly: false, + } + order1Hash, _ := order1.Hash() + fillAmount := big.NewInt(2) + + mockBibliophile := b.NewMockBibliophileClient(ctrl) + mockBibliophile.EXPECT().GetOrderFilledAmount(order0Hash).Return(big.NewInt(0)) + mockBibliophile.EXPECT().GetOrderStatus(order0Hash).Return(int64(1)) // placed + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(order0.AmmIndex.Int64()).Return(common.Address{101}) + mockBibliophile.EXPECT().GetBlockPlaced(order0Hash).Return(big.NewInt(10)) + + mockBibliophile.EXPECT().GetOrderFilledAmount(order1Hash).Return(big.NewInt(0)) + mockBibliophile.EXPECT().GetOrderStatus(order1Hash).Return(int64(1)) // placed + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(order1.AmmIndex.Int64()).Return(common.Address{101}) + mockBibliophile.EXPECT().GetBlockPlaced(order1Hash).Return(big.NewInt(12)) + + mockBibliophile.EXPECT().GetMinSizeRequirement(order1.AmmIndex.Int64()).Return(big.NewInt(5)) + + testCase := ValidateOrdersAndDetermineFillPriceTestCase{ + Order0: order0, + Order1: order1, + FillAmount: fillAmount, + Err: ErrNotMultiple, + BadElement: Generic, + } + + testValidateOrdersAndDetermineFillPriceTestCase(t, mockBibliophile, testCase) + }) + + t.Run("success", func(t *testing.T) { + order0 := &hu.LimitOrder{ + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(10), + Price: big.NewInt(100), + Salt: big.NewInt(1), + ReduceOnly: false, + }, + PostOnly: false, + } + order0Hash, _ := order0.Hash() + order1 := &hu.LimitOrder{ + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(-10), + Price: big.NewInt(100), + Salt: big.NewInt(2), + ReduceOnly: false, + }, + PostOnly: false, + } + order1Hash, _ := order1.Hash() + fillAmount := big.NewInt(2) + + mockBibliophile := b.NewMockBibliophileClient(ctrl) + mockBibliophile.EXPECT().GetOrderFilledAmount(order0Hash).Return(big.NewInt(0)) + mockBibliophile.EXPECT().GetOrderStatus(order0Hash).Return(int64(1)) // placed + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(order0.AmmIndex.Int64()).Return(common.Address{101}) + mockBibliophile.EXPECT().GetBlockPlaced(order0Hash).Return(big.NewInt(10)) + + mockBibliophile.EXPECT().GetOrderFilledAmount(order1Hash).Return(big.NewInt(0)) + mockBibliophile.EXPECT().GetOrderStatus(order1Hash).Return(int64(1)) // placed + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(order1.AmmIndex.Int64()).Return(common.Address{101}) + mockBibliophile.EXPECT().GetBlockPlaced(order1Hash).Return(big.NewInt(12)) + + mockBibliophile.EXPECT().GetMinSizeRequirement(order1.AmmIndex.Int64()).Return(big.NewInt(1)) + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(order1.AmmIndex.Int64()).Return(big.NewInt(110), big.NewInt(90)) + + testCase := ValidateOrdersAndDetermineFillPriceTestCase{ + Order0: order0, + Order1: order1, + FillAmount: fillAmount, + Err: nil, + BadElement: NoError, + } + + response := testValidateOrdersAndDetermineFillPriceTestCase(t, mockBibliophile, testCase) + assert.Equal(t, big.NewInt(100), response.Res.FillPrice) + assert.Equal(t, uint8(0), response.Res.OrderTypes[0]) + assert.Equal(t, uint8(0), response.Res.OrderTypes[1]) + + assert.Equal(t, uint8(NoError), response.Element) + assert.Equal(t, IClearingHouseInstruction{ + AmmIndex: big.NewInt(0), + Trader: trader, + OrderHash: order0Hash, + Mode: uint8(Maker), + }, response.Res.Instructions[0]) + assert.Equal(t, IClearingHouseInstruction{ + AmmIndex: big.NewInt(0), + Trader: trader, + OrderHash: order1Hash, + Mode: uint8(Taker), + }, response.Res.Instructions[1]) + }) +} + +type ValidateLiquidationOrderAndDetermineFillPriceTestCase struct { + Order ob.ContractOrder + LiquidationAmount *big.Int + Err error + BadElement BadElement +} + +func testValidateLiquidationOrderAndDetermineFillPriceTestCase(t *testing.T, mockBibliophile *b.MockBibliophileClient, testCase ValidateLiquidationOrderAndDetermineFillPriceTestCase) ValidateLiquidationOrderAndDetermineFillPriceOutput { + orderBytes, err := testCase.Order.EncodeToABI() + if err != nil { + t.Fatal(err) + } + + resp := ValidateLiquidationOrderAndDetermineFillPrice(mockBibliophile, &ValidateLiquidationOrderAndDetermineFillPriceInput{ + Data: orderBytes, + LiquidationAmount: testCase.LiquidationAmount, + }) + + // verify results + if testCase.Err == nil && resp.Err != "" { + t.Fatalf("expected no error, got %v", resp.Err) + } + if testCase.Err != nil { + if resp.Err != testCase.Err.Error() { + t.Fatalf("expected %v, got %v", testCase.Err, testCase.Err) + } + + if resp.Element != uint8(testCase.BadElement) { + t.Fatalf("expected %v, got %v", testCase.BadElement, resp.Element) + } + } + return resp +} + +func TestValidateLiquidationOrderAndDetermineFillPrice(t *testing.T) { + trader := common.HexToAddress("0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC") + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + t.Run("invalid liquidationAmount", func(t *testing.T) { + order := &hu.LimitOrder{ + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(10), + Price: big.NewInt(100), + Salt: big.NewInt(1), + ReduceOnly: true, + }, + PostOnly: false, + } + + mockBibliophile := b.NewMockBibliophileClient(ctrl) + + testCase := ValidateLiquidationOrderAndDetermineFillPriceTestCase{ + Order: order, + LiquidationAmount: big.NewInt(0), + Err: ErrInvalidFillAmount, + BadElement: Generic, + } + + testValidateLiquidationOrderAndDetermineFillPriceTestCase(t, mockBibliophile, testCase) + }) + + t.Run("fillAmount not multiple", func(t *testing.T) { + order := &hu.LimitOrder{ + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(-10), + Price: big.NewInt(100), + Salt: big.NewInt(2), + ReduceOnly: true, + }, + PostOnly: false, + } + orderHash, _ := order.Hash() + + mockBibliophile := b.NewMockBibliophileClient(ctrl) + mockBibliophile.EXPECT().GetOrderFilledAmount(orderHash).Return(big.NewInt(0)) + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(1)) // placed + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(order.AmmIndex.Int64()).Return(common.Address{101}) + mockBibliophile.EXPECT().GetBlockPlaced(orderHash).Return(big.NewInt(10)) + mockBibliophile.EXPECT().GetSize(common.Address{101}, &trader).Return(big.NewInt(10)) + + mockBibliophile.EXPECT().GetMinSizeRequirement(order.AmmIndex.Int64()).Return(big.NewInt(5)) + + testCase := ValidateLiquidationOrderAndDetermineFillPriceTestCase{ + Order: order, + LiquidationAmount: big.NewInt(2), + Err: ErrNotMultiple, + BadElement: Generic, + } + + testValidateLiquidationOrderAndDetermineFillPriceTestCase(t, mockBibliophile, testCase) + }) + + t.Run("success", func(t *testing.T) { + order := &hu.LimitOrder{ + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(-10), + Price: big.NewInt(100), + Salt: big.NewInt(2), + ReduceOnly: true, + }, + PostOnly: false, + } + orderHash, _ := order.Hash() + + mockBibliophile := b.NewMockBibliophileClient(ctrl) + mockBibliophile.EXPECT().GetOrderFilledAmount(orderHash).Return(big.NewInt(0)) + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(1)) // placed + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(order.AmmIndex.Int64()).Return(common.Address{101}) + mockBibliophile.EXPECT().GetBlockPlaced(orderHash).Return(big.NewInt(10)) + mockBibliophile.EXPECT().GetSize(common.Address{101}, &trader).Return(big.NewInt(10)) + mockBibliophile.EXPECT().GetMinSizeRequirement(order.AmmIndex.Int64()).Return(big.NewInt(1)) + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(order.AmmIndex.Int64()).Return(big.NewInt(110), big.NewInt(90)) + mockBibliophile.EXPECT().GetAcceptableBoundsForLiquidation(order.AmmIndex.Int64()).Return(big.NewInt(110), big.NewInt(90)) + + testCase := ValidateLiquidationOrderAndDetermineFillPriceTestCase{ + Order: order, + LiquidationAmount: big.NewInt(2), + Err: nil, + BadElement: NoError, + } + + response := testValidateLiquidationOrderAndDetermineFillPriceTestCase(t, mockBibliophile, testCase) + + assert.Equal(t, uint8(NoError), response.Element) + assert.Equal(t, IClearingHouseInstruction{ + AmmIndex: big.NewInt(0), + Trader: trader, + OrderHash: orderHash, + Mode: uint8(Maker), + }, response.Res.Instruction) + assert.Equal(t, big.NewInt(100), response.Res.FillPrice) + assert.Equal(t, uint8(0), response.Res.OrderType) + assert.Equal(t, big.NewInt(-2), response.Res.FillAmount) + }) +} + +func TestReducesPosition(t *testing.T) { + testCases := []struct { + positionSize *big.Int + baseAssetQuantity *big.Int + expectedResult bool + }{ + { + positionSize: big.NewInt(100), + baseAssetQuantity: big.NewInt(-50), + expectedResult: true, + }, + { + positionSize: big.NewInt(-100), + baseAssetQuantity: big.NewInt(50), + expectedResult: true, + }, + { + positionSize: big.NewInt(100), + baseAssetQuantity: big.NewInt(50), + expectedResult: false, + }, + { + positionSize: big.NewInt(-100), + baseAssetQuantity: big.NewInt(-50), + expectedResult: false, + }, + } + + for _, tc := range testCases { + result := reducesPosition(tc.positionSize, tc.baseAssetQuantity) + if result != tc.expectedResult { + t.Errorf("reducesPosition(%v, %v) = %v; expected %v", tc.positionSize, tc.baseAssetQuantity, result, tc.expectedResult) + } + } +} + +func TestGetRequiredMargin(t *testing.T) { + trader := common.HexToAddress("0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC") + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockBibliophile := b.NewMockBibliophileClient(ctrl) + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(gomock.Any()).Return(big.NewInt(100), big.NewInt(10)).AnyTimes() + mockBibliophile.EXPECT().GetMinAllowableMargin().Return(big.NewInt(1000)).AnyTimes() + mockBibliophile.EXPECT().GetTakerFee().Return(big.NewInt(5)).AnyTimes() + + // create a mock order + order := ILimitOrderBookOrder{ + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: hu.Mul(big.NewInt(10), hu.ONE_E_18), + Price: hu.Mul(big.NewInt(50), hu.ONE_E_6), + ReduceOnly: false, + Salt: big.NewInt(1), + PostOnly: false, + } + + // call the function + requiredMargin := getRequiredMargin(mockBibliophile, order) + + fmt.Println("#####", requiredMargin) + + // assert that the result is correct + expectedMargin := big.NewInt(502500) // (10 * 50 * 1e6) * (1 + 0.005) + assert.Equal(t, expectedMargin, requiredMargin) +} diff --git a/precompile/contracts/juror/module.go b/precompile/contracts/juror/module.go new file mode 100644 index 0000000000..d65d2bbf7c --- /dev/null +++ b/precompile/contracts/juror/module.go @@ -0,0 +1,63 @@ +// Code generated +// This file is a generated precompile contract config with stubbed abstract functions. +// The file is generated by a template. Please inspect every code and comment in this file before use. + +package juror + +import ( + "fmt" + + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ava-labs/subnet-evm/precompile/modules" + "github.com/ava-labs/subnet-evm/precompile/precompileconfig" + + "github.com/ethereum/go-ethereum/common" +) + +var _ contract.Configurator = &configurator{} + +// ConfigKey is the key used in json config files to specify this precompile precompileconfig. +// must be unique across all precompiles. +const ConfigKey = "jurorConfig" + +// ContractAddress is the defined address of the precompile contract. +// This should be unique across all precompile contracts. +// See precompile/registry/registry.go for registered precompile contracts and more information. +var ContractAddress = common.HexToAddress("0x03000000000000000000000000000000000000a0") // SET A SUITABLE HEX ADDRESS HERE + +// Module is the precompile module. It is used to register the precompile contract. +var Module = modules.Module{ + ConfigKey: ConfigKey, + Address: ContractAddress, + Contract: JurorPrecompile, + Configurator: &configurator{}, +} + +type configurator struct{} + +func init() { + // Register the precompile module. + // Each precompile contract registers itself through [RegisterModule] function. + if err := modules.RegisterModule(Module); err != nil { + panic(err) + } +} + +// MakeConfig returns a new precompile config instance. +// This is required for Marshal/Unmarshal the precompile config. +func (*configurator) MakeConfig() precompileconfig.Config { + return new(Config) +} + +// Configure configures [state] with the given [cfg] precompileconfig. +// This function is called by the EVM once per precompile contract activation. +// You can use this function to set up your precompile contract's initial state, +// by using the [cfg] config and [state] stateDB. +func (*configurator) Configure(chainConfig precompileconfig.ChainConfig, cfg precompileconfig.Config, state contract.StateDB, _ contract.ConfigurationBlockContext) error { + config, ok := cfg.(*Config) + if !ok { + return fmt.Errorf("incorrect config %T: %v", config, config) + } + // CUSTOM CODE STARTS HERE + return nil +} diff --git a/precompile/contracts/juror/notional_position.go b/precompile/contracts/juror/notional_position.go new file mode 100644 index 0000000000..57d4ef9057 --- /dev/null +++ b/precompile/contracts/juror/notional_position.go @@ -0,0 +1,14 @@ +package juror + +import ( + hu "github.com/ava-labs/subnet-evm/plugin/evm/orderbook/hubbleutils" + b "github.com/ava-labs/subnet-evm/precompile/contracts/bibliophile" +) + +func GetNotionalPositionAndMargin(bibliophile b.BibliophileClient, input *GetNotionalPositionAndMarginInput) GetNotionalPositionAndMarginOutput { + notionalPosition, margin := bibliophile.GetNotionalPositionAndMargin(input.Trader, input.IncludeFundingPayments, input.Mode, hu.UpgradeVersionV0orV1(bibliophile.GetTimeStamp())) + return GetNotionalPositionAndMarginOutput{ + NotionalPosition: notionalPosition, + Margin: margin, + } +} diff --git a/precompile/contracts/jurorv2/README.md b/precompile/contracts/jurorv2/README.md new file mode 100644 index 0000000000..d81e622b2b --- /dev/null +++ b/precompile/contracts/jurorv2/README.md @@ -0,0 +1,23 @@ +There are some must-be-done changes waiting in the generated file. Each area requiring you to add your code is marked with CUSTOM CODE to make them easy to find and modify. +Additionally there are other files you need to edit to activate your precompile. +These areas are highlighted with comments "ADD YOUR PRECOMPILE HERE". +For testing take a look at other precompile tests in contract_test.go and config_test.go in other precompile folders. +See the tutorial in for more information about precompile development. + +General guidelines for precompile development: +1- Set a suitable config key in generated module.go. E.g: "yourPrecompileConfig" +2- Read the comment and set a suitable contract address in generated module.go. E.g: +ContractAddress = common.HexToAddress("ASUITABLEHEXADDRESS") +3- It is recommended to only modify code in the highlighted areas marked with "CUSTOM CODE STARTS HERE". Typically, custom codes are required in only those areas. +Modifying code outside of these areas should be done with caution and with a deep understanding of how these changes may impact the EVM. +4- Set gas costs in generated contract.go +5- Force import your precompile package in precompile/registry/registry.go +6- Add your config unit tests under generated package config_test.go +7- Add your contract unit tests under generated package contract_test.go +8- Additionally you can add a full-fledged VM test for your precompile under plugin/vm/vm_test.go. See existing precompile tests for examples. +9- Add your solidity interface and test contract to contracts/contracts +10- Write solidity contract tests for your precompile in contracts/contracts/test +11- Write TypeScript DS-Test counterparts for your solidity tests in contracts/test +12- Create your genesis with your precompile enabled in tests/precompile/genesis/ +13- Create e2e test for your solidity test in tests/precompile/solidity/suites.go +14- Run your e2e precompile Solidity tests with './scripts/run_ginkgo.sh` diff --git a/precompile/contracts/jurorv2/config.go b/precompile/contracts/jurorv2/config.go new file mode 100644 index 0000000000..165d2baa7a --- /dev/null +++ b/precompile/contracts/jurorv2/config.go @@ -0,0 +1,68 @@ +// Code generated +// This file is a generated precompile contract config with stubbed abstract functions. +// The file is generated by a template. Please inspect every code and comment in this file before use. + +package jurorv2 + +import ( + "math/big" + + "github.com/ava-labs/subnet-evm/precompile/precompileconfig" +) + +var _ precompileconfig.Config = &Config{} + +// Config implements the precompileconfig.Config interface and +// adds specific configuration for Juror. +type Config struct { + precompileconfig.Upgrade + // CUSTOM CODE STARTS HERE + // Add your own custom fields for Config here +} + +// NewConfig returns a config for a network upgrade at [blockTimestamp] that enables +// Juror. +func NewConfig(blockTimestamp *big.Int) *Config { + val := blockTimestamp.Uint64() + return &Config{ + Upgrade: precompileconfig.Upgrade{BlockTimestamp: &val}, + } +} + +// NewDisableConfig returns config for a network upgrade at [blockTimestamp] +// that disables Juror. +func NewDisableConfig(blockTimestamp *big.Int) *Config { + val := blockTimestamp.Uint64() + return &Config{ + Upgrade: precompileconfig.Upgrade{ + BlockTimestamp: &val, + Disable: true, + }, + } +} + +// Key returns the key for the Juror precompileconfig. +// This should be the same key as used in the precompile module. +func (*Config) Key() string { return ConfigKey } + +// Verify tries to verify Config and returns an error accordingly. +func (c *Config) Verify(precompileconfig.ChainConfig) error { + // CUSTOM CODE STARTS HERE + // Add your own custom verify code for Config here + // and return an error accordingly + return nil +} + +// Equal returns true if [s] is a [*Config] and it has been configured identical to [c]. +func (c *Config) Equal(s precompileconfig.Config) bool { + // typecast before comparison + other, ok := (s).(*Config) + if !ok { + return false + } + // CUSTOM CODE STARTS HERE + // modify this boolean accordingly with your custom Config, to check if [other] and the current [c] are equal + // if Config contains only Upgrade you can skip modifying it. + equals := c.Upgrade.Equal(&other.Upgrade) + return equals +} diff --git a/precompile/contracts/jurorv2/config_test.go b/precompile/contracts/jurorv2/config_test.go new file mode 100644 index 0000000000..d8e71c96cd --- /dev/null +++ b/precompile/contracts/jurorv2/config_test.go @@ -0,0 +1,62 @@ +// Code generated +// This file is a generated precompile config test with the skeleton of test functions. +// The file is generated by a template. Please inspect every code and comment in this file before use. + +package jurorv2 + +import ( + "math/big" + "testing" + + "github.com/ava-labs/subnet-evm/precompile/precompileconfig" + "github.com/ava-labs/subnet-evm/precompile/testutils" + "go.uber.org/mock/gomock" +) + +// TestVerify tests the verification of Config. +func TestVerify(t *testing.T) { + tests := map[string]testutils.ConfigVerifyTest{ + "valid config": { + Config: NewConfig(big.NewInt(3)), + ExpectedError: "", + }, + // CUSTOM CODE STARTS HERE + // Add your own Verify tests here, e.g.: + // "your custom test name": { + // Config: NewConfig(big.NewInt(3),), + // ExpectedError: ErrYourCustomError.Error(), + // }, + } + // Run verify tests. + testutils.RunVerifyTests(t, tests) +} + +// TestEqual tests the equality of Config with other precompile configs. +func TestEqual(t *testing.T) { + tests := map[string]testutils.ConfigEqualTest{ + "non-nil config and nil other": { + Config: NewConfig(big.NewInt(3)), + Other: nil, + Expected: false, + }, + "different type": { + Config: NewConfig(big.NewInt(3)), + Other: precompileconfig.NewMockConfig(gomock.NewController(t)), + Expected: false, + }, + "different timestamp": { + Config: NewConfig(big.NewInt(3)), + Other: NewConfig(big.NewInt(4)), + Expected: false, + }, + "same config": { + Config: NewConfig(big.NewInt(3)), + Other: NewConfig(big.NewInt(3)), + Expected: true, + }, + // CUSTOM CODE STARTS HERE + // Add your own Equal tests here + } + // Run equal tests. + testutils.RunEqualTests(t, tests) +} diff --git a/precompile/contracts/jurorv2/contract.abi b/precompile/contracts/jurorv2/contract.abi new file mode 100644 index 0000000000..4a9f817580 --- /dev/null +++ b/precompile/contracts/jurorv2/contract.abi @@ -0,0 +1 @@ +[{"inputs":[{"internalType":"address","name":"trader","type":"address"},{"internalType":"bool","name":"includeFundingPayments","type":"bool"},{"internalType":"uint8","name":"mode","type":"uint8"}],"name":"getNotionalPositionAndMargin","outputs":[{"internalType":"uint256","name":"notionalPosition","type":"uint256"},{"internalType":"int256","name":"margin","type":"int256"}],"stateMutability":"view","type":"function"},{"inputs":[{"components":[{"internalType":"uint256","name":"ammIndex","type":"uint256"},{"internalType":"address","name":"trader","type":"address"},{"internalType":"int256","name":"baseAssetQuantity","type":"int256"},{"internalType":"uint256","name":"price","type":"uint256"},{"internalType":"uint256","name":"salt","type":"uint256"},{"internalType":"bool","name":"reduceOnly","type":"bool"},{"internalType":"bool","name":"postOnly","type":"bool"}],"internalType":"struct ILimitOrderBook.Order","name":"order","type":"tuple"},{"internalType":"address","name":"sender","type":"address"},{"internalType":"bool","name":"assertLowMargin","type":"bool"}],"name":"validateCancelLimitOrder","outputs":[{"internalType":"string","name":"err","type":"string"},{"internalType":"bytes32","name":"orderHash","type":"bytes32"},{"components":[{"internalType":"int256","name":"unfilledAmount","type":"int256"},{"internalType":"address","name":"amm","type":"address"}],"internalType":"struct IOrderHandler.CancelOrderRes","name":"res","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"data","type":"bytes"},{"internalType":"uint256","name":"liquidationAmount","type":"uint256"}],"name":"validateLiquidationOrderAndDetermineFillPrice","outputs":[{"internalType":"string","name":"err","type":"string"},{"internalType":"enum IJuror.BadElement","name":"element","type":"uint8"},{"components":[{"components":[{"internalType":"uint256","name":"ammIndex","type":"uint256"},{"internalType":"address","name":"trader","type":"address"},{"internalType":"bytes32","name":"orderHash","type":"bytes32"},{"internalType":"enum IClearingHouse.OrderExecutionMode","name":"mode","type":"uint8"}],"internalType":"struct IClearingHouse.Instruction","name":"instruction","type":"tuple"},{"internalType":"uint8","name":"orderType","type":"uint8"},{"internalType":"bytes","name":"encodedOrder","type":"bytes"},{"internalType":"uint256","name":"fillPrice","type":"uint256"},{"internalType":"int256","name":"fillAmount","type":"int256"}],"internalType":"struct IOrderHandler.LiquidationMatchingValidationRes","name":"res","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes[2]","name":"data","type":"bytes[2]"},{"internalType":"int256","name":"fillAmount","type":"int256"}],"name":"validateOrdersAndDetermineFillPrice","outputs":[{"internalType":"string","name":"err","type":"string"},{"internalType":"enum IJuror.BadElement","name":"element","type":"uint8"},{"components":[{"components":[{"internalType":"uint256","name":"ammIndex","type":"uint256"},{"internalType":"address","name":"trader","type":"address"},{"internalType":"bytes32","name":"orderHash","type":"bytes32"},{"internalType":"enum IClearingHouse.OrderExecutionMode","name":"mode","type":"uint8"}],"internalType":"struct IClearingHouse.Instruction[2]","name":"instructions","type":"tuple[2]"},{"internalType":"uint8[2]","name":"orderTypes","type":"uint8[2]"},{"internalType":"bytes[2]","name":"encodedOrders","type":"bytes[2]"},{"internalType":"uint256","name":"fillPrice","type":"uint256"}],"internalType":"struct IOrderHandler.MatchingValidationRes","name":"res","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"components":[{"internalType":"uint8","name":"orderType","type":"uint8"},{"internalType":"uint256","name":"expireAt","type":"uint256"},{"internalType":"uint256","name":"ammIndex","type":"uint256"},{"internalType":"address","name":"trader","type":"address"},{"internalType":"int256","name":"baseAssetQuantity","type":"int256"},{"internalType":"uint256","name":"price","type":"uint256"},{"internalType":"uint256","name":"salt","type":"uint256"},{"internalType":"bool","name":"reduceOnly","type":"bool"}],"internalType":"struct IImmediateOrCancelOrders.Order","name":"order","type":"tuple"},{"internalType":"address","name":"sender","type":"address"}],"name":"validatePlaceIOCOrder","outputs":[{"internalType":"string","name":"err","type":"string"},{"internalType":"bytes32","name":"orderHash","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"components":[{"internalType":"uint256","name":"ammIndex","type":"uint256"},{"internalType":"address","name":"trader","type":"address"},{"internalType":"int256","name":"baseAssetQuantity","type":"int256"},{"internalType":"uint256","name":"price","type":"uint256"},{"internalType":"uint256","name":"salt","type":"uint256"},{"internalType":"bool","name":"reduceOnly","type":"bool"},{"internalType":"bool","name":"postOnly","type":"bool"}],"internalType":"struct ILimitOrderBook.Order","name":"order","type":"tuple"},{"internalType":"address","name":"sender","type":"address"}],"name":"validatePlaceLimitOrder","outputs":[{"internalType":"string","name":"err","type":"string"},{"internalType":"bytes32","name":"orderhash","type":"bytes32"},{"components":[{"internalType":"uint256","name":"reserveAmount","type":"uint256"},{"internalType":"address","name":"amm","type":"address"}],"internalType":"struct IOrderHandler.PlaceOrderRes","name":"res","type":"tuple"}],"stateMutability":"view","type":"function"}] diff --git a/precompile/contracts/jurorv2/contract.go b/precompile/contracts/jurorv2/contract.go new file mode 100644 index 0000000000..45509cd998 --- /dev/null +++ b/precompile/contracts/jurorv2/contract.go @@ -0,0 +1,287 @@ +// Code generated +// This file is a generated precompile contract config with stubbed abstract functions. +// The file is generated by a template. Please inspect every code and comment in this file before use. + +package jurorv2 + +import ( + "errors" + "fmt" + "math/big" + + "github.com/ava-labs/subnet-evm/accounts/abi" + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ava-labs/subnet-evm/precompile/contracts/bibliophile" + + _ "embed" + + "github.com/ethereum/go-ethereum/common" +) + +const ( + // Gas costs for each function. These are set to 1 by default. + // You should set a gas cost for each function in your contract. + // Generally, you should not set gas costs very low as this may cause your network to be vulnerable to DoS attacks. + // There are some predefined gas costs in contract/utils.go that you can use. + GetNotionalPositionAndMarginGasCost uint64 = 69 + ValidateLiquidationOrderAndDetermineFillPriceGasCost uint64 = 69 + ValidateOrdersAndDetermineFillPriceGasCost uint64 = 69 +) + +// CUSTOM CODE STARTS HERE +// Reference imports to suppress errors from unused imports. This code and any unnecessary imports can be removed. +var ( + _ = abi.JSON + _ = errors.New + _ = big.NewInt +) + +// Singleton StatefulPrecompiledContract and signatures. +var ( + + // JurorRawABI contains the raw ABI of Juror contract. + //go:embed contract.abi + JurorRawABI string + + JurorABI = contract.ParseABI(JurorRawABI) + + JurorPrecompile = createJurorPrecompile() +) + +// IClearingHouseInstruction is an auto generated low-level Go binding around an user-defined struct. +type IClearingHouseInstruction struct { + AmmIndex *big.Int + Trader common.Address + OrderHash [32]byte + Mode uint8 +} + +// ILimitOrderBookOrder is an auto generated low-level Go binding around an user-defined struct. +type ILimitOrderBookOrder struct { + AmmIndex *big.Int + Trader common.Address + BaseAssetQuantity *big.Int + Price *big.Int + Salt *big.Int + ReduceOnly bool + PostOnly bool +} + +// IOrderHandlerLiquidationMatchingValidationRes is an auto generated low-level Go binding around an user-defined struct. +type IOrderHandlerLiquidationMatchingValidationRes struct { + Instruction IClearingHouseInstruction + OrderType uint8 + EncodedOrder []byte + FillPrice *big.Int + FillAmount *big.Int +} + +// IOrderHandlerMatchingValidationRes is an auto generated low-level Go binding around an user-defined struct. +type IOrderHandlerMatchingValidationRes struct { + Instructions [2]IClearingHouseInstruction + OrderTypes [2]uint8 + EncodedOrders [2][]byte + FillPrice *big.Int +} + +type GetNotionalPositionAndMarginInput struct { + Trader common.Address + IncludeFundingPayments bool + Mode uint8 +} + +type GetNotionalPositionAndMarginOutput struct { + NotionalPosition *big.Int + Margin *big.Int +} + +type ValidateLiquidationOrderAndDetermineFillPriceInput struct { + Data []byte + LiquidationAmount *big.Int +} + +type ValidateLiquidationOrderAndDetermineFillPriceOutput struct { + Err string + Element uint8 + Res IOrderHandlerLiquidationMatchingValidationRes +} + +type ValidateOrdersAndDetermineFillPriceInput struct { + Data [2][]byte + FillAmount *big.Int +} + +type ValidateOrdersAndDetermineFillPriceOutput struct { + Err string + Element uint8 + Res IOrderHandlerMatchingValidationRes +} + +// UnpackGetNotionalPositionAndMarginInput attempts to unpack [input] as GetNotionalPositionAndMarginInput +// assumes that [input] does not include selector (omits first 4 func signature bytes) +func UnpackGetNotionalPositionAndMarginInput(input []byte) (GetNotionalPositionAndMarginInput, error) { + inputStruct := GetNotionalPositionAndMarginInput{} + err := JurorABI.UnpackInputIntoInterface(&inputStruct, "getNotionalPositionAndMargin", input, true) + + return inputStruct, err +} + +// PackGetNotionalPositionAndMargin packs [inputStruct] of type GetNotionalPositionAndMarginInput into the appropriate arguments for getNotionalPositionAndMargin. +func PackGetNotionalPositionAndMargin(inputStruct GetNotionalPositionAndMarginInput) ([]byte, error) { + return JurorABI.Pack("getNotionalPositionAndMargin", inputStruct.Trader, inputStruct.IncludeFundingPayments, inputStruct.Mode) +} + +// PackGetNotionalPositionAndMarginOutput attempts to pack given [outputStruct] of type GetNotionalPositionAndMarginOutput +// to conform the ABI outputs. +func PackGetNotionalPositionAndMarginOutput(outputStruct GetNotionalPositionAndMarginOutput) ([]byte, error) { + return JurorABI.PackOutput("getNotionalPositionAndMargin", + outputStruct.NotionalPosition, + outputStruct.Margin, + ) +} + +func getNotionalPositionAndMargin(accessibleState contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + if remainingGas, err = contract.DeductGas(suppliedGas, GetNotionalPositionAndMarginGasCost); err != nil { + return nil, 0, err + } + // attempts to unpack [input] into the arguments to the GetNotionalPositionAndMarginInput. + // Assumes that [input] does not include selector + // You can use unpacked [inputStruct] variable in your code + inputStruct, err := UnpackGetNotionalPositionAndMarginInput(input) + if err != nil { + return nil, remainingGas, err + } + + // CUSTOM CODE STARTS HERE + bibliophile := bibliophile.NewBibliophileClient(accessibleState) + output := GetNotionalPositionAndMargin(bibliophile, &inputStruct) + packedOutput, err := PackGetNotionalPositionAndMarginOutput(output) + if err != nil { + return nil, remainingGas, err + } + + // Return the packed output and the remaining gas + return packedOutput, remainingGas, nil +} + +// UnpackValidateLiquidationOrderAndDetermineFillPriceInput attempts to unpack [input] as ValidateLiquidationOrderAndDetermineFillPriceInput +// assumes that [input] does not include selector (omits first 4 func signature bytes) +func UnpackValidateLiquidationOrderAndDetermineFillPriceInput(input []byte) (ValidateLiquidationOrderAndDetermineFillPriceInput, error) { + inputStruct := ValidateLiquidationOrderAndDetermineFillPriceInput{} + err := JurorABI.UnpackInputIntoInterface(&inputStruct, "validateLiquidationOrderAndDetermineFillPrice", input, true) + + return inputStruct, err +} + +// PackValidateLiquidationOrderAndDetermineFillPrice packs [inputStruct] of type ValidateLiquidationOrderAndDetermineFillPriceInput into the appropriate arguments for validateLiquidationOrderAndDetermineFillPrice. +func PackValidateLiquidationOrderAndDetermineFillPrice(inputStruct ValidateLiquidationOrderAndDetermineFillPriceInput) ([]byte, error) { + return JurorABI.Pack("validateLiquidationOrderAndDetermineFillPrice", inputStruct.Data, inputStruct.LiquidationAmount) +} + +// PackValidateLiquidationOrderAndDetermineFillPriceOutput attempts to pack given [outputStruct] of type ValidateLiquidationOrderAndDetermineFillPriceOutput +// to conform the ABI outputs. +func PackValidateLiquidationOrderAndDetermineFillPriceOutput(outputStruct ValidateLiquidationOrderAndDetermineFillPriceOutput) ([]byte, error) { + return JurorABI.PackOutput("validateLiquidationOrderAndDetermineFillPrice", + outputStruct.Err, + outputStruct.Element, + outputStruct.Res, + ) +} + +func validateLiquidationOrderAndDetermineFillPrice(accessibleState contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + if remainingGas, err = contract.DeductGas(suppliedGas, ValidateLiquidationOrderAndDetermineFillPriceGasCost); err != nil { + return nil, 0, err + } + // attempts to unpack [input] into the arguments to the ValidateLiquidationOrderAndDetermineFillPriceInput. + // Assumes that [input] does not include selector + // You can use unpacked [inputStruct] variable in your code + inputStruct, err := UnpackValidateLiquidationOrderAndDetermineFillPriceInput(input) + if err != nil { + return nil, remainingGas, err + } + + // CUSTOM CODE STARTS HERE + bibliophile := bibliophile.NewBibliophileClient(accessibleState) + output := ValidateLiquidationOrderAndDetermineFillPrice(bibliophile, &inputStruct) + packedOutput, err := PackValidateLiquidationOrderAndDetermineFillPriceOutput(output) + if err != nil { + return nil, remainingGas, err + } + + // Return the packed output and the remaining gas + return packedOutput, remainingGas, nil +} + +// UnpackValidateOrdersAndDetermineFillPriceInput attempts to unpack [input] as ValidateOrdersAndDetermineFillPriceInput +// assumes that [input] does not include selector (omits first 4 func signature bytes) +func UnpackValidateOrdersAndDetermineFillPriceInput(input []byte) (ValidateOrdersAndDetermineFillPriceInput, error) { + inputStruct := ValidateOrdersAndDetermineFillPriceInput{} + err := JurorABI.UnpackInputIntoInterface(&inputStruct, "validateOrdersAndDetermineFillPrice", input, true) + + return inputStruct, err +} + +// PackValidateOrdersAndDetermineFillPrice packs [inputStruct] of type ValidateOrdersAndDetermineFillPriceInput into the appropriate arguments for validateOrdersAndDetermineFillPrice. +func PackValidateOrdersAndDetermineFillPrice(inputStruct ValidateOrdersAndDetermineFillPriceInput) ([]byte, error) { + return JurorABI.Pack("validateOrdersAndDetermineFillPrice", inputStruct.Data, inputStruct.FillAmount) +} + +// PackValidateOrdersAndDetermineFillPriceOutput attempts to pack given [outputStruct] of type ValidateOrdersAndDetermineFillPriceOutput +// to conform the ABI outputs. +func PackValidateOrdersAndDetermineFillPriceOutput(outputStruct ValidateOrdersAndDetermineFillPriceOutput) ([]byte, error) { + return JurorABI.PackOutput("validateOrdersAndDetermineFillPrice", + outputStruct.Err, + outputStruct.Element, + outputStruct.Res, + ) +} + +func validateOrdersAndDetermineFillPrice(accessibleState contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + if remainingGas, err = contract.DeductGas(suppliedGas, ValidateOrdersAndDetermineFillPriceGasCost); err != nil { + return nil, 0, err + } + // attempts to unpack [input] into the arguments to the ValidateOrdersAndDetermineFillPriceInput. + // Assumes that [input] does not include selector + // You can use unpacked [inputStruct] variable in your code + inputStruct, err := UnpackValidateOrdersAndDetermineFillPriceInput(input) + if err != nil { + return nil, remainingGas, err + } + + // CUSTOM CODE STARTS HERE + bibliophile := bibliophile.NewBibliophileClient(accessibleState) + output := ValidateOrdersAndDetermineFillPrice(bibliophile, &inputStruct) + packedOutput, err := PackValidateOrdersAndDetermineFillPriceOutput(output) + if err != nil { + return nil, remainingGas, err + } + + // Return the packed output and the remaining gas + return packedOutput, remainingGas, nil +} + +// createJurorPrecompile returns a StatefulPrecompiledContract with getters and setters for the precompile. + +func createJurorPrecompile() contract.StatefulPrecompiledContract { + var functions []*contract.StatefulPrecompileFunction + + abiFunctionMap := map[string]contract.RunStatefulPrecompileFunc{ + "getNotionalPositionAndMargin": getNotionalPositionAndMargin, + "validateLiquidationOrderAndDetermineFillPrice": validateLiquidationOrderAndDetermineFillPrice, + "validateOrdersAndDetermineFillPrice": validateOrdersAndDetermineFillPrice, + } + + for name, function := range abiFunctionMap { + method, ok := JurorABI.Methods[name] + if !ok { + panic(fmt.Errorf("given method (%s) does not exist in the ABI", name)) + } + functions = append(functions, contract.NewStatefulPrecompileFunction(method.ID, function)) + } + // Construct the contract with no fallback function. + statefulContract, err := contract.NewStatefulPrecompileContract(nil, functions) + if err != nil { + panic(err) + } + return statefulContract +} diff --git a/precompile/contracts/jurorv2/contract_test.go b/precompile/contracts/jurorv2/contract_test.go new file mode 100644 index 0000000000..51bc6d9c7c --- /dev/null +++ b/precompile/contracts/jurorv2/contract_test.go @@ -0,0 +1,91 @@ +// Code generated +// This file is a generated precompile contract test with the skeleton of test functions. +// The file is generated by a template. Please inspect every code and comment in this file before use. + +package jurorv2 + +import ( + "math/big" + "testing" + + "github.com/ava-labs/subnet-evm/core/state" + "github.com/ava-labs/subnet-evm/precompile/testutils" + "github.com/ava-labs/subnet-evm/vmerrs" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +// These tests are run against the precompile contract directly with +// the given input and expected output. They're just a guide to +// help you write your own tests. These tests are for general cases like +// allowlist, readOnly behaviour, and gas cost. You should write your own +// tests for specific cases. +var ( + tests = map[string]testutils.PrecompileTest{ + "insufficient gas for getNotionalPositionAndMargin should fail": { + Caller: common.Address{1}, + InputFn: func(t testing.TB) []byte { + // CUSTOM CODE STARTS HERE + // populate test input here + testInput := GetNotionalPositionAndMarginInput{ + Trader: common.Address{1}, + } + input, err := PackGetNotionalPositionAndMargin(testInput) + require.NoError(t, err) + return input + }, + SuppliedGas: GetNotionalPositionAndMarginGasCost - 1, + ReadOnly: false, + ExpectedErr: vmerrs.ErrOutOfGas.Error(), + }, + "insufficient gas for validateLiquidationOrderAndDetermineFillPrice should fail": { + Caller: common.Address{1}, + InputFn: func(t testing.TB) []byte { + // CUSTOM CODE STARTS HERE + // populate test input here + testInput := ValidateLiquidationOrderAndDetermineFillPriceInput{ + LiquidationAmount: big.NewInt(0), + } + input, err := PackValidateLiquidationOrderAndDetermineFillPrice(testInput) + require.NoError(t, err) + return input + }, + SuppliedGas: ValidateLiquidationOrderAndDetermineFillPriceGasCost - 1, + ReadOnly: false, + ExpectedErr: vmerrs.ErrOutOfGas.Error(), + }, + "insufficient gas for validateOrdersAndDetermineFillPrice should fail": { + Caller: common.Address{1}, + InputFn: func(t testing.TB) []byte { + // CUSTOM CODE STARTS HERE + // populate test input here + testInput := ValidateOrdersAndDetermineFillPriceInput{FillAmount: big.NewInt(0)} + input, err := PackValidateOrdersAndDetermineFillPrice(testInput) + require.NoError(t, err) + return input + }, + SuppliedGas: ValidateOrdersAndDetermineFillPriceGasCost - 1, + ReadOnly: false, + ExpectedErr: vmerrs.ErrOutOfGas.Error(), + }, + } +) + +// TestJurorRun tests the Run function of the precompile contract. +func TestJurorRun(t *testing.T) { + // Run tests. + for name, test := range tests { + t.Run(name, func(t *testing.T) { + test.Run(t, Module, state.NewTestStateDB(t)) + }) + } +} + +func BenchmarkJuror(b *testing.B) { + // Benchmark tests. + for name, test := range tests { + b.Run(name, func(b *testing.B) { + test.Bench(b, Module, state.NewTestStateDB(b)) + }) + } +} diff --git a/precompile/contracts/jurorv2/matching_validation.go b/precompile/contracts/jurorv2/matching_validation.go new file mode 100644 index 0000000000..63154ed8c0 --- /dev/null +++ b/precompile/contracts/jurorv2/matching_validation.go @@ -0,0 +1,599 @@ +package jurorv2 + +import ( + "errors" + "math/big" + "strings" + + ob "github.com/ava-labs/subnet-evm/plugin/evm/orderbook" + hu "github.com/ava-labs/subnet-evm/plugin/evm/orderbook/hubbleutils" + b "github.com/ava-labs/subnet-evm/precompile/contracts/bibliophile" + "github.com/ava-labs/subnet-evm/utils" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" +) + +type Metadata struct { + AmmIndex *big.Int + Trader common.Address + BaseAssetQuantity *big.Int + Price *big.Int + BlockPlaced *big.Int + OrderHash common.Hash + OrderType ob.OrderType + PostOnly bool +} + +type Side uint8 + +const ( + Long Side = iota + Short + Liquidation +) + +type OrderStatus uint8 + +// has to be exact same as IOrderHandler +const ( + Invalid OrderStatus = iota + Placed + Filled + Cancelled +) + +var ( + ErrTwoOrders = errors.New("need 2 orders") + ErrInvalidFillAmount = errors.New("invalid fillAmount") + ErrNotLongOrder = errors.New("not long") + ErrNotShortOrder = errors.New("not short") + ErrNotSameAMM = errors.New("OB_orders_for_different_amms") + ErrNoMatch = errors.New("OB_orders_do_not_match") + ErrBothPostOnly = errors.New("both orders are post only") + ErrNotMultiple = errors.New("not multiple") + + ErrInvalidOrder = errors.New("invalid order") + ErrNotIOCOrder = errors.New("not_ioc_order") + ErrInvalidPrice = errors.New("invalid price") + ErrPricePrecision = errors.New("invalid price precision") + ErrInvalidMarket = errors.New("invalid market") + ErrCancelledOrder = errors.New("cancelled order") + ErrFilledOrder = errors.New("filled order") + ErrOrderAlreadyExists = errors.New("order already exists") + ErrTooLow = errors.New("long price below lower bound") + ErrTooHigh = errors.New("short price above upper bound") + ErrOverFill = errors.New("overfill") + ErrReduceOnlyAmountExceeded = errors.New("not reducing pos") + ErrBaseAssetQuantityZero = errors.New("baseAssetQuantity is zero") + ErrReduceOnlyBaseAssetQuantityInvalid = errors.New("reduce only order must reduce position") + ErrNetReduceOnlyAmountExceeded = errors.New("net reduce only amount exceeded") + ErrStaleReduceOnlyOrders = errors.New("cancel stale reduce only orders") + ErrInsufficientMargin = errors.New("insufficient margin") + ErrCrossingMarket = errors.New("crossing market") + ErrIOCOrderExpired = errors.New("IOC order expired") + ErrOpenOrders = errors.New("open orders") + ErrOpenReduceOnlyOrders = errors.New("open reduce only orders") + ErrNoTradingAuthority = errors.New("no trading authority") + ErrNoReferrer = errors.New("no referrer") +) + +type BadElement uint8 + +// DO NOT change this ordering because it is critical for the orderbook to determine the problematic order +const ( + Order0 BadElement = iota + Order1 + Generic + NoError +) + +// Business Logic +func ValidateOrdersAndDetermineFillPrice(bibliophile b.BibliophileClient, inputStruct *ValidateOrdersAndDetermineFillPriceInput) ValidateOrdersAndDetermineFillPriceOutput { + if len(inputStruct.Data) != 2 { + return getValidateOrdersAndDetermineFillPriceErrorOutput(ErrTwoOrders, Generic, common.Hash{}) + } + + if inputStruct.FillAmount.Sign() <= 0 { + return getValidateOrdersAndDetermineFillPriceErrorOutput(ErrInvalidFillAmount, Generic, common.Hash{}) + } + + decodeStep0, err := hu.DecodeTypeAndEncodedOrder(inputStruct.Data[0]) + if err != nil { + return getValidateOrdersAndDetermineFillPriceErrorOutput(err, Order0, common.Hash{}) + } + m0, err := validateOrder(bibliophile, decodeStep0.OrderType, decodeStep0.EncodedOrder, Long, inputStruct.FillAmount) + if err != nil { + return getValidateOrdersAndDetermineFillPriceErrorOutput(err, Order0, m0.OrderHash) + } + + decodeStep1, err := hu.DecodeTypeAndEncodedOrder(inputStruct.Data[1]) + if err != nil { + return getValidateOrdersAndDetermineFillPriceErrorOutput(err, Order1, common.Hash{}) + } + m1, err := validateOrder(bibliophile, decodeStep1.OrderType, decodeStep1.EncodedOrder, Short, new(big.Int).Neg(inputStruct.FillAmount)) + if err != nil { + return getValidateOrdersAndDetermineFillPriceErrorOutput(err, Order1, m1.OrderHash) + } + + if m0.AmmIndex.Cmp(m1.AmmIndex) != 0 { + return getValidateOrdersAndDetermineFillPriceErrorOutput(ErrNotSameAMM, Generic, common.Hash{}) + } + + if m0.Price.Cmp(m1.Price) < 0 { + return getValidateOrdersAndDetermineFillPriceErrorOutput(ErrNoMatch, Generic, common.Hash{}) + } + + // check 11 + if m0.PostOnly && m1.PostOnly { + return getValidateOrdersAndDetermineFillPriceErrorOutput(ErrBothPostOnly, Generic, common.Hash{}) + } + + minSize := bibliophile.GetMinSizeRequirement(m0.AmmIndex.Int64()) + if new(big.Int).Mod(inputStruct.FillAmount, minSize).Cmp(big.NewInt(0)) != 0 { + return getValidateOrdersAndDetermineFillPriceErrorOutput(ErrNotMultiple, Generic, common.Hash{}) + } + + fillPriceAndModes, err, element := determineFillPrice(bibliophile, m0, m1) + if err != nil { + orderHash := common.Hash{} + if element == Order0 { + orderHash = m0.OrderHash + } else if element == Order1 { + orderHash = m1.OrderHash + } + return getValidateOrdersAndDetermineFillPriceErrorOutput(err, element, orderHash) + } + + return ValidateOrdersAndDetermineFillPriceOutput{ + Err: "", + Element: uint8(NoError), + Res: IOrderHandlerMatchingValidationRes{ + Instructions: [2]IClearingHouseInstruction{ + IClearingHouseInstruction{ + AmmIndex: m0.AmmIndex, + Trader: m0.Trader, + OrderHash: m0.OrderHash, + Mode: uint8(fillPriceAndModes.Mode0), + }, + IClearingHouseInstruction{ + AmmIndex: m1.AmmIndex, + Trader: m1.Trader, + OrderHash: m1.OrderHash, + Mode: uint8(fillPriceAndModes.Mode1), + }, + }, + OrderTypes: [2]uint8{uint8(decodeStep0.OrderType), uint8(decodeStep1.OrderType)}, + EncodedOrders: [2][]byte{ + decodeStep0.EncodedOrder, + decodeStep1.EncodedOrder, + }, + FillPrice: fillPriceAndModes.FillPrice, + }, + } +} + +type executionMode uint8 + +// DO NOT change this ordering because it is critical for the clearing house to determine the correct fill mode +const ( + Taker executionMode = iota + Maker +) + +type FillPriceAndModes struct { + FillPrice *big.Int + Mode0 executionMode + Mode1 executionMode +} + +func determineFillPrice(bibliophile b.BibliophileClient, m0, m1 *Metadata) (*FillPriceAndModes, error, BadElement) { + output := FillPriceAndModes{} + upperBound, lowerBound := bibliophile.GetUpperAndLowerBoundForMarket(m0.AmmIndex.Int64()) + if m0.Price.Cmp(lowerBound) == -1 { + return nil, ErrTooLow, Order0 + } + if m1.Price.Cmp(upperBound) == 1 { + return nil, ErrTooHigh, Order1 + } + + blockDiff := m0.BlockPlaced.Cmp(m1.BlockPlaced) + if blockDiff == -1 { + // order0 came first, can't be IOC order + if m0.OrderType == ob.IOC { + return nil, ErrIOCOrderExpired, Order0 + } + // order1 came second, can't be post only order + if m1.OrderType == ob.Limit && m1.PostOnly { + return nil, ErrCrossingMarket, Order1 + } + output.Mode0 = Maker + output.Mode1 = Taker + } else if blockDiff == 1 { + // order1 came first, can't be IOC order + if m1.OrderType == ob.IOC { + return nil, ErrIOCOrderExpired, Order1 + } + // order0 came second, can't be post only order + if m0.OrderType == ob.Limit && m0.PostOnly { + return nil, ErrCrossingMarket, Order0 + } + output.Mode0 = Taker + output.Mode1 = Maker + } else { + // both orders were placed in same block + if m1.OrderType == ob.IOC { + // order1 is IOC, order0 is Limit or post only + output.Mode0 = Maker + output.Mode1 = Taker + } else { + // scenarios: + // 1. order0 is IOC, order1 is Limit or post only + // 2. both order0 and order1 are Limit or post only (in that scenario we default to long being the taker order, which can sometimes result in a better execution price for them) + output.Mode0 = Taker + output.Mode1 = Maker + } + } + + if output.Mode0 == Maker { + output.FillPrice = utils.BigIntMin(m0.Price, upperBound) + } else { + output.FillPrice = utils.BigIntMax(m1.Price, lowerBound) + } + return &output, nil, NoError +} + +func ValidateLiquidationOrderAndDetermineFillPrice(bibliophile b.BibliophileClient, inputStruct *ValidateLiquidationOrderAndDetermineFillPriceInput) ValidateLiquidationOrderAndDetermineFillPriceOutput { + fillAmount := new(big.Int).Set(inputStruct.LiquidationAmount) + if fillAmount.Sign() <= 0 { + return getValidateLiquidationOrderAndDetermineFillPriceErrorOutput(ErrInvalidFillAmount, Generic, common.Hash{}) + } + + decodeStep0, err := hu.DecodeTypeAndEncodedOrder(inputStruct.Data) + if err != nil { + return getValidateLiquidationOrderAndDetermineFillPriceErrorOutput(err, Order0, common.Hash{}) + } + m0, err := validateOrder(bibliophile, decodeStep0.OrderType, decodeStep0.EncodedOrder, Liquidation, fillAmount) + if err != nil { + return getValidateLiquidationOrderAndDetermineFillPriceErrorOutput(err, Order0, m0.OrderHash) + } + + if m0.BaseAssetQuantity.Sign() < 0 { + fillAmount = new(big.Int).Neg(fillAmount) + } + + minSize := bibliophile.GetMinSizeRequirement(m0.AmmIndex.Int64()) + if new(big.Int).Mod(fillAmount, minSize).Cmp(big.NewInt(0)) != 0 { + return getValidateLiquidationOrderAndDetermineFillPriceErrorOutput(ErrNotMultiple, Generic, common.Hash{}) + } + + fillPrice, err := determineLiquidationFillPrice(bibliophile, m0) + if err != nil { + return getValidateLiquidationOrderAndDetermineFillPriceErrorOutput(err, Order0, m0.OrderHash) + } + + return ValidateLiquidationOrderAndDetermineFillPriceOutput{ + Err: "", + Element: uint8(NoError), + Res: IOrderHandlerLiquidationMatchingValidationRes{ + Instruction: IClearingHouseInstruction{ + AmmIndex: m0.AmmIndex, + Trader: m0.Trader, + OrderHash: m0.OrderHash, + Mode: uint8(Maker), + }, + OrderType: uint8(decodeStep0.OrderType), + EncodedOrder: decodeStep0.EncodedOrder, + FillPrice: fillPrice, + FillAmount: fillAmount, + }, + } +} + +func determineLiquidationFillPrice(bibliophile b.BibliophileClient, m0 *Metadata) (*big.Int, error) { + liqUpperBound, liqLowerBound := bibliophile.GetAcceptableBoundsForLiquidation(m0.AmmIndex.Int64()) + upperBound, lowerBound := bibliophile.GetUpperAndLowerBoundForMarket(m0.AmmIndex.Int64()) + if m0.BaseAssetQuantity.Sign() > 0 { + // we are liquidating a long position + // do not allow liquidation if order.Price < liqLowerBound, because that gives scope for malicious activity to a validator + if m0.Price.Cmp(liqLowerBound) == -1 { + return nil, ErrTooLow + } + return utils.BigIntMin(m0.Price, upperBound /* oracle spread upper bound */), nil + } + + // we are liquidating a short position + if m0.Price.Cmp(liqUpperBound) == 1 { + return nil, ErrTooHigh + } + return utils.BigIntMax(m0.Price, lowerBound /* oracle spread lower bound */), nil +} + +func validateOrder(bibliophile b.BibliophileClient, orderType ob.OrderType, encodedOrder []byte, side Side, fillAmount *big.Int) (metadata *Metadata, err error) { + if orderType == ob.Limit { + order, err := hu.DecodeLimitOrder(encodedOrder) + if err != nil { + return &Metadata{OrderHash: common.Hash{}}, err + } + return validateExecuteLimitOrder(bibliophile, order, side, fillAmount) + } + if orderType == ob.IOC { + order, err := hu.DecodeIOCOrder(encodedOrder) + if err != nil { + return &Metadata{OrderHash: common.Hash{}}, err + } + return validateExecuteIOCOrder(bibliophile, order, side, fillAmount) + } + if orderType == ob.Signed { + order, err := hu.DecodeSignedOrder(encodedOrder) + if err != nil { + return &Metadata{OrderHash: common.Hash{}}, err + } + return validateExecuteSignedOrder(bibliophile, order, side, fillAmount) + } + return &Metadata{OrderHash: common.Hash{}}, errors.New("invalid order type") +} + +func validateExecuteLimitOrder(bibliophile b.BibliophileClient, order *ob.LimitOrder, side Side, fillAmount *big.Int) (metadata *Metadata, err error) { + orderHash, err := order.Hash() + if err != nil { + return &Metadata{OrderHash: common.Hash{}}, err + } + if err := validateLimitOrderLike(bibliophile, &order.BaseOrder, bibliophile.GetOrderFilledAmount(orderHash), OrderStatus(bibliophile.GetOrderStatus(orderHash)), side, fillAmount); err != nil { + return &Metadata{OrderHash: orderHash}, err + } + return &Metadata{ + AmmIndex: order.AmmIndex, + Trader: order.Trader, + BaseAssetQuantity: order.BaseAssetQuantity, + BlockPlaced: bibliophile.GetBlockPlaced(orderHash), + Price: order.Price, + OrderHash: orderHash, + OrderType: ob.Limit, + PostOnly: order.PostOnly, + }, nil +} + +func validateExecuteIOCOrder(bibliophile b.BibliophileClient, order *ob.IOCOrder, side Side, fillAmount *big.Int) (metadata *Metadata, err error) { + orderHash, err := order.Hash() + if err != nil { + return &Metadata{OrderHash: common.Hash{}}, err + } + if ob.OrderType(order.OrderType) != ob.IOC { + return &Metadata{OrderHash: orderHash}, errors.New("not ioc order") + } + if order.ExpireAt.Uint64() < bibliophile.GetTimeStamp() { + return &Metadata{OrderHash: orderHash}, errors.New("ioc expired") + } + if err := validateLimitOrderLike(bibliophile, &order.BaseOrder, bibliophile.IOC_GetOrderFilledAmount(orderHash), OrderStatus(bibliophile.IOC_GetOrderStatus(orderHash)), side, fillAmount); err != nil { + return &Metadata{OrderHash: orderHash}, err + } + return &Metadata{ + AmmIndex: order.AmmIndex, + Trader: order.Trader, + BaseAssetQuantity: order.BaseAssetQuantity, + BlockPlaced: bibliophile.IOC_GetBlockPlaced(orderHash), + Price: order.Price, + OrderHash: orderHash, + OrderType: ob.IOC, + PostOnly: false, + }, nil +} + +func validateExecuteSignedOrder(bibliophile b.BibliophileClient, order *hu.SignedOrder, side Side, fillAmount *big.Int) (metadata *Metadata, err error) { + // these fields are only set in plugin/evm/limit_order.go.NewLimitOrderProcesser + // however, the above is not invoked until the node bootstraps completely, and hence causes the signed order match validations during bootstrap to fail + // here we hardcode the values for mainnet and aylin testnet + if hu.VerifyingContract == "" || hu.ChainId == 0 { + chainId := bibliophile.GetAccessibleState().GetSnowContext().ChainID + if strings.EqualFold(chainId.String(), "2jfjkB7NkK4v8zoaoWmh5eaABNW6ynjQvemPFZpgPQ7ugrmUXv") { // mainnet + hu.SetChainIdAndVerifyingSignedOrdersContract(1992, "0x211682829664a5e289885DE21897B094eF289d18") + } else if strings.EqualFold(chainId.String(), "2qR64ZGVHTJjTZTzEnQTDoD1oMVQMYFVaBtN5tDoYaDKfVY5Xz") { // aylin + hu.SetChainIdAndVerifyingSignedOrdersContract(486, "0xb589490250fAEaF7D80D0b5A41db5059d55A85Df") + } + } + + orderHash, err := order.Hash() + if err != nil { + return &Metadata{OrderHash: common.Hash{}}, err + } + trader, signer, err := hu.ValidateSignedOrder( + order, + hu.SignedOrderValidationFields{ + OrderHash: orderHash, + Now: bibliophile.GetTimeStamp(), + ActiveMarketsCount: bibliophile.GetActiveMarketsCount(), + MinSize: bibliophile.GetMinSizeRequirement(order.AmmIndex.Int64()), + PriceMultiplier: bibliophile.GetPriceMultiplier(bibliophile.GetMarketAddressFromMarketID(order.AmmIndex.Int64())), + Status: bibliophile.GetSignedOrderStatus(orderHash), + }, + ) + if err != nil { + return &Metadata{OrderHash: orderHash}, err + } + + log.Info("validateExecuteSignedOrder", "trader", trader, "signer", signer, "orderHash", orderHash) + if trader != signer && !bibliophile.IsTradingAuthority(trader, signer) { + return &Metadata{OrderHash: orderHash}, hu.ErrNoTradingAuthority + } + + // M1, M2 + if err := validateLimitOrderLike(bibliophile, &order.BaseOrder, bibliophile.GetSignedOrderFilledAmount(orderHash), Placed, side, fillAmount); err != nil { + return &Metadata{OrderHash: orderHash}, err + } + + // M3 + if !bibliophile.HasReferrer(order.Trader) { + return &Metadata{OrderHash: orderHash}, ErrNoReferrer + } + + return &Metadata{ + AmmIndex: order.AmmIndex, + Trader: order.Trader, + BaseAssetQuantity: order.BaseAssetQuantity, + BlockPlaced: big.NewInt(0), // will always be treated as a maker order + Price: order.Price, + OrderHash: orderHash, + OrderType: ob.Signed, + PostOnly: true, + }, nil +} + +func validateLimitOrderLike(bibliophile b.BibliophileClient, order *hu.BaseOrder, filledAmount *big.Int, status OrderStatus, side Side, fillAmount *big.Int) error { + if status != Placed { + return ErrInvalidOrder + } + + // in case of liquidations, side of the order is determined by the sign of the base asset quantity, so basically base asset quantity check is redundant + if side == Liquidation { + if order.BaseAssetQuantity.Sign() > 0 { + side = Long + } else if order.BaseAssetQuantity.Sign() < 0 { + side = Short + fillAmount = new(big.Int).Neg(fillAmount) + } + } + + market := bibliophile.GetMarketAddressFromMarketID(order.AmmIndex.Int64()) + if side == Long { + if order.BaseAssetQuantity.Sign() <= 0 { + return ErrNotLongOrder + } + if fillAmount.Sign() <= 0 { + return ErrInvalidFillAmount + } + if new(big.Int).Add(filledAmount, fillAmount).Cmp(order.BaseAssetQuantity) > 0 { + return ErrOverFill + } + if order.ReduceOnly { + posSize := bibliophile.GetSize(market, &order.Trader) + // posSize should be closed to continue to be Short + // this also returns err if posSize >= 0, which should not happen because we are executing a long reduceOnly order on this account + if new(big.Int).Add(posSize, fillAmount).Sign() > 0 { + return ErrReduceOnlyAmountExceeded + } + } + } else if side == Short { + if order.BaseAssetQuantity.Sign() >= 0 { + return ErrNotShortOrder + } + if fillAmount.Sign() >= 0 { + return ErrInvalidFillAmount + } + if new(big.Int).Add(filledAmount, fillAmount).Cmp(order.BaseAssetQuantity) < 0 { // all quantities are -ve + return ErrOverFill + } + if order.ReduceOnly { + posSize := bibliophile.GetSize(market, &order.Trader) + // posSize should continue to be Long + // this also returns is posSize <= 0, which should not happen because we are executing a short reduceOnly order on this account + if new(big.Int).Add(posSize, fillAmount).Sign() < 0 { + return ErrReduceOnlyAmountExceeded + } + } + } else { + return errors.New("invalid side") + } + return nil +} + +// Common +func reducesPosition(positionSize *big.Int, baseAssetQuantity *big.Int) bool { + if positionSize.Sign() == 1 && baseAssetQuantity.Sign() == -1 && big.NewInt(0).Add(positionSize, baseAssetQuantity).Sign() != -1 { + return true + } + if positionSize.Sign() == -1 && baseAssetQuantity.Sign() == 1 && big.NewInt(0).Add(positionSize, baseAssetQuantity).Sign() != 1 { + return true + } + return false +} + +func getRequiredMargin(bibliophile b.BibliophileClient, order ILimitOrderBookOrder) *big.Int { + price := order.Price + upperBound, _ := bibliophile.GetUpperAndLowerBoundForMarket(order.AmmIndex.Int64()) + if order.BaseAssetQuantity.Sign() == -1 && order.Price.Cmp(upperBound) == -1 { + price = upperBound + } + quoteAsset := big.NewInt(0).Abs(big.NewInt(0).Div(new(big.Int).Mul(order.BaseAssetQuantity, price), big.NewInt(1e18))) + requiredMargin := big.NewInt(0).Div(big.NewInt(0).Mul(bibliophile.GetMinAllowableMargin(), quoteAsset), big.NewInt(1e6)) + takerFee := big.NewInt(0).Div(big.NewInt(0).Mul(quoteAsset, bibliophile.GetTakerFee()), big.NewInt(1e6)) + requiredMargin.Add(requiredMargin, takerFee) + return requiredMargin +} + +func formatOrder(orderBytes []byte) interface{} { + decodeStep0, err := hu.DecodeTypeAndEncodedOrder(orderBytes) + if err != nil { + return orderBytes + } + + if decodeStep0.OrderType == ob.Limit { + order, err := hu.DecodeLimitOrder(decodeStep0.EncodedOrder) + if err != nil { + return decodeStep0 + } + orderJson := order.Map() + orderHash, err := order.Hash() + if err != nil { + return orderJson + } + orderJson["hash"] = orderHash.String() + return orderJson + } + if decodeStep0.OrderType == ob.IOC { + order, err := hu.DecodeIOCOrder(decodeStep0.EncodedOrder) + if err != nil { + return decodeStep0 + } + orderJson := order.Map() + orderHash, err := order.Hash() + if err != nil { + return orderJson + } + orderJson["hash"] = orderHash.String() + return orderJson + } + return nil +} + +func getValidateOrdersAndDetermineFillPriceErrorOutput(err error, element BadElement, orderHash common.Hash) ValidateOrdersAndDetermineFillPriceOutput { + // need to provide an empty res because PackValidateOrdersAndDetermineFillPriceOutput fails if FillPrice is nil, and if res.Instructions[0].AmmIndex is nil + emptyRes := IOrderHandlerMatchingValidationRes{ + Instructions: [2]IClearingHouseInstruction{ + IClearingHouseInstruction{AmmIndex: big.NewInt(0)}, + IClearingHouseInstruction{AmmIndex: big.NewInt(0)}, + }, + OrderTypes: [2]uint8{}, + EncodedOrders: [2][]byte{}, + FillPrice: big.NewInt(0), + } + + var errorString string + if err != nil { + // should always be true + errorString = err.Error() + } + if (element == Order0 || element == Order1) && orderHash != (common.Hash{}) { + emptyRes.Instructions[element].OrderHash = orderHash + } + return ValidateOrdersAndDetermineFillPriceOutput{Err: errorString, Element: uint8(element), Res: emptyRes} +} + +func getValidateLiquidationOrderAndDetermineFillPriceErrorOutput(err error, element BadElement, orderHash common.Hash) ValidateLiquidationOrderAndDetermineFillPriceOutput { + emptyRes := IOrderHandlerLiquidationMatchingValidationRes{ + Instruction: IClearingHouseInstruction{AmmIndex: big.NewInt(0)}, + OrderType: 0, + EncodedOrder: []byte{}, + FillPrice: big.NewInt(0), + FillAmount: big.NewInt(0), + } + + var errorString string + if err != nil { + // should always be true + errorString = err.Error() + } + if element == Order0 && orderHash != (common.Hash{}) { + emptyRes.Instruction.OrderHash = orderHash + } + return ValidateLiquidationOrderAndDetermineFillPriceOutput{Err: errorString, Element: uint8(element), Res: emptyRes} +} diff --git a/precompile/contracts/jurorv2/matching_validation_test.go b/precompile/contracts/jurorv2/matching_validation_test.go new file mode 100644 index 0000000000..12ac568786 --- /dev/null +++ b/precompile/contracts/jurorv2/matching_validation_test.go @@ -0,0 +1,1479 @@ +// Code generated +// This file is a generated precompile contract test with the skeleton of test functions. +// The file is generated by a template. Please inspect every code and comment in this file before use. + +package jurorv2 + +import ( + "encoding/hex" + "fmt" + "math/big" + "strings" + + "testing" + + ob "github.com/ava-labs/subnet-evm/plugin/evm/orderbook" + hu "github.com/ava-labs/subnet-evm/plugin/evm/orderbook/hubbleutils" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/assert" + + b "github.com/ava-labs/subnet-evm/precompile/contracts/bibliophile" + gomock "github.com/golang/mock/gomock" +) + +func TestValidateLimitOrderLike(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockBibliophile := b.NewMockBibliophileClient(ctrl) + + trader := common.HexToAddress("0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC") + order := &hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(10), + Price: big.NewInt(20), + Salt: big.NewInt(1), + ReduceOnly: false, + } + filledAmount := big.NewInt(5) + fillAmount := big.NewInt(5) + + t.Run("Side=Long", func(t *testing.T) { + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(gomock.Any()).Return(common.Address{}).AnyTimes() + t.Run("OrderStatus != Placed will throw error", func(t *testing.T) { + err := validateLimitOrderLike(mockBibliophile, order, filledAmount, Invalid, Long, fillAmount) + assert.EqualError(t, err, ErrInvalidOrder.Error()) + + err = validateLimitOrderLike(mockBibliophile, order, filledAmount, Filled, Long, fillAmount) + assert.EqualError(t, err, ErrInvalidOrder.Error()) + + err = validateLimitOrderLike(mockBibliophile, order, filledAmount, Cancelled, Long, fillAmount) + assert.EqualError(t, err, ErrInvalidOrder.Error()) + }) + + t.Run("base asset quantity <= 0", func(t *testing.T) { + badOrder := *order + badOrder.BaseAssetQuantity = big.NewInt(-23) + + err := validateLimitOrderLike(mockBibliophile, &badOrder, filledAmount, Placed, Long, fillAmount) + assert.EqualError(t, err, ErrNotLongOrder.Error()) + + badOrder.BaseAssetQuantity = big.NewInt(0) + err = validateLimitOrderLike(mockBibliophile, &badOrder, filledAmount, Placed, Long, fillAmount) + assert.EqualError(t, err, ErrNotLongOrder.Error()) + }) + + t.Run("ErrOverFill", func(t *testing.T) { + fillAmount := big.NewInt(6) + + err := validateLimitOrderLike(mockBibliophile, order, filledAmount, Placed, Long, fillAmount) + assert.EqualError(t, err, ErrOverFill.Error()) + }) + + t.Run("negative fillAmount", func(t *testing.T) { + fillAmount := big.NewInt(-6) + + err := validateLimitOrderLike(mockBibliophile, order, filledAmount, Placed, Long, fillAmount) + assert.EqualError(t, err, ErrInvalidFillAmount.Error()) + }) + + t.Run("ErrReduceOnlyAmountExceeded", func(t *testing.T) { + badOrder := *order + badOrder.ReduceOnly = true + + for i := int64(10); /* any +ve # */ i > new(big.Int).Neg(fillAmount).Int64(); i-- { + mockBibliophile.EXPECT().GetSize(gomock.Any(), gomock.Any()).Return(big.NewInt(i)).Times(1) + err := validateLimitOrderLike(mockBibliophile, &badOrder, filledAmount, Placed, Long, fillAmount) + assert.EqualError(t, err, ErrReduceOnlyAmountExceeded.Error()) + } + }) + + t.Run("all conditions met for reduceOnly order", func(t *testing.T) { + badOrder := *order + badOrder.ReduceOnly = true + + start := new(big.Int).Neg(fillAmount).Int64() + for i := start; i > start-5; i-- { + mockBibliophile.EXPECT().GetSize(gomock.Any(), gomock.Any()).Return(big.NewInt(i)).Times(1) + err := validateLimitOrderLike(mockBibliophile, &badOrder, filledAmount, Placed, Long, fillAmount) + assert.Nil(t, err) + } + }) + + t.Run("all conditions met", func(t *testing.T) { + err := validateLimitOrderLike(mockBibliophile, order, filledAmount, Placed, Long, fillAmount) + assert.Nil(t, err) + }) + }) + + t.Run("Side=Short", func(t *testing.T) { + order := &hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(-10), + Price: big.NewInt(20), + Salt: big.NewInt(1), + ReduceOnly: false, + } + filledAmount := big.NewInt(-5) + fillAmount := big.NewInt(-5) + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(gomock.Any()).Return(common.Address{}).AnyTimes() + t.Run("OrderStatus != Placed will throw error", func(t *testing.T) { + err := validateLimitOrderLike(mockBibliophile, order, filledAmount, Invalid, Short, fillAmount) + assert.EqualError(t, err, ErrInvalidOrder.Error()) + + err = validateLimitOrderLike(mockBibliophile, order, filledAmount, Filled, Short, fillAmount) + assert.EqualError(t, err, ErrInvalidOrder.Error()) + + err = validateLimitOrderLike(mockBibliophile, order, filledAmount, Cancelled, Short, fillAmount) + assert.EqualError(t, err, ErrInvalidOrder.Error()) + }) + + t.Run("base asset quantity >= 0", func(t *testing.T) { + badOrder := *order + badOrder.BaseAssetQuantity = big.NewInt(23) + + err := validateLimitOrderLike(mockBibliophile, &badOrder, filledAmount, Placed, Short, fillAmount) + assert.EqualError(t, err, ErrNotShortOrder.Error()) + + badOrder.BaseAssetQuantity = big.NewInt(0) + err = validateLimitOrderLike(mockBibliophile, &badOrder, filledAmount, Placed, Short, fillAmount) + assert.EqualError(t, err, ErrNotShortOrder.Error()) + }) + + t.Run("positive fillAmount", func(t *testing.T) { + fillAmount := big.NewInt(6) + + err := validateLimitOrderLike(mockBibliophile, order, filledAmount, Placed, Short, fillAmount) + assert.EqualError(t, err, ErrInvalidFillAmount.Error()) + }) + + t.Run("ErrOverFill", func(t *testing.T) { + fillAmount := big.NewInt(-6) + + err := validateLimitOrderLike(mockBibliophile, order, filledAmount, Placed, Short, fillAmount) + assert.EqualError(t, err, ErrOverFill.Error()) + }) + + t.Run("ErrReduceOnlyAmountExceeded", func(t *testing.T) { + badOrder := *order + badOrder.ReduceOnly = true + + for i := int64(-10); /* any -ve # */ i < new(big.Int).Abs(fillAmount).Int64(); i++ { + mockBibliophile.EXPECT().GetSize(gomock.Any(), gomock.Any()).Return(big.NewInt(i)).Times(1) + err := validateLimitOrderLike(mockBibliophile, &badOrder, filledAmount, Placed, Short, fillAmount) + assert.EqualError(t, err, ErrReduceOnlyAmountExceeded.Error()) + } + }) + + t.Run("all conditions met for reduceOnly order", func(t *testing.T) { + badOrder := *order + badOrder.ReduceOnly = true + + start := new(big.Int).Abs(fillAmount).Int64() + for i := start; i < start+5; i++ { + mockBibliophile.EXPECT().GetSize(gomock.Any(), gomock.Any()).Return(big.NewInt(i)).Times(1) + err := validateLimitOrderLike(mockBibliophile, &badOrder, filledAmount, Placed, Short, fillAmount) + assert.Nil(t, err) + } + }) + + t.Run("all conditions met", func(t *testing.T) { + err := validateLimitOrderLike(mockBibliophile, order, filledAmount, Placed, Short, fillAmount) + assert.Nil(t, err) + }) + }) + + t.Run("invalid side", func(t *testing.T) { + order := &hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(10), + Price: big.NewInt(20), + Salt: big.NewInt(1), + ReduceOnly: false, + } + filledAmount := big.NewInt(0) + fillAmount := big.NewInt(5) + + err := validateLimitOrderLike(mockBibliophile, order, filledAmount, Placed, Side(4), fillAmount) // assuming 4 is an invalid Side value + assert.EqualError(t, err, "invalid side") + }) +} + +func TestValidateExecuteSignedOrder(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockBibliophile := b.NewMockBibliophileClient(ctrl) + + t.Run("validateExecuteSignedOrder - long", func(t *testing.T) { + hu.SetChainIdAndVerifyingSignedOrdersContract(321123, "0x4c5859f0F772848b2D91F1D83E2Fe57935348029") + orderHash := strings.TrimPrefix("0x73d5196ac9576efaccb6e54b193b894e2cc0afd68ce5af519c901fec7e588595", "0x") + signature := strings.TrimPrefix("0x3027ae4ab98663490d0facab04c71665e41da867a44b7ddc29e14cb8de3a3cfa12985be54945ce040196b2fcdcc4dafc56f7955ee72628bc9e7a634a7f258ce61c", "0x") + sig, err := hex.DecodeString(signature) + assert.Nil(t, err) + order := &hu.SignedOrder{ + LimitOrder: hu.LimitOrder{ + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8"), + BaseAssetQuantity: big.NewInt(5000000000000000000), // 5 + Price: big.NewInt(1000000000), + Salt: big.NewInt(1688994806105), + ReduceOnly: false, + }, + PostOnly: true, + }, + OrderType: 2, + ExpireAt: big.NewInt(1688994854), + Sig: sig, + } + filledAmount := big.NewInt(2000000000000000000) // 2 + fillAmount := big.NewInt(3000000000000000000) // 3 + testValidateExecuteSignedOrder(t, mockBibliophile, order, orderHash, Long, fillAmount, filledAmount) + }) + + t.Run("validateExecuteSignedOrder - short", func(t *testing.T) { + hu.SetChainIdAndVerifyingSignedOrdersContract(321123, "0x809d550fca64d94Bd9F66E60752A544199cfAC3D") + orderHash := strings.TrimPrefix("0xee4b26ae386d1c88f89eb2f8b4b4205271576742f5ff4e0488633612f7a9a5e7", "0x") + signature := strings.TrimPrefix("0xb2704b73b99f2700ecc90a218f514c254d1f5d46af47117f5317f6cc0348ce962dcfb024c7264fdeb1f1513e4564c2a7cd9c1d0be33d7b934cd5a73b96440eaf1c", "0x") + sig, err := hex.DecodeString(signature) + assert.Nil(t, err) + order := &hu.SignedOrder{ + LimitOrder: hu.LimitOrder{ + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8"), + BaseAssetQuantity: big.NewInt(-5000000000000000000), // -5 + Price: big.NewInt(1000000000), + Salt: big.NewInt(1688994806105), + ReduceOnly: false, + }, + PostOnly: true, + }, + OrderType: 2, + ExpireAt: big.NewInt(1688994854), + Sig: sig, + } + filledAmount := big.NewInt(-2000000000000000000) // -2 + fillAmount := big.NewInt(-3000000000000000000) // -3 + testValidateExecuteSignedOrder(t, mockBibliophile, order, orderHash, Short, fillAmount, filledAmount) + }) + + // t.Run("validateExecuteLimitOrder returns orderHash even when validation fails", func(t *testing.T) { + // orderHash, err := order.Hash() + // assert.Nil(t, err) + + // mockBibliophile.EXPECT().GetOrderFilledAmount(orderHash).Return(filledAmount).Times(1) + // mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(2)).Times(1) // Filled + + // m, err := validateExecuteLimitOrder(mockBibliophile, order, Long, fillAmount) + // assert.EqualError(t, err, ErrInvalidOrder.Error()) + // assert.Equal(t, orderHash, m.OrderHash) + // }) +} + +func testValidateExecuteSignedOrder(t *testing.T, mockBibliophile *b.MockBibliophileClient, order *hu.SignedOrder, orderHash string, side Side, fillAmount, filledAmount *big.Int) { + h, err := order.Hash() + assert.Nil(t, err) + assert.Equal(t, orderHash, strings.TrimPrefix(h.Hex(), "0x")) + + encodedOrder, err := order.EncodeToABIWithoutType() + assert.Nil(t, err) + + marketAddress := common.HexToAddress("0xa72b463C21dA61cCc86069cFab82e9e8491152a0") + mockBibliophile.EXPECT().GetTimeStamp().Return(order.ExpireAt.Uint64()).Times(1) + mockBibliophile.EXPECT().GetActiveMarketsCount().Return(int64(1)).Times(1) + mockBibliophile.EXPECT().GetMinSizeRequirement(order.AmmIndex.Int64()).Return(big.NewInt(1e18)) + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(order.AmmIndex.Int64()).Return(marketAddress).Times(2) + mockBibliophile.EXPECT().GetPriceMultiplier(marketAddress).Return(big.NewInt(1e6)) + mockBibliophile.EXPECT().GetSignedOrderStatus(h).Return(int64(0)).Times(1) // Invalid + mockBibliophile.EXPECT().GetSignedOrderFilledAmount(h).Return(filledAmount).Times(1) + mockBibliophile.EXPECT().HasReferrer(order.Trader).Return(true).Times(1) + + m, err := validateOrder(mockBibliophile, ob.Signed, encodedOrder, side, fillAmount) + assert.Nil(t, err) + assertMetadataEquality(t, &Metadata{ + AmmIndex: new(big.Int).Set(order.AmmIndex), + Trader: order.Trader, + BaseAssetQuantity: new(big.Int).Set(order.BaseAssetQuantity), + BlockPlaced: big.NewInt(0), + Price: new(big.Int).Set(order.Price), + OrderHash: h, + OrderType: ob.Signed, + PostOnly: true, + }, m) +} + +func TestValidateExecuteLimitOrder(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockBibliophile := b.NewMockBibliophileClient(ctrl) + marketAddress := common.HexToAddress("0xa72b463C21dA61cCc86069cFab82e9e8491152a0") + trader := common.HexToAddress("0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC") + + order := &hu.LimitOrder{ + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(534), + Trader: trader, + BaseAssetQuantity: big.NewInt(10), + Price: big.NewInt(20), + Salt: big.NewInt(1), + ReduceOnly: false, + }, + PostOnly: false, + } + filledAmount := big.NewInt(5) + fillAmount := big.NewInt(5) + + t.Run("validateExecuteLimitOrder", func(t *testing.T) { + orderHash, err := order.Hash() + assert.Nil(t, err) + encodedOrder, err := order.EncodeToABIWithoutType() + assert.Nil(t, err) + + blockPlaced := big.NewInt(42) + mockBibliophile.EXPECT().GetOrderFilledAmount(orderHash).Return(filledAmount).Times(1) + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(1)).Times(1) // placed + mockBibliophile.EXPECT().GetBlockPlaced(orderHash).Return(blockPlaced).Times(1) // placed + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(order.AmmIndex.Int64()).Return(marketAddress).Times(1) // placed + + m, err := validateOrder(mockBibliophile, ob.Limit, encodedOrder, Long, fillAmount) + assert.Nil(t, err) + assertMetadataEquality(t, &Metadata{ + AmmIndex: new(big.Int).Set(order.AmmIndex), + Trader: trader, + BaseAssetQuantity: new(big.Int).Set(order.BaseAssetQuantity), + BlockPlaced: blockPlaced, + Price: new(big.Int).Set(order.Price), + OrderHash: orderHash, + }, m) + }) + + t.Run("validateExecuteLimitOrder returns orderHash even when validation fails", func(t *testing.T) { + orderHash, err := order.Hash() + assert.Nil(t, err) + encodedOrder, err := order.EncodeToABIWithoutType() + assert.Nil(t, err) + + mockBibliophile.EXPECT().GetOrderFilledAmount(orderHash).Return(filledAmount).Times(1) + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(2)).Times(1) // Filled + + m, err := validateOrder(mockBibliophile, ob.Limit, encodedOrder, Long, fillAmount) + assert.EqualError(t, err, ErrInvalidOrder.Error()) + assert.Equal(t, orderHash, m.OrderHash) + }) +} + +func TestValidateExecuteIOCOrder(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockBibliophile := b.NewMockBibliophileClient(ctrl) + marketAddress := common.HexToAddress("0xa72b463C21dA61cCc86069cFab82e9e8491152a0") + trader := common.HexToAddress("0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC") + + order := &hu.IOCOrder{ + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(534), + Trader: trader, + BaseAssetQuantity: big.NewInt(10), + Price: big.NewInt(20), + Salt: big.NewInt(1), + ReduceOnly: false, + }, + OrderType: uint8(ob.IOC), + ExpireAt: big.NewInt(65), + } + filledAmount := big.NewInt(5) + fillAmount := big.NewInt(5) + + t.Run("validateExecuteIOCOrder success", func(t *testing.T) { + orderHash, err := order.Hash() + assert.Nil(t, err) + encodedOrder, err := order.EncodeToABIWithoutType() + assert.Nil(t, err) + + blockPlaced := big.NewInt(42) + mockBibliophile.EXPECT().IOC_GetOrderFilledAmount(orderHash).Return(filledAmount).Times(1) + mockBibliophile.EXPECT().IOC_GetOrderStatus(orderHash).Return(int64(1)).Times(1) // placed + mockBibliophile.EXPECT().IOC_GetBlockPlaced(orderHash).Return(blockPlaced).Times(1) // placed + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(order.AmmIndex.Int64()).Return(marketAddress).Times(1) // placed + mockBibliophile.EXPECT().GetTimeStamp().Return(uint64(60)).Times(1) // placed + + m, err := validateOrder(mockBibliophile, ob.IOC, encodedOrder, Long, fillAmount) + assert.Nil(t, err) + assertMetadataEquality(t, &Metadata{ + AmmIndex: new(big.Int).Set(order.AmmIndex), + Trader: trader, + BaseAssetQuantity: new(big.Int).Set(order.BaseAssetQuantity), + BlockPlaced: blockPlaced, + Price: new(big.Int).Set(order.Price), + OrderHash: orderHash, + }, m) + }) + + t.Run("validateExecuteIOCOrder not ioc order", func(t *testing.T) { + order.OrderType = uint8(ob.Limit) + orderHash, err := order.Hash() + assert.Nil(t, err) + encodedOrder, err := order.EncodeToABIWithoutType() + assert.Nil(t, err) + + m, err := validateOrder(mockBibliophile, ob.IOC, encodedOrder, Long, fillAmount) + assert.NotNil(t, err) + assert.Equal(t, err.Error(), "not ioc order") + assert.Equal(t, m.OrderHash, orderHash) + }) + + t.Run("validateExecuteIOCOrder order expires", func(t *testing.T) { + order.OrderType = uint8(ob.IOC) + orderHash, err := order.Hash() + assert.Nil(t, err) + encodedOrder, err := order.EncodeToABIWithoutType() + assert.Nil(t, err) + + mockBibliophile.EXPECT().GetTimeStamp().Return(uint64(66)).Times(1) + + m, err := validateOrder(mockBibliophile, ob.IOC, encodedOrder, Long, fillAmount) + assert.NotNil(t, err) + assert.Equal(t, err.Error(), "ioc expired") + assert.Equal(t, m.OrderHash, orderHash) + }) +} + +func assertMetadataEquality(t *testing.T, expected, actual *Metadata) { + assert.Equal(t, expected.AmmIndex.Int64(), actual.AmmIndex.Int64()) + assert.Equal(t, expected.Trader, actual.Trader) + assert.Equal(t, expected.BaseAssetQuantity, actual.BaseAssetQuantity) + assert.Equal(t, expected.BlockPlaced, actual.BlockPlaced) + assert.Equal(t, expected.Price, actual.Price) + assert.Equal(t, expected.OrderHash, actual.OrderHash) +} + +func TestDetermineFillPrice(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockBibliophile := b.NewMockBibliophileClient(ctrl) + + oraclePrice := hu.Mul1e6(big.NewInt(20)) // $20 + spreadLimit := new(big.Int).Mul(big.NewInt(50), big.NewInt(1e4)) // 50% + upperbound := hu.Div1e6(new(big.Int).Mul(oraclePrice, new(big.Int).Add(big.NewInt(1e6), spreadLimit))) // $10 + lowerbound := hu.Div1e6(new(big.Int).Mul(oraclePrice, new(big.Int).Sub(big.NewInt(1e6), spreadLimit))) // $30 + market := int64(5) + + t.Run("long order came first", func(t *testing.T) { + blockPlaced0 := big.NewInt(69) + blockPlaced1 := big.NewInt(70) + t.Run("long price < lower bound", func(t *testing.T) { + t.Run("short price < long price", func(t *testing.T) { + m0 := &Metadata{ + Price: hu.Mul1e6(big.NewInt(9)), + AmmIndex: big.NewInt(market), + BlockPlaced: blockPlaced0, + } + m1 := &Metadata{ + Price: hu.Mul1e6(big.NewInt(8)), + BlockPlaced: blockPlaced1, + } + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(market).Return(upperbound, lowerbound).Times(1) + output, err, _ := determineFillPrice(mockBibliophile, m0, m1) + assert.Nil(t, output) + assert.Equal(t, ErrTooLow, err) + }) + + t.Run("short price == long price", func(t *testing.T) { + m0 := &Metadata{ + Price: hu.Mul1e6(big.NewInt(7)), + AmmIndex: big.NewInt(market), + BlockPlaced: blockPlaced0, + } + m1 := &Metadata{ + Price: hu.Mul1e6(big.NewInt(7)), + BlockPlaced: blockPlaced1, + } + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(market).Return(upperbound, lowerbound).Times(1) + output, err, _ := determineFillPrice(mockBibliophile, m0, m1) + assert.Nil(t, output) + assert.Equal(t, ErrTooLow, err) + }) + }) + + t.Run("long price == lower bound", func(t *testing.T) { + longPrice := lowerbound + t.Run("short price < long price", func(t *testing.T) { + m0 := &Metadata{ + Price: longPrice, + AmmIndex: big.NewInt(market), + BlockPlaced: blockPlaced0, + } + m1 := &Metadata{ + Price: new(big.Int).Sub(longPrice, big.NewInt(1)), + BlockPlaced: blockPlaced1, + } + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(market).Return(upperbound, lowerbound).Times(1) + output, err, _ := determineFillPrice(mockBibliophile, m0, m1) + assert.Nil(t, err) + assert.Equal(t, FillPriceAndModes{longPrice, Maker, Taker}, *output) + }) + + t.Run("short price == long price", func(t *testing.T) { + m0 := &Metadata{ + Price: longPrice, + AmmIndex: big.NewInt(market), + BlockPlaced: blockPlaced0, + } + m1 := &Metadata{ + Price: longPrice, + BlockPlaced: blockPlaced1, + } + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(market).Return(upperbound, lowerbound).Times(1) + output, err, _ := determineFillPrice(mockBibliophile, m0, m1) + assert.Nil(t, err) + assert.Equal(t, FillPriceAndModes{longPrice, Maker, Taker}, *output) + }) + }) + + t.Run("lowerbound < long price < oracle", func(t *testing.T) { + longPrice := hu.Mul1e6(big.NewInt(15)) + t.Run("short price < long price", func(t *testing.T) { + m0 := &Metadata{ + Price: longPrice, + AmmIndex: big.NewInt(market), + BlockPlaced: blockPlaced0, + } + m1 := &Metadata{ + Price: new(big.Int).Sub(longPrice, big.NewInt(1)), + BlockPlaced: blockPlaced1, + } + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(market).Return(upperbound, lowerbound).Times(1) + output, err, _ := determineFillPrice(mockBibliophile, m0, m1) + assert.Nil(t, err) + assert.Equal(t, FillPriceAndModes{longPrice, Maker, Taker}, *output) + }) + + t.Run("short price == long price", func(t *testing.T) { + m0 := &Metadata{ + Price: longPrice, + AmmIndex: big.NewInt(market), + BlockPlaced: blockPlaced0, + } + m1 := &Metadata{ + Price: longPrice, + BlockPlaced: blockPlaced1, + } + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(market).Return(upperbound, lowerbound).Times(1) + output, err, _ := determineFillPrice(mockBibliophile, m0, m1) + assert.Nil(t, err) + assert.Equal(t, FillPriceAndModes{longPrice, Maker, Taker}, *output) + }) + }) + + t.Run("long price == oracle", func(t *testing.T) { + longPrice := oraclePrice + t.Run("short price < long price", func(t *testing.T) { + m0 := &Metadata{ + Price: longPrice, + AmmIndex: big.NewInt(market), + BlockPlaced: blockPlaced0, + } + m1 := &Metadata{ + Price: new(big.Int).Sub(longPrice, big.NewInt(1)), + BlockPlaced: blockPlaced1, + } + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(market).Return(upperbound, lowerbound).Times(1) + output, err, _ := determineFillPrice(mockBibliophile, m0, m1) + assert.Nil(t, err) + assert.Equal(t, FillPriceAndModes{longPrice, Maker, Taker}, *output) + }) + + t.Run("short price == long price", func(t *testing.T) { + m0 := &Metadata{ + Price: longPrice, + AmmIndex: big.NewInt(market), + BlockPlaced: blockPlaced0, + } + m1 := &Metadata{ + Price: longPrice, + BlockPlaced: blockPlaced1, + } + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(market).Return(upperbound, lowerbound).Times(1) + output, err, _ := determineFillPrice(mockBibliophile, m0, m1) + assert.Nil(t, err) + assert.Equal(t, FillPriceAndModes{longPrice, Maker, Taker}, *output) + }) + }) + + t.Run("oracle < long price < upper bound", func(t *testing.T) { + longPrice := hu.Mul1e6(big.NewInt(25)) + t.Run("short price < long price", func(t *testing.T) { + m0 := &Metadata{ + Price: longPrice, + AmmIndex: big.NewInt(market), + BlockPlaced: blockPlaced0, + } + m1 := &Metadata{ + Price: new(big.Int).Sub(longPrice, big.NewInt(1)), + BlockPlaced: blockPlaced1, + } + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(market).Return(upperbound, lowerbound).Times(1) + output, err, _ := determineFillPrice(mockBibliophile, m0, m1) + assert.Nil(t, err) + assert.Equal(t, FillPriceAndModes{longPrice, Maker, Taker}, *output) + }) + + t.Run("short price == long price", func(t *testing.T) { + m0 := &Metadata{ + Price: longPrice, + AmmIndex: big.NewInt(market), + BlockPlaced: blockPlaced0, + } + m1 := &Metadata{ + Price: longPrice, + BlockPlaced: blockPlaced1, + } + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(market).Return(upperbound, lowerbound).Times(1) + output, err, _ := determineFillPrice(mockBibliophile, m0, m1) + assert.Nil(t, err) + assert.Equal(t, FillPriceAndModes{longPrice, Maker, Taker}, *output) + }) + }) + + t.Run("long price == upper bound", func(t *testing.T) { + longPrice := upperbound + t.Run("short price < long price", func(t *testing.T) { + m0 := &Metadata{ + Price: longPrice, + AmmIndex: big.NewInt(market), + BlockPlaced: blockPlaced0, + } + m1 := &Metadata{ + Price: new(big.Int).Sub(longPrice, big.NewInt(1)), + BlockPlaced: blockPlaced1, + } + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(market).Return(upperbound, lowerbound).Times(1) + output, err, _ := determineFillPrice(mockBibliophile, m0, m1) + assert.Nil(t, err) + assert.Equal(t, FillPriceAndModes{longPrice, Maker, Taker}, *output) + }) + + t.Run("short price == long price", func(t *testing.T) { + m0 := &Metadata{ + Price: longPrice, + AmmIndex: big.NewInt(market), + BlockPlaced: blockPlaced0, + } + m1 := &Metadata{ + Price: longPrice, + BlockPlaced: blockPlaced1, + } + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(market).Return(upperbound, lowerbound).Times(1) + output, err, _ := determineFillPrice(mockBibliophile, m0, m1) + assert.Nil(t, err) + assert.Equal(t, FillPriceAndModes{longPrice, Maker, Taker}, *output) + }) + }) + + t.Run("upper bound < long price", func(t *testing.T) { + longPrice := new(big.Int).Add(upperbound, big.NewInt(42)) + t.Run("upper < short price < long price", func(t *testing.T) { + m0 := &Metadata{ + Price: longPrice, + AmmIndex: big.NewInt(market), + BlockPlaced: blockPlaced0, + } + m1 := &Metadata{ + Price: new(big.Int).Add(upperbound, big.NewInt(1)), + BlockPlaced: blockPlaced1, + } + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(market).Return(upperbound, lowerbound).Times(1) + output, err, _ := determineFillPrice(mockBibliophile, m0, m1) + assert.Nil(t, output) + assert.Equal(t, ErrTooHigh, err) + }) + + t.Run("upper == short price < long price", func(t *testing.T) { + m0 := &Metadata{ + Price: longPrice, + AmmIndex: big.NewInt(market), + BlockPlaced: blockPlaced0, + } + m1 := &Metadata{ + Price: upperbound, + BlockPlaced: blockPlaced1, + } + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(market).Return(upperbound, lowerbound).Times(1) + output, err, _ := determineFillPrice(mockBibliophile, m0, m1) + assert.Nil(t, err) + assert.Equal(t, FillPriceAndModes{upperbound, Maker, Taker}, *output) + }) + + t.Run("short price < upper", func(t *testing.T) { + m0 := &Metadata{ + Price: longPrice, + AmmIndex: big.NewInt(market), + BlockPlaced: blockPlaced0, + } + m1 := &Metadata{ + Price: new(big.Int).Sub(upperbound, big.NewInt(1)), + BlockPlaced: blockPlaced1, + } + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(market).Return(upperbound, lowerbound).Times(1) + output, err, _ := determineFillPrice(mockBibliophile, m0, m1) + assert.Nil(t, err) + assert.Equal(t, FillPriceAndModes{upperbound, Maker, Taker}, *output) + }) + + t.Run("short price < lower", func(t *testing.T) { + m0 := &Metadata{ + Price: longPrice, + AmmIndex: big.NewInt(market), + BlockPlaced: blockPlaced0, + } + m1 := &Metadata{ + Price: new(big.Int).Sub(lowerbound, big.NewInt(1)), + BlockPlaced: blockPlaced1, + } + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(market).Return(upperbound, lowerbound).Times(1) + output, err, _ := determineFillPrice(mockBibliophile, m0, m1) + assert.Nil(t, err) + assert.Equal(t, FillPriceAndModes{upperbound, Maker, Taker}, *output) + }) + }) + }) + + t.Run("short order came first", func(t *testing.T) { + blockPlaced0 := big.NewInt(70) + blockPlaced1 := big.NewInt(69) + t.Run("short price < long price", func(t *testing.T) { + m0 := &Metadata{ + Price: hu.Mul1e6(big.NewInt(20)), + AmmIndex: big.NewInt(market), + BlockPlaced: blockPlaced0, + } + m1 := &Metadata{ + Price: hu.Mul1e6(big.NewInt(19)), + BlockPlaced: blockPlaced1, + } + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(market).Return(upperbound, lowerbound).Times(1) + output, err, _ := determineFillPrice(mockBibliophile, m0, m1) + assert.Nil(t, err) + assert.Equal(t, FillPriceAndModes{m1.Price, Taker, Maker}, *output) + }) + t.Run("short order is IOC", func(t *testing.T) { + m0 := &Metadata{ + Price: hu.Mul1e6(big.NewInt(20)), + AmmIndex: big.NewInt(market), + BlockPlaced: blockPlaced0, + } + m1 := &Metadata{ + Price: hu.Mul1e6(big.NewInt(19)), + BlockPlaced: blockPlaced1, + OrderType: ob.IOC, + } + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(market).Return(upperbound, lowerbound).Times(1) + output, err, errorOrder := determineFillPrice(mockBibliophile, m0, m1) + assert.Nil(t, output) + assert.Equal(t, ErrIOCOrderExpired, err) + assert.Equal(t, Order1, errorOrder) + }) + t.Run("long order is post only", func(t *testing.T) { + m0 := &Metadata{ + Price: hu.Mul1e6(big.NewInt(20)), + AmmIndex: big.NewInt(market), + BlockPlaced: blockPlaced0, + OrderType: ob.Limit, + PostOnly: true, + } + m1 := &Metadata{ + Price: hu.Mul1e6(big.NewInt(19)), + BlockPlaced: blockPlaced1, + } + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(market).Return(upperbound, lowerbound).Times(1) + output, err, errorOrder := determineFillPrice(mockBibliophile, m0, m1) + assert.Nil(t, output) + assert.Equal(t, ErrCrossingMarket, err) + assert.Equal(t, Order0, errorOrder) + }) + }) + t.Run("both orders in the same block", func(t *testing.T) { + blockPlaced0 := big.NewInt(69) + blockPlaced1 := big.NewInt(69) + t.Run("short order = IOC", func(t *testing.T) { + m0 := &Metadata{ + Price: hu.Mul1e6(big.NewInt(19)), + AmmIndex: big.NewInt(market), + BlockPlaced: blockPlaced0, + } + m1 := &Metadata{ + Price: hu.Mul1e6(big.NewInt(19)), + BlockPlaced: blockPlaced1, + OrderType: ob.IOC, + } + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(market).Return(upperbound, lowerbound).Times(1) + output, err, _ := determineFillPrice(mockBibliophile, m0, m1) + assert.Nil(t, err) + assert.Equal(t, FillPriceAndModes{m0.Price, Maker, Taker}, *output) + }) + t.Run("short order != IOC", func(t *testing.T) { + m0 := &Metadata{ + Price: hu.Mul1e6(big.NewInt(19)), + AmmIndex: big.NewInt(market), + BlockPlaced: blockPlaced0, + } + m1 := &Metadata{ + Price: hu.Mul1e6(big.NewInt(19)), + BlockPlaced: blockPlaced1, + } + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(market).Return(upperbound, lowerbound).Times(1) + output, err, _ := determineFillPrice(mockBibliophile, m0, m1) + assert.Nil(t, err) + assert.Equal(t, FillPriceAndModes{m1.Price, Taker, Maker}, *output) + }) + }) +} + +func TestDetermineLiquidationFillPrice(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockBibliophile := b.NewMockBibliophileClient(ctrl) + + liqUpperBound, liqLowerBound := hu.Mul1e6(big.NewInt(22)), hu.Mul1e6(big.NewInt(18)) + + upperbound := hu.Mul1e6(big.NewInt(30)) // $30 + lowerbound := hu.Mul1e6(big.NewInt(10)) // $10 + market := int64(7) + + t.Run("long position is being liquidated", func(t *testing.T) { + t.Run("order price < liqLowerBound", func(t *testing.T) { + m0 := &Metadata{ + Price: new(big.Int).Sub(liqLowerBound, big.NewInt(1)), + BaseAssetQuantity: big.NewInt(5), + AmmIndex: big.NewInt(market), + } + mockBibliophile.EXPECT().GetAcceptableBoundsForLiquidation(market).Return(liqUpperBound, liqLowerBound).Times(1) + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(market).Return(upperbound, lowerbound).Times(1) + output, err := determineLiquidationFillPrice(mockBibliophile, m0) + assert.Nil(t, output) + assert.Equal(t, ErrTooLow, err) + }) + t.Run("order price == liqLowerBound", func(t *testing.T) { + m0 := &Metadata{ + Price: liqLowerBound, + BaseAssetQuantity: big.NewInt(5), + AmmIndex: big.NewInt(market), + } + mockBibliophile.EXPECT().GetAcceptableBoundsForLiquidation(market).Return(liqUpperBound, liqLowerBound).Times(1) + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(market).Return(upperbound, lowerbound).Times(1) + output, err := determineLiquidationFillPrice(mockBibliophile, m0) + assert.Nil(t, err) + assert.Equal(t, liqLowerBound, output) + }) + + t.Run("liqLowerBound < order price < upper bound", func(t *testing.T) { + m0 := &Metadata{ + Price: new(big.Int).Add(liqLowerBound, big.NewInt(99)), + BaseAssetQuantity: big.NewInt(5), + AmmIndex: big.NewInt(market), + } + mockBibliophile.EXPECT().GetAcceptableBoundsForLiquidation(market).Return(liqUpperBound, liqLowerBound).Times(1) + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(market).Return(upperbound, lowerbound).Times(1) + output, err := determineLiquidationFillPrice(mockBibliophile, m0) + assert.Nil(t, err) + assert.Equal(t, m0.Price, output) + }) + + t.Run("order price == upper bound", func(t *testing.T) { + m0 := &Metadata{ + Price: upperbound, + BaseAssetQuantity: big.NewInt(5), + AmmIndex: big.NewInt(market), + } + mockBibliophile.EXPECT().GetAcceptableBoundsForLiquidation(market).Return(liqUpperBound, liqLowerBound).Times(1) + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(market).Return(upperbound, lowerbound).Times(1) + output, err := determineLiquidationFillPrice(mockBibliophile, m0) + assert.Nil(t, err) + assert.Equal(t, upperbound, output) + }) + + t.Run("order price > upper bound", func(t *testing.T) { + m0 := &Metadata{ + Price: new(big.Int).Add(upperbound, big.NewInt(99)), + BaseAssetQuantity: big.NewInt(5), + AmmIndex: big.NewInt(market), + } + mockBibliophile.EXPECT().GetAcceptableBoundsForLiquidation(market).Return(liqUpperBound, liqLowerBound).Times(1) + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(market).Return(upperbound, lowerbound).Times(1) + output, err := determineLiquidationFillPrice(mockBibliophile, m0) + assert.Nil(t, err) + assert.Equal(t, upperbound, output) + }) + }) +} + +type ValidateOrdersAndDetermineFillPriceTestCase struct { + Order0, Order1 ob.ContractOrder + FillAmount *big.Int + Err error + BadElement BadElement +} + +func testValidateOrdersAndDetermineFillPriceTestCase(t *testing.T, mockBibliophile *b.MockBibliophileClient, testCase ValidateOrdersAndDetermineFillPriceTestCase) ValidateOrdersAndDetermineFillPriceOutput { + order0Bytes, err := testCase.Order0.EncodeToABI() + if err != nil { + t.Fatal(err) + } + order1Bytes, err := testCase.Order1.EncodeToABI() + if err != nil { + t.Fatal(err) + } + resp := ValidateOrdersAndDetermineFillPrice(mockBibliophile, &ValidateOrdersAndDetermineFillPriceInput{ + Data: [2][]byte{order0Bytes, order1Bytes}, + FillAmount: testCase.FillAmount, + }) + + // verify results + if testCase.Err == nil && resp.Err != "" { + t.Fatalf("expected no error, got %v", resp.Err) + } + if testCase.Err != nil { + if resp.Err != testCase.Err.Error() { + t.Fatalf("expected %v, got %v", testCase.Err, testCase.Err) + } + + if resp.Element != uint8(testCase.BadElement) { + t.Fatalf("expected %v, got %v", testCase.BadElement, resp.Element) + } + } + return resp +} + +func TestValidateOrdersAndDetermineFillPrice(t *testing.T) { + // create a mock BibliophileClient + trader := common.HexToAddress("0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC") + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + t.Run("invalid fillAmount", func(t *testing.T) { + order0 := &hu.LimitOrder{ + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(10), + Price: big.NewInt(100), + Salt: big.NewInt(1), + ReduceOnly: false, + }, + PostOnly: false, + } + order1 := &hu.LimitOrder{ + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(-10), + Price: big.NewInt(100), + Salt: big.NewInt(2), + ReduceOnly: false, + }, + PostOnly: false, + } + fillAmount := big.NewInt(0) + + mockBibliophile := b.NewMockBibliophileClient(ctrl) + + testCase := ValidateOrdersAndDetermineFillPriceTestCase{ + Order0: order0, + Order1: order1, + FillAmount: fillAmount, + Err: ErrInvalidFillAmount, + BadElement: Generic, + } + + testValidateOrdersAndDetermineFillPriceTestCase(t, mockBibliophile, testCase) + }) + + t.Run("different amm", func(t *testing.T) { + order0 := &hu.LimitOrder{ + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(10), + Price: big.NewInt(100), + Salt: big.NewInt(1), + ReduceOnly: false, + }, + PostOnly: false, + } + order0Hash, _ := order0.Hash() + order1 := &hu.LimitOrder{ + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(1), + Trader: trader, + BaseAssetQuantity: big.NewInt(-10), + Price: big.NewInt(100), + Salt: big.NewInt(2), + ReduceOnly: false, + }, + PostOnly: false, + } + order1Hash, _ := order1.Hash() + fillAmount := big.NewInt(2) + + mockBibliophile := b.NewMockBibliophileClient(ctrl) + mockBibliophile.EXPECT().GetOrderFilledAmount(order0Hash).Return(big.NewInt(0)) + mockBibliophile.EXPECT().GetOrderStatus(order0Hash).Return(int64(1)) // placed + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(order0.AmmIndex.Int64()).Return(common.Address{101}) + mockBibliophile.EXPECT().GetBlockPlaced(order0Hash).Return(big.NewInt(10)) + + mockBibliophile.EXPECT().GetOrderFilledAmount(order1Hash).Return(big.NewInt(0)) + mockBibliophile.EXPECT().GetOrderStatus(order1Hash).Return(int64(1)) // placed + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(order1.AmmIndex.Int64()).Return(common.Address{102}) + mockBibliophile.EXPECT().GetBlockPlaced(order1Hash).Return(big.NewInt(12)) + testCase := ValidateOrdersAndDetermineFillPriceTestCase{ + Order0: order0, + Order1: order1, + FillAmount: fillAmount, + Err: ErrNotSameAMM, + BadElement: Generic, + } + + testValidateOrdersAndDetermineFillPriceTestCase(t, mockBibliophile, testCase) + }) + + t.Run("price mismatch", func(t *testing.T) { + order0 := &hu.LimitOrder{ + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(10), + Price: big.NewInt(99), + Salt: big.NewInt(1), + ReduceOnly: false, + }, + PostOnly: false, + } + order0Hash, _ := order0.Hash() + order1 := &hu.LimitOrder{ + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(-10), + Price: big.NewInt(100), + Salt: big.NewInt(2), + ReduceOnly: false, + }, + PostOnly: false, + } + order1Hash, _ := order1.Hash() + fillAmount := big.NewInt(2) + + mockBibliophile := b.NewMockBibliophileClient(ctrl) + mockBibliophile.EXPECT().GetOrderFilledAmount(order0Hash).Return(big.NewInt(0)) + mockBibliophile.EXPECT().GetOrderStatus(order0Hash).Return(int64(1)) // placed + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(order0.AmmIndex.Int64()).Return(common.Address{101}) + mockBibliophile.EXPECT().GetBlockPlaced(order0Hash).Return(big.NewInt(10)) + + mockBibliophile.EXPECT().GetOrderFilledAmount(order1Hash).Return(big.NewInt(0)) + mockBibliophile.EXPECT().GetOrderStatus(order1Hash).Return(int64(1)) // placed + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(order1.AmmIndex.Int64()).Return(common.Address{101}) + mockBibliophile.EXPECT().GetBlockPlaced(order1Hash).Return(big.NewInt(12)) + testCase := ValidateOrdersAndDetermineFillPriceTestCase{ + Order0: order0, + Order1: order1, + FillAmount: fillAmount, + Err: ErrNoMatch, + BadElement: Generic, + } + + testValidateOrdersAndDetermineFillPriceTestCase(t, mockBibliophile, testCase) + }) + + t.Run("fillAmount not multiple", func(t *testing.T) { + order0 := &hu.LimitOrder{ + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(10), + Price: big.NewInt(100), + Salt: big.NewInt(1), + ReduceOnly: false, + }, + PostOnly: false, + } + order0Hash, _ := order0.Hash() + order1 := &hu.LimitOrder{ + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(-10), + Price: big.NewInt(100), + Salt: big.NewInt(2), + ReduceOnly: false, + }, + PostOnly: false, + } + order1Hash, _ := order1.Hash() + fillAmount := big.NewInt(2) + + mockBibliophile := b.NewMockBibliophileClient(ctrl) + mockBibliophile.EXPECT().GetOrderFilledAmount(order0Hash).Return(big.NewInt(0)) + mockBibliophile.EXPECT().GetOrderStatus(order0Hash).Return(int64(1)) // placed + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(order0.AmmIndex.Int64()).Return(common.Address{101}) + mockBibliophile.EXPECT().GetBlockPlaced(order0Hash).Return(big.NewInt(10)) + + mockBibliophile.EXPECT().GetOrderFilledAmount(order1Hash).Return(big.NewInt(0)) + mockBibliophile.EXPECT().GetOrderStatus(order1Hash).Return(int64(1)) // placed + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(order1.AmmIndex.Int64()).Return(common.Address{101}) + mockBibliophile.EXPECT().GetBlockPlaced(order1Hash).Return(big.NewInt(12)) + + mockBibliophile.EXPECT().GetMinSizeRequirement(order1.AmmIndex.Int64()).Return(big.NewInt(5)) + + testCase := ValidateOrdersAndDetermineFillPriceTestCase{ + Order0: order0, + Order1: order1, + FillAmount: fillAmount, + Err: ErrNotMultiple, + BadElement: Generic, + } + + testValidateOrdersAndDetermineFillPriceTestCase(t, mockBibliophile, testCase) + }) + + t.Run("success", func(t *testing.T) { + order0 := &hu.LimitOrder{ + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(10), + Price: big.NewInt(100), + Salt: big.NewInt(1), + ReduceOnly: false, + }, + PostOnly: false, + } + order0Hash, _ := order0.Hash() + order1 := &hu.LimitOrder{ + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(-10), + Price: big.NewInt(100), + Salt: big.NewInt(2), + ReduceOnly: false, + }, + PostOnly: false, + } + order1Hash, _ := order1.Hash() + fillAmount := big.NewInt(2) + + mockBibliophile := b.NewMockBibliophileClient(ctrl) + mockBibliophile.EXPECT().GetOrderFilledAmount(order0Hash).Return(big.NewInt(0)) + mockBibliophile.EXPECT().GetOrderStatus(order0Hash).Return(int64(1)) // placed + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(order0.AmmIndex.Int64()).Return(common.Address{101}) + mockBibliophile.EXPECT().GetBlockPlaced(order0Hash).Return(big.NewInt(10)) + + mockBibliophile.EXPECT().GetOrderFilledAmount(order1Hash).Return(big.NewInt(0)) + mockBibliophile.EXPECT().GetOrderStatus(order1Hash).Return(int64(1)) // placed + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(order1.AmmIndex.Int64()).Return(common.Address{101}) + mockBibliophile.EXPECT().GetBlockPlaced(order1Hash).Return(big.NewInt(12)) + + mockBibliophile.EXPECT().GetMinSizeRequirement(order1.AmmIndex.Int64()).Return(big.NewInt(1)) + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(order1.AmmIndex.Int64()).Return(big.NewInt(110), big.NewInt(90)) + + testCase := ValidateOrdersAndDetermineFillPriceTestCase{ + Order0: order0, + Order1: order1, + FillAmount: fillAmount, + Err: nil, + BadElement: NoError, + } + + response := testValidateOrdersAndDetermineFillPriceTestCase(t, mockBibliophile, testCase) + assert.Equal(t, big.NewInt(100), response.Res.FillPrice) + assert.Equal(t, uint8(0), response.Res.OrderTypes[0]) + assert.Equal(t, uint8(0), response.Res.OrderTypes[1]) + + assert.Equal(t, uint8(NoError), response.Element) + assert.Equal(t, IClearingHouseInstruction{ + AmmIndex: big.NewInt(0), + Trader: trader, + OrderHash: order0Hash, + Mode: uint8(Maker), + }, response.Res.Instructions[0]) + assert.Equal(t, IClearingHouseInstruction{ + AmmIndex: big.NewInt(0), + Trader: trader, + OrderHash: order1Hash, + Mode: uint8(Taker), + }, response.Res.Instructions[1]) + }) + + t.Run("2 market orders can't be matched", func(t *testing.T) { + order0 := &hu.IOCOrder{ + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(10), + Price: big.NewInt(100), + Salt: big.NewInt(1), + ReduceOnly: false, + }, + OrderType: 1, + ExpireAt: big.NewInt(100), + } + order0Hash, _ := order0.Hash() + order1 := &hu.IOCOrder{ + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(-10), + Price: big.NewInt(100), + Salt: big.NewInt(2), + ReduceOnly: false, + }, + OrderType: 1, + ExpireAt: big.NewInt(100), + } + order1Hash, _ := order1.Hash() + fillAmount := big.NewInt(2) + + mockBibliophile := b.NewMockBibliophileClient(ctrl) + mockBibliophile.EXPECT().IOC_GetOrderFilledAmount(order0Hash).Return(big.NewInt(0)) + mockBibliophile.EXPECT().IOC_GetOrderStatus(order0Hash).Return(int64(1)) // placed + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(order0.AmmIndex.Int64()).Return(common.Address{101}) + mockBibliophile.EXPECT().IOC_GetBlockPlaced(order0Hash).Return(big.NewInt(10)) + + mockBibliophile.EXPECT().IOC_GetOrderFilledAmount(order1Hash).Return(big.NewInt(0)) + mockBibliophile.EXPECT().IOC_GetOrderStatus(order1Hash).Return(int64(1)) // placed + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(order1.AmmIndex.Int64()).Return(common.Address{101}) + mockBibliophile.EXPECT().IOC_GetBlockPlaced(order1Hash).Return(big.NewInt(12)) + mockBibliophile.EXPECT().GetTimeStamp().Times(2).Return(uint64(99)) // expiry is 100 + + mockBibliophile.EXPECT().GetMinSizeRequirement(order1.AmmIndex.Int64()).Return(big.NewInt(1)) + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(order1.AmmIndex.Int64()).Return(big.NewInt(110), big.NewInt(90)) + + testCase := ValidateOrdersAndDetermineFillPriceTestCase{ + Order0: order0, + Order1: order1, + FillAmount: fillAmount, + Err: ErrIOCOrderExpired, + BadElement: Order0, + } + testValidateOrdersAndDetermineFillPriceTestCase(t, mockBibliophile, testCase) + }) +} + +type ValidateLiquidationOrderAndDetermineFillPriceTestCase struct { + Order ob.ContractOrder + LiquidationAmount *big.Int + Err error + BadElement BadElement +} + +func testValidateLiquidationOrderAndDetermineFillPriceTestCase(t *testing.T, mockBibliophile *b.MockBibliophileClient, testCase ValidateLiquidationOrderAndDetermineFillPriceTestCase) ValidateLiquidationOrderAndDetermineFillPriceOutput { + orderBytes, err := testCase.Order.EncodeToABI() + if err != nil { + t.Fatal(err) + } + + resp := ValidateLiquidationOrderAndDetermineFillPrice(mockBibliophile, &ValidateLiquidationOrderAndDetermineFillPriceInput{ + Data: orderBytes, + LiquidationAmount: testCase.LiquidationAmount, + }) + + // verify results + if testCase.Err == nil && resp.Err != "" { + t.Fatalf("expected no error, got %v", resp.Err) + } + if testCase.Err != nil { + if resp.Err != testCase.Err.Error() { + t.Fatalf("expected %v, got %v", testCase.Err, testCase.Err) + } + + if resp.Element != uint8(testCase.BadElement) { + t.Fatalf("expected %v, got %v", testCase.BadElement, resp.Element) + } + } + return resp +} + +func TestValidateLiquidationOrderAndDetermineFillPrice(t *testing.T) { + trader := common.HexToAddress("0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC") + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + t.Run("invalid liquidationAmount", func(t *testing.T) { + order := &hu.LimitOrder{ + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(10), + Price: big.NewInt(100), + Salt: big.NewInt(1), + ReduceOnly: true, + }, + PostOnly: false, + } + + mockBibliophile := b.NewMockBibliophileClient(ctrl) + + testCase := ValidateLiquidationOrderAndDetermineFillPriceTestCase{ + Order: order, + LiquidationAmount: big.NewInt(0), + Err: ErrInvalidFillAmount, + BadElement: Generic, + } + + testValidateLiquidationOrderAndDetermineFillPriceTestCase(t, mockBibliophile, testCase) + }) + + t.Run("fillAmount not multiple", func(t *testing.T) { + order := &hu.LimitOrder{ + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(-10), + Price: big.NewInt(100), + Salt: big.NewInt(2), + ReduceOnly: true, + }, + PostOnly: false, + } + orderHash, _ := order.Hash() + + mockBibliophile := b.NewMockBibliophileClient(ctrl) + mockBibliophile.EXPECT().GetOrderFilledAmount(orderHash).Return(big.NewInt(0)) + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(1)) // placed + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(order.AmmIndex.Int64()).Return(common.Address{101}) + mockBibliophile.EXPECT().GetBlockPlaced(orderHash).Return(big.NewInt(10)) + mockBibliophile.EXPECT().GetSize(common.Address{101}, &trader).Return(big.NewInt(10)) + + mockBibliophile.EXPECT().GetMinSizeRequirement(order.AmmIndex.Int64()).Return(big.NewInt(5)) + + testCase := ValidateLiquidationOrderAndDetermineFillPriceTestCase{ + Order: order, + LiquidationAmount: big.NewInt(2), + Err: ErrNotMultiple, + BadElement: Generic, + } + + testValidateLiquidationOrderAndDetermineFillPriceTestCase(t, mockBibliophile, testCase) + }) + + t.Run("success", func(t *testing.T) { + order := &hu.LimitOrder{ + BaseOrder: hu.BaseOrder{ + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: big.NewInt(-10), + Price: big.NewInt(100), + Salt: big.NewInt(2), + ReduceOnly: true, + }, + PostOnly: false, + } + orderHash, _ := order.Hash() + + mockBibliophile := b.NewMockBibliophileClient(ctrl) + mockBibliophile.EXPECT().GetOrderFilledAmount(orderHash).Return(big.NewInt(0)) + mockBibliophile.EXPECT().GetOrderStatus(orderHash).Return(int64(1)) // placed + mockBibliophile.EXPECT().GetMarketAddressFromMarketID(order.AmmIndex.Int64()).Return(common.Address{101}) + mockBibliophile.EXPECT().GetBlockPlaced(orderHash).Return(big.NewInt(10)) + mockBibliophile.EXPECT().GetSize(common.Address{101}, &trader).Return(big.NewInt(10)) + mockBibliophile.EXPECT().GetMinSizeRequirement(order.AmmIndex.Int64()).Return(big.NewInt(1)) + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(order.AmmIndex.Int64()).Return(big.NewInt(110), big.NewInt(90)) + mockBibliophile.EXPECT().GetAcceptableBoundsForLiquidation(order.AmmIndex.Int64()).Return(big.NewInt(110), big.NewInt(90)) + + testCase := ValidateLiquidationOrderAndDetermineFillPriceTestCase{ + Order: order, + LiquidationAmount: big.NewInt(2), + Err: nil, + BadElement: NoError, + } + + response := testValidateLiquidationOrderAndDetermineFillPriceTestCase(t, mockBibliophile, testCase) + + assert.Equal(t, uint8(NoError), response.Element) + assert.Equal(t, IClearingHouseInstruction{ + AmmIndex: big.NewInt(0), + Trader: trader, + OrderHash: orderHash, + Mode: uint8(Maker), + }, response.Res.Instruction) + assert.Equal(t, big.NewInt(100), response.Res.FillPrice) + assert.Equal(t, uint8(0), response.Res.OrderType) + assert.Equal(t, big.NewInt(-2), response.Res.FillAmount) + }) +} + +func TestReducesPosition(t *testing.T) { + testCases := []struct { + positionSize *big.Int + baseAssetQuantity *big.Int + expectedResult bool + }{ + { + positionSize: big.NewInt(100), + baseAssetQuantity: big.NewInt(-50), + expectedResult: true, + }, + { + positionSize: big.NewInt(-100), + baseAssetQuantity: big.NewInt(50), + expectedResult: true, + }, + { + positionSize: big.NewInt(100), + baseAssetQuantity: big.NewInt(50), + expectedResult: false, + }, + { + positionSize: big.NewInt(-100), + baseAssetQuantity: big.NewInt(-50), + expectedResult: false, + }, + } + + for _, tc := range testCases { + result := reducesPosition(tc.positionSize, tc.baseAssetQuantity) + if result != tc.expectedResult { + t.Errorf("reducesPosition(%v, %v) = %v; expected %v", tc.positionSize, tc.baseAssetQuantity, result, tc.expectedResult) + } + } +} + +func TestGetRequiredMargin(t *testing.T) { + trader := common.HexToAddress("0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC") + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockBibliophile := b.NewMockBibliophileClient(ctrl) + mockBibliophile.EXPECT().GetUpperAndLowerBoundForMarket(gomock.Any()).Return(big.NewInt(100), big.NewInt(10)).AnyTimes() + mockBibliophile.EXPECT().GetMinAllowableMargin().Return(big.NewInt(1000)).AnyTimes() + mockBibliophile.EXPECT().GetTakerFee().Return(big.NewInt(5)).AnyTimes() + + // create a mock order + order := ILimitOrderBookOrder{ + AmmIndex: big.NewInt(0), + Trader: trader, + BaseAssetQuantity: hu.Mul(big.NewInt(10), hu.ONE_E_18), + Price: hu.Mul(big.NewInt(50), hu.ONE_E_6), + ReduceOnly: false, + Salt: big.NewInt(1), + PostOnly: false, + } + + // call the function + requiredMargin := getRequiredMargin(mockBibliophile, order) + + fmt.Println("#####", requiredMargin) + + // assert that the result is correct + expectedMargin := big.NewInt(502500) // (10 * 50 * 1e6) * (1 + 0.005) + assert.Equal(t, expectedMargin, requiredMargin) +} diff --git a/precompile/contracts/jurorv2/module.go b/precompile/contracts/jurorv2/module.go new file mode 100644 index 0000000000..59fce051d3 --- /dev/null +++ b/precompile/contracts/jurorv2/module.go @@ -0,0 +1,63 @@ +// Code generated +// This file is a generated precompile contract config with stubbed abstract functions. +// The file is generated by a template. Please inspect every code and comment in this file before use. + +package jurorv2 + +import ( + "fmt" + + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ava-labs/subnet-evm/precompile/modules" + "github.com/ava-labs/subnet-evm/precompile/precompileconfig" + + "github.com/ethereum/go-ethereum/common" +) + +var _ contract.Configurator = &configurator{} + +// ConfigKey is the key used in json config files to specify this precompile precompileconfig. +// must be unique across all precompiles. +const ConfigKey = "jurorV2Config" + +// ContractAddress is the defined address of the precompile contract. +// This should be unique across all precompile contracts. +// See precompile/registry/registry.go for registered precompile contracts and more information. +var ContractAddress = common.HexToAddress("0x03000000000000000000000000000000000000a2") // SET A SUITABLE HEX ADDRESS HERE + +// Module is the precompile module. It is used to register the precompile contract. +var Module = modules.Module{ + ConfigKey: ConfigKey, + Address: ContractAddress, + Contract: JurorPrecompile, + Configurator: &configurator{}, +} + +type configurator struct{} + +func init() { + // Register the precompile module. + // Each precompile contract registers itself through [RegisterModule] function. + if err := modules.RegisterModule(Module); err != nil { + panic(err) + } +} + +// MakeConfig returns a new precompile config instance. +// This is required for Marshal/Unmarshal the precompile config. +func (*configurator) MakeConfig() precompileconfig.Config { + return new(Config) +} + +// Configure configures [state] with the given [cfg] precompileconfig. +// This function is called by the EVM once per precompile contract activation. +// You can use this function to set up your precompile contract's initial state, +// by using the [cfg] config and [state] stateDB. +func (*configurator) Configure(chainConfig precompileconfig.ChainConfig, cfg precompileconfig.Config, state contract.StateDB, _ contract.ConfigurationBlockContext) error { + config, ok := cfg.(*Config) + if !ok { + return fmt.Errorf("incorrect config %T: %v", config, config) + } + // CUSTOM CODE STARTS HERE + return nil +} diff --git a/precompile/contracts/jurorv2/notional_position.go b/precompile/contracts/jurorv2/notional_position.go new file mode 100644 index 0000000000..8cd36e7bc1 --- /dev/null +++ b/precompile/contracts/jurorv2/notional_position.go @@ -0,0 +1,14 @@ +package jurorv2 + +import ( + hu "github.com/ava-labs/subnet-evm/plugin/evm/orderbook/hubbleutils" + b "github.com/ava-labs/subnet-evm/precompile/contracts/bibliophile" +) + +func GetNotionalPositionAndMargin(bibliophile b.BibliophileClient, input *GetNotionalPositionAndMarginInput) GetNotionalPositionAndMarginOutput { + notionalPosition, margin := bibliophile.GetNotionalPositionAndMargin(input.Trader, input.IncludeFundingPayments, input.Mode, hu.V2) + return GetNotionalPositionAndMarginOutput{ + NotionalPosition: notionalPosition, + Margin: margin, + } +} diff --git a/precompile/contracts/ticks/README.md b/precompile/contracts/ticks/README.md new file mode 100644 index 0000000000..d81e622b2b --- /dev/null +++ b/precompile/contracts/ticks/README.md @@ -0,0 +1,23 @@ +There are some must-be-done changes waiting in the generated file. Each area requiring you to add your code is marked with CUSTOM CODE to make them easy to find and modify. +Additionally there are other files you need to edit to activate your precompile. +These areas are highlighted with comments "ADD YOUR PRECOMPILE HERE". +For testing take a look at other precompile tests in contract_test.go and config_test.go in other precompile folders. +See the tutorial in for more information about precompile development. + +General guidelines for precompile development: +1- Set a suitable config key in generated module.go. E.g: "yourPrecompileConfig" +2- Read the comment and set a suitable contract address in generated module.go. E.g: +ContractAddress = common.HexToAddress("ASUITABLEHEXADDRESS") +3- It is recommended to only modify code in the highlighted areas marked with "CUSTOM CODE STARTS HERE". Typically, custom codes are required in only those areas. +Modifying code outside of these areas should be done with caution and with a deep understanding of how these changes may impact the EVM. +4- Set gas costs in generated contract.go +5- Force import your precompile package in precompile/registry/registry.go +6- Add your config unit tests under generated package config_test.go +7- Add your contract unit tests under generated package contract_test.go +8- Additionally you can add a full-fledged VM test for your precompile under plugin/vm/vm_test.go. See existing precompile tests for examples. +9- Add your solidity interface and test contract to contracts/contracts +10- Write solidity contract tests for your precompile in contracts/contracts/test +11- Write TypeScript DS-Test counterparts for your solidity tests in contracts/test +12- Create your genesis with your precompile enabled in tests/precompile/genesis/ +13- Create e2e test for your solidity test in tests/precompile/solidity/suites.go +14- Run your e2e precompile Solidity tests with './scripts/run_ginkgo.sh` diff --git a/precompile/contracts/ticks/config.go b/precompile/contracts/ticks/config.go new file mode 100644 index 0000000000..ad707c0ec9 --- /dev/null +++ b/precompile/contracts/ticks/config.go @@ -0,0 +1,64 @@ +// Code generated +// This file is a generated precompile contract config with stubbed abstract functions. +// The file is generated by a template. Please inspect every code and comment in this file before use. + +package ticks + +import ( + "github.com/ava-labs/subnet-evm/precompile/precompileconfig" +) + +var _ precompileconfig.Config = &Config{} + +// Config implements the precompileconfig.Config interface and +// adds specific configuration for Ticks. +type Config struct { + precompileconfig.Upgrade + // CUSTOM CODE STARTS HERE + // Add your own custom fields for Config here +} + +// NewConfig returns a config for a network upgrade at [blockTimestamp] that enables +// Ticks. +func NewConfig(blockTimestamp *uint64) *Config { + return &Config{ + Upgrade: precompileconfig.Upgrade{BlockTimestamp: blockTimestamp}, + } +} + +// NewDisableConfig returns config for a network upgrade at [blockTimestamp] +// that disables Ticks. +func NewDisableConfig(blockTimestamp *uint64) *Config { + return &Config{ + Upgrade: precompileconfig.Upgrade{ + BlockTimestamp: blockTimestamp, + Disable: true, + }, + } +} + +// Key returns the key for the Ticks precompileconfig. +// This should be the same key as used in the precompile module. +func (*Config) Key() string { return ConfigKey } + +// Verify tries to verify Config and returns an error accordingly. +func (c *Config) Verify(precompileconfig.ChainConfig) error { + // CUSTOM CODE STARTS HERE + // Add your own custom verify code for Config here + // and return an error accordingly + return nil +} + +// Equal returns true if [s] is a [*Config] and it has been configured identical to [c]. +func (c *Config) Equal(s precompileconfig.Config) bool { + // typecast before comparison + other, ok := (s).(*Config) + if !ok { + return false + } + // CUSTOM CODE STARTS HERE + // modify this boolean accordingly with your custom Config, to check if [other] and the current [c] are equal + // if Config contains only Upgrade you can skip modifying it. + equals := c.Upgrade.Equal(&other.Upgrade) + return equals +} diff --git a/precompile/contracts/ticks/config_test.go b/precompile/contracts/ticks/config_test.go new file mode 100644 index 0000000000..26cfec2a5a --- /dev/null +++ b/precompile/contracts/ticks/config_test.go @@ -0,0 +1,62 @@ +// Code generated +// This file is a generated precompile config test with the skeleton of test functions. +// The file is generated by a template. Please inspect every code and comment in this file before use. + +package ticks + +import ( + "testing" + + "github.com/ava-labs/subnet-evm/precompile/precompileconfig" + "github.com/ava-labs/subnet-evm/precompile/testutils" + "github.com/ava-labs/subnet-evm/utils" + "go.uber.org/mock/gomock" +) + +// TestVerify tests the verification of Config. +func TestVerify(t *testing.T) { + tests := map[string]testutils.ConfigVerifyTest{ + "valid config": { + Config: NewConfig(utils.NewUint64(3)), + ExpectedError: "", + }, + // CUSTOM CODE STARTS HERE + // Add your own Verify tests here, e.g.: + // "your custom test name": { + // Config: NewConfig(utils.NewUint64(3),), + // ExpectedError: ErrYourCustomError.Error(), + // }, + } + // Run verify tests. + testutils.RunVerifyTests(t, tests) +} + +// TestEqual tests the equality of Config with other precompile configs. +func TestEqual(t *testing.T) { + tests := map[string]testutils.ConfigEqualTest{ + "non-nil config and nil other": { + Config: NewConfig(utils.NewUint64(3)), + Other: nil, + Expected: false, + }, + "different type": { + Config: NewConfig(utils.NewUint64(3)), + Other: precompileconfig.NewMockConfig(gomock.NewController(t)), + Expected: false, + }, + "different timestamp": { + Config: NewConfig(utils.NewUint64(3)), + Other: NewConfig(utils.NewUint64(4)), + Expected: false, + }, + "same config": { + Config: NewConfig(utils.NewUint64(3)), + Other: NewConfig(utils.NewUint64(3)), + Expected: true, + }, + // CUSTOM CODE STARTS HERE + // Add your own Equal tests here + } + // Run equal tests. + testutils.RunEqualTests(t, tests) +} diff --git a/precompile/contracts/ticks/contract.abi b/precompile/contracts/ticks/contract.abi new file mode 100644 index 0000000000..3c5f7e9f50 --- /dev/null +++ b/precompile/contracts/ticks/contract.abi @@ -0,0 +1 @@ +[{"inputs":[{"internalType":"address","name":"amm","type":"address"},{"internalType":"int256","name":"quoteQuantity","type":"int256"}],"name":"getBaseQuote","outputs":[{"internalType":"uint256","name":"rate","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"amm","type":"address"},{"internalType":"bool","name":"isBid","type":"bool"},{"internalType":"uint256","name":"tick","type":"uint256"}],"name":"getPrevTick","outputs":[{"internalType":"uint256","name":"prevTick","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"amm","type":"address"},{"internalType":"int256","name":"baseAssetQuantity","type":"int256"}],"name":"getQuote","outputs":[{"internalType":"uint256","name":"rate","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"amm","type":"address"}],"name":"sampleImpactAsk","outputs":[{"internalType":"uint256","name":"impactAsk","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"amm","type":"address"}],"name":"sampleImpactBid","outputs":[{"internalType":"uint256","name":"impactBid","type":"uint256"}],"stateMutability":"view","type":"function"}] \ No newline at end of file diff --git a/precompile/contracts/ticks/contract.go b/precompile/contracts/ticks/contract.go new file mode 100644 index 0000000000..964eadcd69 --- /dev/null +++ b/precompile/contracts/ticks/contract.go @@ -0,0 +1,326 @@ +// Code generated +// This file is a generated precompile contract config with stubbed abstract functions. +// The file is generated by a template. Please inspect every code and comment in this file before use. + +package ticks + +import ( + "errors" + "fmt" + "math/big" + + "github.com/ava-labs/subnet-evm/accounts/abi" + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ava-labs/subnet-evm/precompile/contracts/bibliophile" + + _ "embed" + + "github.com/ethereum/go-ethereum/common" +) + +const ( + // Gas costs for each function. These are set to 1 by default. + // You should set a gas cost for each function in your contract. + // Generally, you should not set gas costs very low as this may cause your network to be vulnerable to DoS attacks. + // There are some predefined gas costs in contract/utils.go that you can use. + GetBaseQuoteGasCost uint64 = 69 + GetPrevTickGasCost uint64 = 69 + GetQuoteGasCost uint64 = 69 + SampleImpactAskGasCost uint64 = 69 + SampleImpactBidGasCost uint64 = 69 +) + +// CUSTOM CODE STARTS HERE +// Reference imports to suppress errors from unused imports. This code and any unnecessary imports can be removed. +var ( + _ = abi.JSON + _ = errors.New + _ = big.NewInt +) + +// Singleton StatefulPrecompiledContract and signatures. +var ( + + // TicksRawABI contains the raw ABI of Ticks contract. + //go:embed contract.abi + TicksRawABI string + + TicksABI = contract.ParseABI(TicksRawABI) + + TicksPrecompile = createTicksPrecompile() +) + +type GetBaseQuoteInput struct { + Amm common.Address + QuoteQuantity *big.Int +} + +type GetPrevTickInput struct { + Amm common.Address + IsBid bool + Tick *big.Int +} + +type GetQuoteInput struct { + Amm common.Address + BaseAssetQuantity *big.Int +} + +// UnpackGetBaseQuoteInput attempts to unpack [input] as GetBaseQuoteInput +// assumes that [input] does not include selector (omits first 4 func signature bytes) +func UnpackGetBaseQuoteInput(input []byte) (GetBaseQuoteInput, error) { + inputStruct := GetBaseQuoteInput{} + err := TicksABI.UnpackInputIntoInterface(&inputStruct, "getBaseQuote", input, true) + + return inputStruct, err +} + +// PackGetBaseQuote packs [inputStruct] of type GetBaseQuoteInput into the appropriate arguments for getBaseQuote. +func PackGetBaseQuote(inputStruct GetBaseQuoteInput) ([]byte, error) { + return TicksABI.Pack("getBaseQuote", inputStruct.Amm, inputStruct.QuoteQuantity) +} + +// PackGetBaseQuoteOutput attempts to pack given rate of type *big.Int +// to conform the ABI outputs. +func PackGetBaseQuoteOutput(rate *big.Int) ([]byte, error) { + return TicksABI.PackOutput("getBaseQuote", rate) +} + +func getBaseQuote(accessibleState contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + if remainingGas, err = contract.DeductGas(suppliedGas, GetBaseQuoteGasCost); err != nil { + return nil, 0, err + } + // attempts to unpack [input] into the arguments to the GetBaseQuoteInput. + // Assumes that [input] does not include selector + // You can use unpacked [inputStruct] variable in your code + inputStruct, err := UnpackGetBaseQuoteInput(input) + if err != nil { + return nil, remainingGas, err + } + + // CUSTOM CODE STARTS HERE + bibliophile := bibliophile.NewBibliophileClient(accessibleState) + output := GetBaseQuote(bibliophile, inputStruct.Amm, inputStruct.QuoteQuantity) + packedOutput, err := PackGetBaseQuoteOutput(output) + if err != nil { + return nil, remainingGas, err + } + + // Return the packed output and the remaining gas + return packedOutput, remainingGas, nil +} + +// UnpackGetPrevTickInput attempts to unpack [input] as GetPrevTickInput +// assumes that [input] does not include selector (omits first 4 func signature bytes) +func UnpackGetPrevTickInput(input []byte) (GetPrevTickInput, error) { + inputStruct := GetPrevTickInput{} + err := TicksABI.UnpackInputIntoInterface(&inputStruct, "getPrevTick", input, true) + + return inputStruct, err +} + +// PackGetPrevTick packs [inputStruct] of type GetPrevTickInput into the appropriate arguments for getPrevTick. +func PackGetPrevTick(inputStruct GetPrevTickInput) ([]byte, error) { + return TicksABI.Pack("getPrevTick", inputStruct.Amm, inputStruct.IsBid, inputStruct.Tick) +} + +// PackGetPrevTickOutput attempts to pack given prevTick of type *big.Int +// to conform the ABI outputs. +func PackGetPrevTickOutput(prevTick *big.Int) ([]byte, error) { + return TicksABI.PackOutput("getPrevTick", prevTick) +} + +func getPrevTick(accessibleState contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + if remainingGas, err = contract.DeductGas(suppliedGas, GetPrevTickGasCost); err != nil { + return nil, 0, err + } + // attempts to unpack [input] into the arguments to the GetPrevTickInput. + // Assumes that [input] does not include selector + // You can use unpacked [inputStruct] variable in your code + inputStruct, err := UnpackGetPrevTickInput(input) + if err != nil { + return nil, remainingGas, err + } + + // CUSTOM CODE STARTS HERE + bibliophile := bibliophile.NewBibliophileClient(accessibleState) + output, err := GetPrevTick(bibliophile, inputStruct) + if err != nil { + return nil, remainingGas, err + } + packedOutput, err := PackGetPrevTickOutput(output) + if err != nil { + return nil, remainingGas, err + } + + // Return the packed output and the remaining gas + return packedOutput, remainingGas, nil +} + +// UnpackGetQuoteInput attempts to unpack [input] as GetQuoteInput +// assumes that [input] does not include selector (omits first 4 func signature bytes) +func UnpackGetQuoteInput(input []byte) (GetQuoteInput, error) { + inputStruct := GetQuoteInput{} + err := TicksABI.UnpackInputIntoInterface(&inputStruct, "getQuote", input, true) + + return inputStruct, err +} + +// PackGetQuote packs [inputStruct] of type GetQuoteInput into the appropriate arguments for getQuote. +func PackGetQuote(inputStruct GetQuoteInput) ([]byte, error) { + return TicksABI.Pack("getQuote", inputStruct.Amm, inputStruct.BaseAssetQuantity) +} + +// PackGetQuoteOutput attempts to pack given rate of type *big.Int +// to conform the ABI outputs. +func PackGetQuoteOutput(rate *big.Int) ([]byte, error) { + return TicksABI.PackOutput("getQuote", rate) +} + +func getQuote(accessibleState contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + if remainingGas, err = contract.DeductGas(suppliedGas, GetQuoteGasCost); err != nil { + return nil, 0, err + } + // attempts to unpack [input] into the arguments to the GetQuoteInput. + // Assumes that [input] does not include selector + // You can use unpacked [inputStruct] variable in your code + inputStruct, err := UnpackGetQuoteInput(input) + if err != nil { + return nil, remainingGas, err + } + + // CUSTOM CODE STARTS HERE + bibliophile := bibliophile.NewBibliophileClient(accessibleState) + output := GetQuote(bibliophile, inputStruct.Amm, inputStruct.BaseAssetQuantity) + packedOutput, err := PackGetQuoteOutput(output) + if err != nil { + return nil, remainingGas, err + } + + // Return the packed output and the remaining gas + return packedOutput, remainingGas, nil +} + +// UnpackSampleImpactAskInput attempts to unpack [input] into the common.Address type argument +// assumes that [input] does not include selector (omits first 4 func signature bytes) +func UnpackSampleImpactAskInput(input []byte) (common.Address, error) { + res, err := TicksABI.UnpackInput("sampleImpactAsk", input, true) + if err != nil { + return *new(common.Address), err + } + unpacked := *abi.ConvertType(res[0], new(common.Address)).(*common.Address) + return unpacked, nil +} + +// PackSampleImpactAsk packs [amm] of type common.Address into the appropriate arguments for sampleImpactAsk. +// the packed bytes include selector (first 4 func signature bytes). +// This function is mostly used for tests. +func PackSampleImpactAsk(amm common.Address) ([]byte, error) { + return TicksABI.Pack("sampleImpactAsk", amm) +} + +// PackSampleImpactAskOutput attempts to pack given impactAsk of type *big.Int +// to conform the ABI outputs. +func PackSampleImpactAskOutput(impactAsk *big.Int) ([]byte, error) { + return TicksABI.PackOutput("sampleImpactAsk", impactAsk) +} + +func sampleImpactAsk(accessibleState contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + if remainingGas, err = contract.DeductGas(suppliedGas, SampleImpactAskGasCost); err != nil { + return nil, 0, err + } + // attempts to unpack [input] into the arguments to the SampleImpactAskInput. + // Assumes that [input] does not include selector + // You can use unpacked [inputStruct] variable in your code + inputStruct, err := UnpackSampleImpactAskInput(input) + if err != nil { + return nil, remainingGas, err + } + + // CUSTOM CODE STARTS HERE + bibliophile := bibliophile.NewBibliophileClient(accessibleState) + output := SampleImpactAsk(bibliophile, inputStruct) + packedOutput, err := PackSampleImpactAskOutput(output) + if err != nil { + return nil, remainingGas, err + } + + // Return the packed output and the remaining gas + return packedOutput, remainingGas, nil +} + +// UnpackSampleImpactBidInput attempts to unpack [input] into the common.Address type argument +// assumes that [input] does not include selector (omits first 4 func signature bytes) +func UnpackSampleImpactBidInput(input []byte) (common.Address, error) { + res, err := TicksABI.UnpackInput("sampleImpactBid", input, true) + if err != nil { + return *new(common.Address), err + } + unpacked := *abi.ConvertType(res[0], new(common.Address)).(*common.Address) + return unpacked, nil +} + +// PackSampleImpactBid packs [amm] of type common.Address into the appropriate arguments for sampleImpactBid. +// the packed bytes include selector (first 4 func signature bytes). +// This function is mostly used for tests. +func PackSampleImpactBid(amm common.Address) ([]byte, error) { + return TicksABI.Pack("sampleImpactBid", amm) +} + +// PackSampleImpactBidOutput attempts to pack given impactBid of type *big.Int +// to conform the ABI outputs. +func PackSampleImpactBidOutput(impactBid *big.Int) ([]byte, error) { + return TicksABI.PackOutput("sampleImpactBid", impactBid) +} + +func sampleImpactBid(accessibleState contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + if remainingGas, err = contract.DeductGas(suppliedGas, SampleImpactBidGasCost); err != nil { + return nil, 0, err + } + // attempts to unpack [input] into the arguments to the SampleImpactBidInput. + // Assumes that [input] does not include selector + // You can use unpacked [inputStruct] variable in your code + inputStruct, err := UnpackSampleImpactBidInput(input) + if err != nil { + return nil, remainingGas, err + } + + // CUSTOM CODE STARTS HERE + bibliophile := bibliophile.NewBibliophileClient(accessibleState) + output := SampleImpactBid(bibliophile, inputStruct) + packedOutput, err := PackSampleImpactBidOutput(output) + if err != nil { + return nil, remainingGas, err + } + + // Return the packed output and the remaining gas + return packedOutput, remainingGas, nil +} + +// createTicksPrecompile returns a StatefulPrecompiledContract with getters and setters for the precompile. + +func createTicksPrecompile() contract.StatefulPrecompiledContract { + var functions []*contract.StatefulPrecompileFunction + + abiFunctionMap := map[string]contract.RunStatefulPrecompileFunc{ + "getBaseQuote": getBaseQuote, + "getPrevTick": getPrevTick, + "getQuote": getQuote, + "sampleImpactAsk": sampleImpactAsk, + "sampleImpactBid": sampleImpactBid, + } + + for name, function := range abiFunctionMap { + method, ok := TicksABI.Methods[name] + if !ok { + panic(fmt.Errorf("given method (%s) does not exist in the ABI", name)) + } + functions = append(functions, contract.NewStatefulPrecompileFunction(method.ID, function)) + } + // Construct the contract with no fallback function. + statefulContract, err := contract.NewStatefulPrecompileContract(nil, functions) + if err != nil { + panic(err) + } + return statefulContract +} diff --git a/precompile/contracts/ticks/contract_test.go b/precompile/contracts/ticks/contract_test.go new file mode 100644 index 0000000000..7adf103994 --- /dev/null +++ b/precompile/contracts/ticks/contract_test.go @@ -0,0 +1,121 @@ +// Code generated +// This file is a generated precompile contract test with the skeleton of test functions. +// The file is generated by a template. Please inspect every code and comment in this file before use. + +package ticks + +import ( + "math/big" + "testing" + + "github.com/ava-labs/subnet-evm/core/state" + "github.com/ava-labs/subnet-evm/precompile/testutils" + "github.com/ava-labs/subnet-evm/vmerrs" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +// These tests are run against the precompile contract directly with +// the given input and expected output. They're just a guide to +// help you write your own tests. These tests are for general cases like +// allowlist, readOnly behaviour, and gas cost. You should write your own +// tests for specific cases. +var ( + tests = map[string]testutils.PrecompileTest{ + "insufficient gas for getBaseQuote should fail": { + Caller: common.Address{1}, + InputFn: func(t testing.TB) []byte { + // CUSTOM CODE STARTS HERE + // populate test input here + testInput := GetBaseQuoteInput{ + QuoteQuantity: big.NewInt(0), + } + input, err := PackGetBaseQuote(testInput) + require.NoError(t, err) + return input + }, + SuppliedGas: GetBaseQuoteGasCost - 1, + ReadOnly: false, + ExpectedErr: vmerrs.ErrOutOfGas.Error(), + }, + "insufficient gas for getPrevTick should fail": { + Caller: common.Address{1}, + InputFn: func(t testing.TB) []byte { + // CUSTOM CODE STARTS HERE + // populate test input here + testInput := GetPrevTickInput{ + Tick: big.NewInt(0), + } + input, err := PackGetPrevTick(testInput) + require.NoError(t, err) + return input + }, + SuppliedGas: GetPrevTickGasCost - 1, + ReadOnly: false, + ExpectedErr: vmerrs.ErrOutOfGas.Error(), + }, + "insufficient gas for getQuote should fail": { + Caller: common.Address{1}, + InputFn: func(t testing.TB) []byte { + // CUSTOM CODE STARTS HERE + // populate test input here + testInput := GetQuoteInput{ + BaseAssetQuantity: big.NewInt(0), + } + input, err := PackGetQuote(testInput) + require.NoError(t, err) + return input + }, + SuppliedGas: GetQuoteGasCost - 1, + ReadOnly: false, + ExpectedErr: vmerrs.ErrOutOfGas.Error(), + }, + "insufficient gas for sampleImpactAsk should fail": { + Caller: common.Address{1}, + InputFn: func(t testing.TB) []byte { + // CUSTOM CODE STARTS HERE + // set test input to a value here + var testInput common.Address + input, err := PackSampleImpactAsk(testInput) + require.NoError(t, err) + return input + }, + SuppliedGas: SampleImpactAskGasCost - 1, + ReadOnly: false, + ExpectedErr: vmerrs.ErrOutOfGas.Error(), + }, + "insufficient gas for sampleImpactBid should fail": { + Caller: common.Address{1}, + InputFn: func(t testing.TB) []byte { + // CUSTOM CODE STARTS HERE + // set test input to a value here + var testInput common.Address + input, err := PackSampleImpactBid(testInput) + require.NoError(t, err) + return input + }, + SuppliedGas: SampleImpactBidGasCost - 1, + ReadOnly: false, + ExpectedErr: vmerrs.ErrOutOfGas.Error(), + }, + } +) + +// TestTicksRun tests the Run function of the precompile contract. +func TestTicksRun(t *testing.T) { + // Run tests. + for name, test := range tests { + t.Run(name, func(t *testing.T) { + test.Run(t, Module, state.NewTestStateDB(t)) + }) + } +} + +func BenchmarkTicks(b *testing.B) { + // Benchmark tests. + for name, test := range tests { + b.Run(name, func(b *testing.B) { + test.Bench(b, Module, state.NewTestStateDB(b)) + }) + } +} diff --git a/precompile/contracts/ticks/logic.go b/precompile/contracts/ticks/logic.go new file mode 100644 index 0000000000..86a3fef8d8 --- /dev/null +++ b/precompile/contracts/ticks/logic.go @@ -0,0 +1,173 @@ +package ticks + +import ( + "errors" + "fmt" + "math/big" + + hu "github.com/ava-labs/subnet-evm/plugin/evm/orderbook/hubbleutils" + b "github.com/ava-labs/subnet-evm/precompile/contracts/bibliophile" + "github.com/ethereum/go-ethereum/common" +) + +func GetPrevTick(bibliophile b.BibliophileClient, input GetPrevTickInput) (*big.Int, error) { + if input.Tick.Sign() == 0 { + return nil, errors.New("tick price cannot be zero") + } + if input.IsBid { + currentTick := bibliophile.GetBidsHead(input.Amm) + if input.Tick.Cmp(currentTick) >= 0 { + return nil, fmt.Errorf("tick %d is greater than or equal to bidsHead %d", input.Tick, currentTick) + } + for { + nextTick := bibliophile.GetNextBidPrice(input.Amm, currentTick) + if nextTick.Cmp(input.Tick) <= 0 { + return currentTick, nil + } + currentTick = nextTick + } + } + currentTick := bibliophile.GetAsksHead(input.Amm) + if currentTick.Sign() == 0 { + return nil, errors.New("asksHead is zero") + } + if input.Tick.Cmp(currentTick) <= 0 { + return nil, fmt.Errorf("tick %d is less than or equal to asksHead %d", input.Tick, currentTick) + } + for { + nextTick := bibliophile.GetNextAskPrice(input.Amm, currentTick) + if nextTick.Cmp(input.Tick) >= 0 || nextTick.Sign() == 0 { + return currentTick, nil + } + currentTick = nextTick + } +} + +func SampleImpactBid(bibliophile b.BibliophileClient, ammAddress common.Address) *big.Int { + impactMarginNotional := bibliophile.GetImpactMarginNotional(ammAddress) + if impactMarginNotional.Sign() == 0 { + return big.NewInt(0) + } + return _sampleImpactBid(bibliophile, ammAddress, impactMarginNotional) +} + +func _sampleImpactBid(bibliophile b.BibliophileClient, ammAddress common.Address, _impactMarginNotional *big.Int) *big.Int { + if _impactMarginNotional.Sign() == 0 { + return big.NewInt(0) + } + impactMarginNotional := new(big.Int).Mul(_impactMarginNotional, big.NewInt(1e12)) + accNotional := big.NewInt(0) // 18 decimals + accBaseQ := big.NewInt(0) // 18 decimals + tick := bibliophile.GetBidsHead(ammAddress) + for tick.Sign() != 0 { + amount := bibliophile.GetBidSize(ammAddress, tick) + accumulator := new(big.Int).Add(accNotional, hu.Div1e6(big.NewInt(0).Mul(amount, tick))) + if accumulator.Cmp(impactMarginNotional) >= 0 { + break + } + accNotional = accumulator + accBaseQ.Add(accBaseQ, amount) + tick = bibliophile.GetNextBidPrice(ammAddress, tick) + } + if tick.Sign() == 0 { + return big.NewInt(0) + } + baseQAtTick := new(big.Int).Div(hu.Mul1e6(new(big.Int).Sub(impactMarginNotional, accNotional)), tick) + return new(big.Int).Div(hu.Mul1e6(impactMarginNotional), new(big.Int).Add(baseQAtTick, accBaseQ)) // return value is in 6 decimals +} + +func SampleImpactAsk(bibliophile b.BibliophileClient, ammAddress common.Address) *big.Int { + impactMarginNotional := bibliophile.GetImpactMarginNotional(ammAddress) + if impactMarginNotional.Sign() == 0 { + return big.NewInt(0) + } + return _sampleImpactAsk(bibliophile, ammAddress, impactMarginNotional) +} + +func _sampleImpactAsk(bibliophile b.BibliophileClient, ammAddress common.Address, _impactMarginNotional *big.Int) *big.Int { + if _impactMarginNotional.Sign() == 0 { + return big.NewInt(0) + } + impactMarginNotional := new(big.Int).Mul(_impactMarginNotional, big.NewInt(1e12)) + tick := bibliophile.GetAsksHead(ammAddress) + accNotional := big.NewInt(0) // 18 decimals + accBaseQ := big.NewInt(0) // 18 decimals + for tick.Sign() != 0 { + amount := bibliophile.GetAskSize(ammAddress, tick) + accumulator := new(big.Int).Add(accNotional, hu.Div1e6(big.NewInt(0).Mul(amount, tick))) + if accumulator.Cmp(impactMarginNotional) >= 0 { + break + } + accNotional = accumulator + accBaseQ.Add(accBaseQ, amount) + tick = bibliophile.GetNextAskPrice(ammAddress, tick) + } + if tick.Sign() == 0 { + return big.NewInt(0) + } + baseQAtTick := new(big.Int).Div(hu.Mul1e6(new(big.Int).Sub(impactMarginNotional, accNotional)), tick) + return new(big.Int).Div(hu.Mul1e6(impactMarginNotional), new(big.Int).Add(baseQAtTick, accBaseQ)) // return value is in 6 decimals +} + +func GetBaseQuote(bibliophile b.BibliophileClient, ammAddress common.Address, quoteAssetQuantity *big.Int) *big.Int { + if quoteAssetQuantity.Sign() > 0 { // get the qoute to long quoteQuantity dollars + return _sampleImpactAsk(bibliophile, ammAddress, quoteAssetQuantity) + } + // get the qoute to short quoteQuantity dollars + return _sampleImpactBid(bibliophile, ammAddress, new(big.Int).Neg(quoteAssetQuantity)) +} + +func GetQuote(bibliophile b.BibliophileClient, ammAddress common.Address, baseAssetQuantity *big.Int) *big.Int { + if baseAssetQuantity.Sign() > 0 { + return _sampleAsk(bibliophile, ammAddress, baseAssetQuantity) + } + return _sampleBid(bibliophile, ammAddress, new(big.Int).Neg(baseAssetQuantity)) +} + +func _sampleAsk(bibliophile b.BibliophileClient, ammAddress common.Address, baseAssetQuantity *big.Int) *big.Int { + if baseAssetQuantity.Sign() <= 0 { + return big.NewInt(0) + } + tick := bibliophile.GetAsksHead(ammAddress) + accNotional := big.NewInt(0) // 18 decimals + accBaseQ := big.NewInt(0) // 18 decimals + for tick.Sign() != 0 { + amount := bibliophile.GetAskSize(ammAddress, tick) + accumulator := hu.Add(accBaseQ, amount) + if accumulator.Cmp(baseAssetQuantity) >= 0 { + break + } + accNotional.Add(accNotional, hu.Div1e6(hu.Mul(amount, tick))) + accBaseQ = accumulator + tick = bibliophile.GetNextAskPrice(ammAddress, tick) + } + if tick.Sign() == 0 { + return big.NewInt(0) // insufficient liquidity + } + notionalAtTick := hu.Div1e6(hu.Mul(hu.Sub(baseAssetQuantity, accBaseQ), tick)) + return hu.Div(hu.Mul1e6(hu.Add(accNotional, notionalAtTick)), baseAssetQuantity) // return value is in 6 decimals +} + +func _sampleBid(bibliophile b.BibliophileClient, ammAddress common.Address, baseAssetQuantity *big.Int) *big.Int { + if baseAssetQuantity.Sign() <= 0 { + return big.NewInt(0) + } + tick := bibliophile.GetBidsHead(ammAddress) + accNotional := big.NewInt(0) // 18 decimals + accBaseQ := big.NewInt(0) // 18 decimals + for tick.Sign() != 0 { + amount := bibliophile.GetBidSize(ammAddress, tick) + accumulator := hu.Add(accBaseQ, amount) + if accumulator.Cmp(baseAssetQuantity) >= 0 { + break + } + accNotional.Add(accNotional, hu.Div1e6(hu.Mul(amount, tick))) + accBaseQ = accumulator + tick = bibliophile.GetNextBidPrice(ammAddress, tick) + } + if tick.Sign() == 0 { + return big.NewInt(0) // insufficient liquidity + } + notionalAtTick := hu.Div1e6(hu.Mul(hu.Sub(baseAssetQuantity, accBaseQ), tick)) + return hu.Div(hu.Mul1e6(hu.Add(accNotional, notionalAtTick)), baseAssetQuantity) // return value is in 6 decimals +} diff --git a/precompile/contracts/ticks/logic_test.go b/precompile/contracts/ticks/logic_test.go new file mode 100644 index 0000000000..bc6d3882eb --- /dev/null +++ b/precompile/contracts/ticks/logic_test.go @@ -0,0 +1,601 @@ +package ticks + +import ( + "fmt" + "math/big" + + "testing" + + hu "github.com/ava-labs/subnet-evm/plugin/evm/orderbook/hubbleutils" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/assert" + + b "github.com/ava-labs/subnet-evm/precompile/contracts/bibliophile" + gomock "github.com/golang/mock/gomock" +) + +func TestGetPrevTick(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockBibliophile := b.NewMockBibliophileClient(ctrl) + ammAddress := common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8") + t.Run("when input tick price is 0", func(t *testing.T) { + t.Run("For a bid", func(t *testing.T) { + input := GetPrevTickInput{ + Amm: ammAddress, + Tick: big.NewInt(0), + IsBid: true, + } + output, err := GetPrevTick(mockBibliophile, input) + assert.Equal(t, "tick price cannot be zero", err.Error()) + var expectedPrevTick *big.Int = nil + assert.Equal(t, expectedPrevTick, output) + }) + t.Run("For an ask", func(t *testing.T) { + input := GetPrevTickInput{ + Amm: ammAddress, + Tick: big.NewInt(0), + IsBid: false, + } + output, err := GetPrevTick(mockBibliophile, input) + assert.Equal(t, "tick price cannot be zero", err.Error()) + var expectedPrevTick *big.Int = nil + assert.Equal(t, expectedPrevTick, output) + }) + }) + t.Run("when input tick price > 0", func(t *testing.T) { + t.Run("For a bid", func(t *testing.T) { + bidsHead := big.NewInt(10000000) // 10 + t.Run("when bid price >= bidsHead", func(t *testing.T) { + //covers bidsHead == 0 + t.Run("it returns error when bid price == bidsHead", func(t *testing.T) { + input := GetPrevTickInput{ + Amm: ammAddress, + Tick: big.NewInt(0).Set(bidsHead), + IsBid: true, + } + mockBibliophile.EXPECT().GetBidsHead(ammAddress).Return(bidsHead).Times(1) + prevTick, err := GetPrevTick(mockBibliophile, input) + assert.Equal(t, fmt.Sprintf("tick %v is greater than or equal to bidsHead %v", input.Tick, bidsHead), err.Error()) + var expectedPrevTick *big.Int = nil + assert.Equal(t, expectedPrevTick, prevTick) + }) + t.Run("it returns error when bid price > bidsHead", func(t *testing.T) { + input := GetPrevTickInput{ + Amm: ammAddress, + Tick: big.NewInt(0).Add(bidsHead, big.NewInt(1)), + IsBid: true, + } + mockBibliophile.EXPECT().GetBidsHead(ammAddress).Return(bidsHead).Times(1) + prevTick, err := GetPrevTick(mockBibliophile, input) + assert.Equal(t, fmt.Sprintf("tick %v is greater than or equal to bidsHead %v", input.Tick, bidsHead), err.Error()) + var expectedPrevTick *big.Int = nil + assert.Equal(t, expectedPrevTick, prevTick) + }) + }) + t.Run("when bid price < bidsHead", func(t *testing.T) { + t.Run("when there is only 1 bid in orderbook", func(t *testing.T) { + t.Run("it returns bidsHead as prevTick", func(t *testing.T) { + input := GetPrevTickInput{ + Amm: ammAddress, + Tick: big.NewInt(0).Div(bidsHead, big.NewInt(2)), + IsBid: true, + } + mockBibliophile.EXPECT().GetBidsHead(ammAddress).Return(bidsHead).Times(1) + mockBibliophile.EXPECT().GetNextBidPrice(input.Amm, bidsHead).Return(big.NewInt(0)).Times(1) + prevTick, err := GetPrevTick(mockBibliophile, input) + assert.Equal(t, nil, err) + assert.Equal(t, bidsHead, prevTick) + }) + }) + t.Run("when there are more than 1 bids in orderbook", func(t *testing.T) { + bids := []*big.Int{big.NewInt(10000000), big.NewInt(9000000), big.NewInt(8000000), big.NewInt(7000000)} + t.Run("when bid price does not match any bids in orderbook", func(t *testing.T) { + t.Run("it returns prevTick when bid price falls between bids in orderbook", func(t *testing.T) { + input := GetPrevTickInput{ + Amm: ammAddress, + Tick: big.NewInt(8100000), + IsBid: true, + } + mockBibliophile.EXPECT().GetBidsHead(ammAddress).Return(bidsHead).Times(1) + mockBibliophile.EXPECT().GetNextBidPrice(input.Amm, bids[0]).Return(bids[1]).Times(1) + mockBibliophile.EXPECT().GetNextBidPrice(input.Amm, bids[1]).Return(bids[2]).Times(1) + prevTick, err := GetPrevTick(mockBibliophile, input) + assert.Equal(t, nil, err) + assert.Equal(t, bids[1], prevTick) + }) + t.Run("it returns prevTick when bid price is lowest in orderbook", func(t *testing.T) { + input := GetPrevTickInput{ + Amm: ammAddress, + Tick: big.NewInt(400000), + IsBid: true, + } + mockBibliophile.EXPECT().GetBidsHead(ammAddress).Return(bidsHead).Times(1) + for i := 0; i < len(bids)-1; i++ { + mockBibliophile.EXPECT().GetNextBidPrice(input.Amm, bids[i]).Return(bids[i+1]).Times(1) + } + mockBibliophile.EXPECT().GetNextBidPrice(input.Amm, bids[len(bids)-1]).Return(big.NewInt(0)).Times(1) + prevTick, err := GetPrevTick(mockBibliophile, input) + assert.Equal(t, nil, err) + assert.Equal(t, bids[len(bids)-1], prevTick) + }) + }) + t.Run("when bid price matches another bid's price in orderbook", func(t *testing.T) { + t.Run("it returns prevTick", func(t *testing.T) { + input := GetPrevTickInput{ + Amm: ammAddress, + Tick: bids[2], + IsBid: true, + } + mockBibliophile.EXPECT().GetBidsHead(ammAddress).Return(bidsHead).Times(1) + mockBibliophile.EXPECT().GetNextBidPrice(input.Amm, bids[0]).Return(bids[1]).Times(1) + mockBibliophile.EXPECT().GetNextBidPrice(input.Amm, bids[1]).Return(bids[2]).Times(1) + prevTick, err := GetPrevTick(mockBibliophile, input) + assert.Equal(t, nil, err) + assert.Equal(t, bids[1], prevTick) + }) + }) + }) + }) + }) + t.Run("For an ask", func(t *testing.T) { + t.Run("when asksHead is 0", func(t *testing.T) { + t.Run("it returns error", func(t *testing.T) { + asksHead := big.NewInt(0) + + mockBibliophile.EXPECT().GetAsksHead(ammAddress).Return(asksHead).Times(1) + input := GetPrevTickInput{ + Amm: ammAddress, + Tick: big.NewInt(10), + IsBid: false, + } + prevTick, err := GetPrevTick(mockBibliophile, input) + assert.Equal(t, "asksHead is zero", err.Error()) + var expectedPrevTick *big.Int = nil + assert.Equal(t, expectedPrevTick, prevTick) + }) + }) + t.Run("when asksHead > 0", func(t *testing.T) { + asksHead := big.NewInt(10000000) + t.Run("it returns error when ask price == asksHead", func(t *testing.T) { + mockBibliophile.EXPECT().GetAsksHead(ammAddress).Return(asksHead).Times(1) + input := GetPrevTickInput{ + Amm: ammAddress, + Tick: big.NewInt(0).Set(asksHead), + IsBid: false, + } + prevTick, err := GetPrevTick(mockBibliophile, input) + assert.Equal(t, fmt.Sprintf("tick %d is less than or equal to asksHead %d", input.Tick, asksHead), err.Error()) + var expectedPrevTick *big.Int = nil + assert.Equal(t, expectedPrevTick, prevTick) + }) + t.Run("it returns error when ask price < asksHead", func(t *testing.T) { + mockBibliophile.EXPECT().GetAsksHead(ammAddress).Return(asksHead).Times(1) + input := GetPrevTickInput{ + Amm: ammAddress, + Tick: big.NewInt(0).Sub(asksHead, big.NewInt(1)), + IsBid: false, + } + prevTick, err := GetPrevTick(mockBibliophile, input) + assert.Equal(t, fmt.Sprintf("tick %d is less than or equal to asksHead %d", input.Tick, asksHead), err.Error()) + var expectedPrevTick *big.Int = nil + assert.Equal(t, expectedPrevTick, prevTick) + }) + t.Run("when ask price > asksHead", func(t *testing.T) { + t.Run("when there is only one ask in orderbook", func(t *testing.T) { + t.Run("it returns asksHead as prevTick", func(t *testing.T) { + mockBibliophile.EXPECT().GetAsksHead(ammAddress).Return(asksHead).Times(1) + input := GetPrevTickInput{ + Amm: ammAddress, + Tick: big.NewInt(0).Add(asksHead, big.NewInt(1)), + IsBid: false, + } + mockBibliophile.EXPECT().GetNextAskPrice(input.Amm, asksHead).Return(big.NewInt(0)).Times(1) + prevTick, err := GetPrevTick(mockBibliophile, input) + assert.Equal(t, nil, err) + var expectedPrevTick *big.Int = asksHead + assert.Equal(t, expectedPrevTick, prevTick) + }) + }) + t.Run("when there are multiple asks in orderbook", func(t *testing.T) { + asks := []*big.Int{asksHead, big.NewInt(11000000), big.NewInt(12000000), big.NewInt(13000000)} + t.Run("when ask price does not match any asks in orderbook", func(t *testing.T) { + t.Run("it returns prevTick when ask price falls between asks in orderbook", func(t *testing.T) { + mockBibliophile.EXPECT().GetAsksHead(ammAddress).Return(asksHead).Times(1) + askPrice := big.NewInt(11500000) + input := GetPrevTickInput{ + Amm: ammAddress, + Tick: askPrice, + IsBid: false, + } + mockBibliophile.EXPECT().GetNextAskPrice(input.Amm, asksHead).Return(asks[0]).Times(1) + mockBibliophile.EXPECT().GetNextAskPrice(input.Amm, asks[0]).Return(asks[1]).Times(1) + mockBibliophile.EXPECT().GetNextAskPrice(input.Amm, asks[1]).Return(asks[2]).Times(1) + prevTick, err := GetPrevTick(mockBibliophile, input) + assert.Equal(t, nil, err) + var expectedPrevTick *big.Int = asks[1] + assert.Equal(t, expectedPrevTick, prevTick) + }) + t.Run("it returns prevTick when ask price is highest in orderbook", func(t *testing.T) { + mockBibliophile.EXPECT().GetAsksHead(ammAddress).Return(asksHead).Times(1) + askPrice := big.NewInt(0).Add(asks[len(asks)-1], big.NewInt(1)) + input := GetPrevTickInput{ + Amm: ammAddress, + Tick: askPrice, + IsBid: false, + } + mockBibliophile.EXPECT().GetNextAskPrice(input.Amm, asksHead).Return(asks[0]).Times(1) + mockBibliophile.EXPECT().GetNextAskPrice(input.Amm, asks[0]).Return(asks[1]).Times(1) + mockBibliophile.EXPECT().GetNextAskPrice(input.Amm, asks[1]).Return(asks[2]).Times(1) + mockBibliophile.EXPECT().GetNextAskPrice(input.Amm, asks[2]).Return(big.NewInt(0)).Times(1) + prevTick, err := GetPrevTick(mockBibliophile, input) + assert.Equal(t, nil, err) + var expectedPrevTick *big.Int = asks[2] + assert.Equal(t, expectedPrevTick, prevTick) + }) + }) + t.Run("when ask price matches another ask's price in orderbook", func(t *testing.T) { + t.Run("it returns prevTick", func(t *testing.T) { + mockBibliophile.EXPECT().GetAsksHead(ammAddress).Return(asksHead).Times(1) + askPrice := asks[1] + input := GetPrevTickInput{ + Amm: ammAddress, + Tick: askPrice, + IsBid: false, + } + mockBibliophile.EXPECT().GetNextAskPrice(input.Amm, asksHead).Return(asks[0]).Times(1) + mockBibliophile.EXPECT().GetNextAskPrice(input.Amm, asks[0]).Return(asks[1]).Times(1) + prevTick, err := GetPrevTick(mockBibliophile, input) + assert.Equal(t, nil, err) + var expectedPrevTick *big.Int = asks[0] + assert.Equal(t, expectedPrevTick, prevTick) + }) + }) + }) + }) + }) + }) + }) +} + +func TestSampleImpactBid(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockBibliophile := b.NewMockBibliophileClient(ctrl) + ammAddress := common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8") + t.Run("when impactMarginNotional is zero", func(t *testing.T) { + mockBibliophile.EXPECT().GetImpactMarginNotional(ammAddress).Return(big.NewInt(0)).Times(1) + output := SampleImpactBid(mockBibliophile, ammAddress) + assert.Equal(t, big.NewInt(0), output) + }) + t.Run("when impactMarginNotional is > zero", func(t *testing.T) { + impactMarginNotional := big.NewInt(4000000000) // 4000 units + t.Run("when bidsHead is 0", func(t *testing.T) { + mockBibliophile.EXPECT().GetImpactMarginNotional(ammAddress).Return(impactMarginNotional).Times(1) + mockBibliophile.EXPECT().GetBidsHead(ammAddress).Return(big.NewInt(0)).Times(1) + output := SampleImpactBid(mockBibliophile, ammAddress) + assert.Equal(t, big.NewInt(0), output) + }) + t.Run("when bidsHead > 0", func(t *testing.T) { + bidsHead := big.NewInt(20000000) // 20 units + t.Run("when bids in orderbook are not enough to cover impactMarginNotional", func(t *testing.T) { + t.Run("when there is only one bid in orderbook it returns 0", func(t *testing.T) { + mockBibliophile.EXPECT().GetImpactMarginNotional(ammAddress).Return(impactMarginNotional).Times(1) + mockBibliophile.EXPECT().GetBidsHead(ammAddress).Return(bidsHead).Times(1) + mockBibliophile.EXPECT().GetBidSize(ammAddress, bidsHead).Return(big.NewInt(1e18)).Times(1) + mockBibliophile.EXPECT().GetNextBidPrice(ammAddress, bidsHead).Return(big.NewInt(0)).Times(1) + output := SampleImpactBid(mockBibliophile, ammAddress) + assert.Equal(t, big.NewInt(0), output) + }) + t.Run("when there are multiple bids", func(t *testing.T) { + bids := []*big.Int{bidsHead, big.NewInt(2100000), big.NewInt(2200000), big.NewInt(2300000)} + size := big.NewInt(1e18) // 1 ether + mockBibliophile.EXPECT().GetImpactMarginNotional(ammAddress).Return(impactMarginNotional).Times(1) + mockBibliophile.EXPECT().GetBidsHead(ammAddress).Return(bidsHead).Times(1) + for i := 0; i < len(bids); i++ { + mockBibliophile.EXPECT().GetBidSize(ammAddress, bids[i]).Return(size).Times(1) + if i != len(bids)-1 { + mockBibliophile.EXPECT().GetNextBidPrice(ammAddress, bids[i]).Return(bids[i+1]).Times(1) + } else { + mockBibliophile.EXPECT().GetNextBidPrice(ammAddress, bids[i]).Return(big.NewInt(0)).Times(1) + } + } + + accumulatedMarginNotional := big.NewInt(0) + for i := 0; i < len(bids); i++ { + accumulatedMarginNotional.Add(accumulatedMarginNotional, hu.Div(hu.Mul(bids[i], size), big.NewInt(1e18))) + } + //asserting to check if testing conditions are setup correctly + assert.Equal(t, -1, accumulatedMarginNotional.Cmp(impactMarginNotional)) + // accBaseQ := big.NewInt(0).Mul(size, big.NewInt(int64(len(bids)))) + // expectedSampleImpactBid := hu.Div(hu.Mul(accumulatedMarginNotional, big.NewInt(1e18)), accBaseQ) + output := SampleImpactBid(mockBibliophile, ammAddress) + assert.Equal(t, big.NewInt(0), output) + // assert.Equal(t, expectedSampleImpactBid, output) + }) + }) + t.Run("when bids in orderbook are enough to cover impactMarginNotional", func(t *testing.T) { + t.Run("when there is only one bid in orderbook it returns bidsHead", func(t *testing.T) { + bidsHead := impactMarginNotional + mockBibliophile.EXPECT().GetImpactMarginNotional(ammAddress).Return(impactMarginNotional).Times(1) + mockBibliophile.EXPECT().GetBidsHead(ammAddress).Return(bidsHead).Times(1) + mockBibliophile.EXPECT().GetBidSize(ammAddress, bidsHead).Return(big.NewInt(1e18)).Times(1) + output := SampleImpactBid(mockBibliophile, ammAddress) + assert.Equal(t, bidsHead, output) + }) + t.Run("when there are multiple bids, it tries to fill with available bids and average price is returned for rest", func(t *testing.T) { + bidsHead := big.NewInt(2000000000) // 2000 units + bids := []*big.Int{bidsHead} + for i := int64(1); i < 6; i++ { + bids = append(bids, big.NewInt(0).Sub(bidsHead, big.NewInt(i))) + } + size := big.NewInt(6e17) // 0.6 ether + mockBibliophile.EXPECT().GetImpactMarginNotional(ammAddress).Return(impactMarginNotional).Times(1) + mockBibliophile.EXPECT().GetBidsHead(ammAddress).Return(bidsHead).Times(1) + mockBibliophile.EXPECT().GetNextBidPrice(ammAddress, bids[0]).Return(bids[1]).Times(1) + mockBibliophile.EXPECT().GetNextBidPrice(ammAddress, bids[1]).Return(bids[2]).Times(1) + mockBibliophile.EXPECT().GetNextBidPrice(ammAddress, bids[2]).Return(bids[3]).Times(1) + mockBibliophile.EXPECT().GetBidSize(ammAddress, bids[0]).Return(size).Times(1) + mockBibliophile.EXPECT().GetBidSize(ammAddress, bids[1]).Return(size).Times(1) + mockBibliophile.EXPECT().GetBidSize(ammAddress, bids[2]).Return(size).Times(1) + mockBibliophile.EXPECT().GetBidSize(ammAddress, bids[3]).Return(size).Times(1) + + output := SampleImpactBid(mockBibliophile, ammAddress) + // 3 bids are filled and 3 are left + totalBaseQ := big.NewInt(0).Mul(size, big.NewInt(3)) + filledQuote := big.NewInt(0) + for i := 0; i < 3; i++ { + filledQuote.Add(filledQuote, (hu.Div(hu.Mul(bids[i], size), big.NewInt(1e18)))) + } + unfulFilledQuote := big.NewInt(0).Sub(impactMarginNotional, filledQuote) + // as quantity is in 1e18 baseQ = price * 1e18 / price + baseQAtTick := big.NewInt(0).Div(big.NewInt(0).Mul(unfulFilledQuote, big.NewInt(1e18)), bids[3]) + expectedOutput := big.NewInt(0).Div(big.NewInt(0).Mul(impactMarginNotional, big.NewInt(1e18)), big.NewInt(0).Add(totalBaseQ, baseQAtTick)) + assert.Equal(t, expectedOutput, output) + }) + }) + }) + }) +} + +func TestSampleImpactAsk(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockBibliophile := b.NewMockBibliophileClient(ctrl) + ammAddress := common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8") + t.Run("when impactMarginNotional is zero", func(t *testing.T) { + mockBibliophile.EXPECT().GetImpactMarginNotional(ammAddress).Return(big.NewInt(0)).Times(1) + output := SampleImpactAsk(mockBibliophile, ammAddress) + assert.Equal(t, big.NewInt(0), output) + }) + t.Run("when impactMarginNotional is > zero", func(t *testing.T) { + impactMarginNotional := big.NewInt(4000000000) // 4000 units + t.Run("when asksHead is 0", func(t *testing.T) { + mockBibliophile.EXPECT().GetImpactMarginNotional(ammAddress).Return(impactMarginNotional).Times(1) + mockBibliophile.EXPECT().GetAsksHead(ammAddress).Return(big.NewInt(0)).Times(1) + output := SampleImpactAsk(mockBibliophile, ammAddress) + assert.Equal(t, big.NewInt(0), output) + }) + t.Run("when asksHead > 0", func(t *testing.T) { + asksHead := big.NewInt(20000000) // 20 units + t.Run("when asks in orderbook are not enough to cover impactMarginNotional", func(t *testing.T) { + t.Run("when there is only one ask in orderbook it returns asksHead", func(t *testing.T) { + mockBibliophile.EXPECT().GetImpactMarginNotional(ammAddress).Return(impactMarginNotional).Times(1) + mockBibliophile.EXPECT().GetAsksHead(ammAddress).Return(asksHead).Times(1) + mockBibliophile.EXPECT().GetAskSize(ammAddress, asksHead).Return(big.NewInt(1e18)).Times(1) + mockBibliophile.EXPECT().GetNextAskPrice(ammAddress, asksHead).Return(big.NewInt(0)).Times(1) + output := SampleImpactAsk(mockBibliophile, ammAddress) + assert.Equal(t, big.NewInt(0), output) + }) + t.Run("when there are multiple asks", func(t *testing.T) { + asks := []*big.Int{asksHead, big.NewInt(2100000), big.NewInt(2200000), big.NewInt(2300000)} + size := big.NewInt(1e18) // 1 ether + mockBibliophile.EXPECT().GetImpactMarginNotional(ammAddress).Return(impactMarginNotional).Times(1) + mockBibliophile.EXPECT().GetAsksHead(ammAddress).Return(asksHead).Times(1) + for i := 0; i < len(asks); i++ { + mockBibliophile.EXPECT().GetAskSize(ammAddress, asks[i]).Return(size).Times(1) + if i != len(asks)-1 { + mockBibliophile.EXPECT().GetNextAskPrice(ammAddress, asks[i]).Return(asks[i+1]).Times(1) + } else { + mockBibliophile.EXPECT().GetNextAskPrice(ammAddress, asks[i]).Return(big.NewInt(0)).Times(1) + } + } + + accumulatedMarginNotional := big.NewInt(0) + for i := 0; i < len(asks); i++ { + accumulatedMarginNotional.Add(accumulatedMarginNotional, hu.Div(hu.Mul(asks[i], size), big.NewInt(1e18))) + } + //asserting to check if testing conditions are setup correctly + assert.Equal(t, -1, accumulatedMarginNotional.Cmp(impactMarginNotional)) + output := SampleImpactAsk(mockBibliophile, ammAddress) + assert.Equal(t, big.NewInt(0), output) + }) + }) + t.Run("when asks in orderbook are enough to cover impactMarginNotional", func(t *testing.T) { + t.Run("when there is only one ask in orderbook it returns asksHead", func(t *testing.T) { + newAsksHead := impactMarginNotional + mockBibliophile.EXPECT().GetImpactMarginNotional(ammAddress).Return(impactMarginNotional).Times(1) + mockBibliophile.EXPECT().GetAsksHead(ammAddress).Return(newAsksHead).Times(1) + mockBibliophile.EXPECT().GetAskSize(ammAddress, newAsksHead).Return(big.NewInt(1e18)).Times(1) + output := SampleImpactAsk(mockBibliophile, ammAddress) + assert.Equal(t, newAsksHead, output) + }) + t.Run("when there are multiple asks, it tries to fill with available asks and average price is returned for rest", func(t *testing.T) { + newAsksHead := big.NewInt(2000000000) // 2000 units + asks := []*big.Int{newAsksHead} + for i := int64(1); i < 6; i++ { + asks = append(asks, big.NewInt(0).Add(newAsksHead, big.NewInt(i))) + } + size := big.NewInt(6e17) // 0.6 ether + mockBibliophile.EXPECT().GetImpactMarginNotional(ammAddress).Return(impactMarginNotional).Times(1) + mockBibliophile.EXPECT().GetAsksHead(ammAddress).Return(newAsksHead).Times(1) + mockBibliophile.EXPECT().GetNextAskPrice(ammAddress, asks[0]).Return(asks[1]).Times(1) + mockBibliophile.EXPECT().GetNextAskPrice(ammAddress, asks[1]).Return(asks[2]).Times(1) + mockBibliophile.EXPECT().GetNextAskPrice(ammAddress, asks[2]).Return(asks[3]).Times(1) + mockBibliophile.EXPECT().GetAskSize(ammAddress, asks[0]).Return(size).Times(1) + mockBibliophile.EXPECT().GetAskSize(ammAddress, asks[1]).Return(size).Times(1) + mockBibliophile.EXPECT().GetAskSize(ammAddress, asks[2]).Return(size).Times(1) + mockBibliophile.EXPECT().GetAskSize(ammAddress, asks[3]).Return(size).Times(1) + + // 2000 * .6 + 2001 * .6 + 2002 * .6 = 3,601.8 + // 3 asks are filled and 3 are left + accBaseQ := big.NewInt(0).Mul(size, big.NewInt(3)) + filledQuote := big.NewInt(0) + for i := 0; i < 3; i++ { + filledQuote.Add(filledQuote, hu.Div1e6(big.NewInt(0).Mul(asks[i], size))) + } + _impactMarginNotional := new(big.Int).Mul(impactMarginNotional, big.NewInt(1e12)) + baseQAtTick := new(big.Int).Div(hu.Mul1e6(big.NewInt(0).Sub(_impactMarginNotional, filledQuote)), asks[3]) + expectedOutput := new(big.Int).Div(hu.Mul1e6(_impactMarginNotional), new(big.Int).Add(baseQAtTick, accBaseQ)) + assert.Equal(t, expectedOutput, SampleImpactAsk(mockBibliophile, ammAddress)) + }) + }) + }) + }) +} + +func TestSampleBid(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockBibliophile := b.NewMockBibliophileClient(ctrl) + ammAddress := common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8") + bidsHead := big.NewInt(20 * 1e6) // $20 + baseAssetQuantity := big.NewInt(1e18) // 1 ether + t.Run("when bidsHead is 0", func(t *testing.T) { + mockBibliophile.EXPECT().GetBidsHead(ammAddress).Return(big.NewInt(0)).Times(1) + output := _sampleBid(mockBibliophile, ammAddress, baseAssetQuantity) + assert.Equal(t, big.NewInt(0), output) + }) + t.Run("when bidsHead > 0", func(t *testing.T) { + t.Run("when bids in orderbook are not enough to cover baseAssetQuantity", func(t *testing.T) { + t.Run("when there is only one bid in orderbook it returns 0", func(t *testing.T) { + mockBibliophile.EXPECT().GetBidsHead(ammAddress).Return(bidsHead).Times(1) + mockBibliophile.EXPECT().GetBidSize(ammAddress, bidsHead).Return(hu.Sub(baseAssetQuantity, big.NewInt(1))).Times(1) + mockBibliophile.EXPECT().GetNextBidPrice(ammAddress, bidsHead).Return(big.NewInt(0)).Times(1) + output := _sampleBid(mockBibliophile, ammAddress, baseAssetQuantity) + assert.Equal(t, big.NewInt(0), output) + }) + t.Run("when there are multiple bids", func(t *testing.T) { + bids := []*big.Int{bidsHead, big.NewInt(2100000), big.NewInt(2200000), big.NewInt(2300000)} + size := big.NewInt(24 * 1e16) // 0.24 ether + mockBibliophile.EXPECT().GetBidsHead(ammAddress).Return(bidsHead).Times(1) + for i := 0; i < len(bids); i++ { + mockBibliophile.EXPECT().GetBidSize(ammAddress, bids[i]).Return(size).Times(1) + if i != len(bids)-1 { + mockBibliophile.EXPECT().GetNextBidPrice(ammAddress, bids[i]).Return(bids[i+1]).Times(1) + } else { + mockBibliophile.EXPECT().GetNextBidPrice(ammAddress, bids[i]).Return(big.NewInt(0)).Times(1) + } + } + output := _sampleBid(mockBibliophile, ammAddress, baseAssetQuantity) + assert.Equal(t, big.NewInt(0), output) + }) + }) + t.Run("when bids in orderbook are enough to cover baseAssetQuantity", func(t *testing.T) { + t.Run("when there is only one bid in orderbook it returns bidsHead", func(t *testing.T) { + mockBibliophile.EXPECT().GetBidsHead(ammAddress).Return(bidsHead).Times(1) + mockBibliophile.EXPECT().GetBidSize(ammAddress, bidsHead).Return(baseAssetQuantity).Times(1) + output := _sampleBid(mockBibliophile, ammAddress, baseAssetQuantity) + assert.Equal(t, bidsHead, output) + }) + t.Run("when there are multiple bids, it tries to fill with available bids and average price is returned for rest", func(t *testing.T) { + bids := []*big.Int{bidsHead} + for i := int64(1); i < 6; i++ { + bids = append(bids, hu.Sub(bidsHead, big.NewInt(i))) + } + size := big.NewInt(3e17) // 0.3 ether + mockBibliophile.EXPECT().GetBidsHead(ammAddress).Return(bidsHead).Times(1) + mockBibliophile.EXPECT().GetNextBidPrice(ammAddress, bids[0]).Return(bids[1]).Times(1) + mockBibliophile.EXPECT().GetNextBidPrice(ammAddress, bids[1]).Return(bids[2]).Times(1) + mockBibliophile.EXPECT().GetNextBidPrice(ammAddress, bids[2]).Return(bids[3]).Times(1) + mockBibliophile.EXPECT().GetBidSize(ammAddress, bids[0]).Return(size).Times(1) + mockBibliophile.EXPECT().GetBidSize(ammAddress, bids[1]).Return(size).Times(1) + mockBibliophile.EXPECT().GetBidSize(ammAddress, bids[2]).Return(size).Times(1) + mockBibliophile.EXPECT().GetBidSize(ammAddress, bids[3]).Return(size).Times(1) + + output := _sampleBid(mockBibliophile, ammAddress, baseAssetQuantity) + accBaseQ := hu.Mul(size, big.NewInt(3)) + accNotional := big.NewInt(0) + for i := 0; i < 3; i++ { + accNotional.Add(accNotional, (hu.Div1e6(hu.Mul(bids[i], size)))) + } + notionalAtTick := hu.Div1e6(hu.Mul(hu.Sub(baseAssetQuantity, accBaseQ), bids[3])) + expectedOutput := hu.Div(hu.Mul1e6(hu.Add(accNotional, notionalAtTick)), baseAssetQuantity) + assert.Equal(t, expectedOutput, output) + }) + }) + }) +} + +func TestSampleAsk(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockBibliophile := b.NewMockBibliophileClient(ctrl) + ammAddress := common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8") + asksHead := big.NewInt(20 * 1e6) // $20 + baseAssetQuantity := big.NewInt(1e18) // 1 ether + t.Run("when asksHead is 0", func(t *testing.T) { + mockBibliophile.EXPECT().GetAsksHead(ammAddress).Return(big.NewInt(0)).Times(1) + output := _sampleAsk(mockBibliophile, ammAddress, baseAssetQuantity) + assert.Equal(t, big.NewInt(0), output) + }) + t.Run("when asksHead > 0", func(t *testing.T) { + t.Run("when asks in orderbook are not enough to cover baseAssetQuantity", func(t *testing.T) { + t.Run("when there is only one ask in orderbook it returns 0", func(t *testing.T) { + mockBibliophile.EXPECT().GetAsksHead(ammAddress).Return(asksHead).Times(1) + mockBibliophile.EXPECT().GetAskSize(ammAddress, asksHead).Return(hu.Sub(baseAssetQuantity, big.NewInt(1))).Times(1) + mockBibliophile.EXPECT().GetNextAskPrice(ammAddress, asksHead).Return(big.NewInt(0)).Times(1) + output := _sampleAsk(mockBibliophile, ammAddress, baseAssetQuantity) + assert.Equal(t, big.NewInt(0), output) + }) + t.Run("when there are multiple asks, it tries to fill with available asks", func(t *testing.T) { + asks := []*big.Int{asksHead, big.NewInt(2100000), big.NewInt(2200000), big.NewInt(2300000)} + size := big.NewInt(24 * 1e16) // 0.24 ether + mockBibliophile.EXPECT().GetAsksHead(ammAddress).Return(asksHead).Times(1) + for i := 0; i < len(asks); i++ { + mockBibliophile.EXPECT().GetAskSize(ammAddress, asks[i]).Return(size).Times(1) + if i != len(asks)-1 { + mockBibliophile.EXPECT().GetNextAskPrice(ammAddress, asks[i]).Return(asks[i+1]).Times(1) + } else { + mockBibliophile.EXPECT().GetNextAskPrice(ammAddress, asks[i]).Return(big.NewInt(0)).Times(1) + } + } + output := _sampleAsk(mockBibliophile, ammAddress, baseAssetQuantity) + assert.Equal(t, big.NewInt(0), output) + }) + }) + t.Run("when asks in orderbook are enough to cover baseAssetQuantity", func(t *testing.T) { + t.Run("when there is only one ask in orderbook it returns asksHead", func(t *testing.T) { + mockBibliophile.EXPECT().GetAsksHead(ammAddress).Return(asksHead).Times(1) + mockBibliophile.EXPECT().GetAskSize(ammAddress, asksHead).Return(baseAssetQuantity).Times(1) + output := _sampleAsk(mockBibliophile, ammAddress, baseAssetQuantity) + assert.Equal(t, asksHead, output) + }) + t.Run("when there are multiple asks, it tries to fill with available asks and average price is returned for rest", func(t *testing.T) { + asks := []*big.Int{asksHead} + for i := int64(1); i < 6; i++ { + asks = append(asks, hu.Sub(asksHead, big.NewInt(i))) + } + size := big.NewInt(31e16) // 0.31 ether + mockBibliophile.EXPECT().GetAsksHead(ammAddress).Return(asksHead).Times(1) + mockBibliophile.EXPECT().GetNextAskPrice(ammAddress, asks[0]).Return(asks[1]).Times(1) + mockBibliophile.EXPECT().GetNextAskPrice(ammAddress, asks[1]).Return(asks[2]).Times(1) + mockBibliophile.EXPECT().GetNextAskPrice(ammAddress, asks[2]).Return(asks[3]).Times(1) + mockBibliophile.EXPECT().GetAskSize(ammAddress, asks[0]).Return(size).Times(1) + mockBibliophile.EXPECT().GetAskSize(ammAddress, asks[1]).Return(size).Times(1) + mockBibliophile.EXPECT().GetAskSize(ammAddress, asks[2]).Return(size).Times(1) + mockBibliophile.EXPECT().GetAskSize(ammAddress, asks[3]).Return(size).Times(1) + + output := _sampleAsk(mockBibliophile, ammAddress, baseAssetQuantity) + accBaseQ := hu.Mul(size, big.NewInt(3)) + accNotional := big.NewInt(0) + for i := 0; i < 3; i++ { + accNotional.Add(accNotional, (hu.Div1e6(hu.Mul(asks[i], size)))) + } + notionalAtTick := hu.Div1e6(hu.Mul(hu.Sub(baseAssetQuantity, accBaseQ), asks[3])) + expectedOutput := hu.Div(hu.Mul1e6(hu.Add(accNotional, notionalAtTick)), baseAssetQuantity) + assert.Equal(t, expectedOutput, output) + }) + }) + }) +} diff --git a/precompile/contracts/ticks/module.go b/precompile/contracts/ticks/module.go new file mode 100644 index 0000000000..ba1e88ed37 --- /dev/null +++ b/precompile/contracts/ticks/module.go @@ -0,0 +1,63 @@ +// Code generated +// This file is a generated precompile contract config with stubbed abstract functions. +// The file is generated by a template. Please inspect every code and comment in this file before use. + +package ticks + +import ( + "fmt" + + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ava-labs/subnet-evm/precompile/modules" + "github.com/ava-labs/subnet-evm/precompile/precompileconfig" + + "github.com/ethereum/go-ethereum/common" +) + +var _ contract.Configurator = &configurator{} + +// ConfigKey is the key used in json config files to specify this precompile precompileconfig. +// must be unique across all precompiles. +const ConfigKey = "ticksConfig" + +// ContractAddress is the defined address of the precompile contract. +// This should be unique across all precompile contracts. +// See precompile/registry/registry.go for registered precompile contracts and more information. +var ContractAddress = common.HexToAddress("0x03000000000000000000000000000000000000a1") // SET A SUITABLE HEX ADDRESS HERE + +// Module is the precompile module. It is used to register the precompile contract. +var Module = modules.Module{ + ConfigKey: ConfigKey, + Address: ContractAddress, + Contract: TicksPrecompile, + Configurator: &configurator{}, +} + +type configurator struct{} + +func init() { + // Register the precompile module. + // Each precompile contract registers itself through [RegisterModule] function. + if err := modules.RegisterModule(Module); err != nil { + panic(err) + } +} + +// MakeConfig returns a new precompile config instance. +// This is required for Marshal/Unmarshal the precompile config. +func (*configurator) MakeConfig() precompileconfig.Config { + return new(Config) +} + +// Configure configures [state] with the given [cfg] precompileconfig. +// This function is called by the EVM once per precompile contract activation. +// You can use this function to set up your precompile contract's initial state, +// by using the [cfg] config and [state] stateDB. +func (*configurator) Configure(chainConfig precompileconfig.ChainConfig, cfg precompileconfig.Config, state contract.StateDB, _ contract.ConfigurationBlockContext) error { + config, ok := cfg.(*Config) + if !ok { + return fmt.Errorf("incorrect config %T: %v", config, config) + } + // CUSTOM CODE STARTS HERE + return nil +} diff --git a/precompile/registry/registry.go b/precompile/registry/registry.go index 490694d669..ae09b11707 100644 --- a/precompile/registry/registry.go +++ b/precompile/registry/registry.go @@ -17,6 +17,9 @@ import ( _ "github.com/ava-labs/subnet-evm/precompile/contracts/rewardmanager" + _ "github.com/ava-labs/subnet-evm/precompile/contracts/juror" + _ "github.com/ava-labs/subnet-evm/precompile/contracts/jurorv2" + _ "github.com/ava-labs/subnet-evm/precompile/contracts/ticks" _ "github.com/ava-labs/subnet-evm/precompile/contracts/warp" // ADD YOUR PRECOMPILE HERE // _ "github.com/ava-labs/subnet-evm/precompile/contracts/yourprecompile" @@ -31,7 +34,7 @@ import ( // These start at the address: 0x0100000000000000000000000000000000000000 and will increment by 1. // Optional precompiles implemented in subnet-evm start at 0x0200000000000000000000000000000000000000 and will increment by 1 // from here to reduce the risk of conflicts. -// For forks of subnet-evm, users should start at 0x0300000000000000000000000000000000000000 to ensure +// For forks of subnet-evm, users should start at 0x03000000000000000000000000000000000000b0 to ensure // that their own modifications do not conflict with stateful precompiles that may be added to subnet-evm // in the future. // ContractDeployerAllowListAddress = common.HexToAddress("0x0200000000000000000000000000000000000000") @@ -40,5 +43,17 @@ import ( // FeeManagerAddress = common.HexToAddress("0x0200000000000000000000000000000000000003") // RewardManagerAddress = common.HexToAddress("0x0200000000000000000000000000000000000004") // WarpAddress = common.HexToAddress("0x0200000000000000000000000000000000000005") + // ADD YOUR PRECOMPILE HERE -// {YourPrecompile}Address = common.HexToAddress("0x03000000000000000000000000000000000000??") +// juror = common.HexToAddress("0x03000000000000000000000000000000000000a0") +// ticks = common.HexToAddress("0x03000000000000000000000000000000000000a1") +// jurorV2 = common.HexToAddress("0x03000000000000000000000000000000000000a2") + +// GenesisAddress +// OrderBook = common.HexToAddress("0x03000000000000000000000000000000000000b0") +// MarginAccount = common.HexToAddress("0x03000000000000000000000000000000000000b1") +// ClearingHouse = common.HexToAddress("0x03000000000000000000000000000000000000b2") +// limitOrderBook = common.HexToAddress("0x03000000000000000000000000000000000000b3") +// iocOrderBook = common.HexToAddress("0x03000000000000000000000000000000000000b4") + +// {YourPrecompile}Address = common.HexToAddress("0x03000000000000000000000000000000000000??") diff --git a/scripts/build_image.sh b/scripts/build_image.sh index 90ecccd522..9d0d00bfc4 100755 --- a/scripts/build_image.sh +++ b/scripts/build_image.sh @@ -17,6 +17,7 @@ source "$SUBNET_EVM_PATH"/scripts/versions.sh source "$SUBNET_EVM_PATH"/scripts/constants.sh BUILD_IMAGE_ID=${BUILD_IMAGE_ID:-"${AVALANCHE_VERSION}-Subnet-EVM-${CURRENT_BRANCH}"} +DOCKERHUB_REPO=${DOCKERHUB_REPO:-"avaplatform/avalanchego"} echo "Building Docker Image: $DOCKERHUB_REPO:$BUILD_IMAGE_ID based of $AVALANCHE_VERSION" docker build -t "$DOCKERHUB_REPO:$BUILD_IMAGE_ID" "$SUBNET_EVM_PATH" -f "$SUBNET_EVM_PATH/Dockerfile" \ diff --git a/scripts/check_local_health.sh b/scripts/check_local_health.sh new file mode 100755 index 0000000000..d61871114e --- /dev/null +++ b/scripts/check_local_health.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +set -e + +source local_status.sh + +apis=( + "http://127.0.0.1:9650/ext/bc/$CHAIN_ID/rpc" + "http://127.0.0.1:9652/ext/bc/$CHAIN_ID/rpc" + "http://127.0.0.1:9654/ext/bc/$CHAIN_ID/rpc" + "http://127.0.0.1:9656/ext/bc/$CHAIN_ID/rpc" + "http://127.0.0.1:9658/ext/bc/$CHAIN_ID/rpc" +) + +# Flag to track if any API took longer than 1 second to respond +error_flag=false + +# Loop through each API endpoint +for api in "${apis[@]}"; do + # Call the API endpoint with a timeout of 1 second + if ! curl --connect-timeout 1 --max-time 1 -s "$api" > /dev/null; then + echo "API $api did not respond within 1 second." + error_flag=true + fi +done + +# Check if any API took longer to respond +if [ "$error_flag" = true ]; then + echo "Error: One or more APIs did not respond within 1 second." +else + echo "OK: All APIs responded within 1 second." +fi diff --git a/scripts/constants.sh b/scripts/constants.sh index 54fe90b254..1c0d3663e2 100644 --- a/scripts/constants.sh +++ b/scripts/constants.sh @@ -8,9 +8,6 @@ set -euo pipefail # Set the PATHS GOPATH="$(go env GOPATH)" -# Avalabs docker hub -DOCKERHUB_REPO="avaplatform/avalanchego" - # if this isn't a git repository (say building from a release), don't set our git constants. if [ ! -d .git ]; then CURRENT_BRANCH="" diff --git a/scripts/run_local.sh b/scripts/run_local.sh new file mode 100755 index 0000000000..9d9e033511 --- /dev/null +++ b/scripts/run_local.sh @@ -0,0 +1,33 @@ + +#!/usr/bin/env bash +set -e +source ./scripts/utils.sh + +if ! [[ "$0" =~ scripts/run_local.sh ]]; then + echo "must be run from repository root" + exit 255 +fi + +avalanche network clean + +./scripts/build.sh custom_evm.bin + +FILE=/tmp/validator.pk +if [ ! -f "$FILE" ] +then + echo "$FILE does not exist; creating" + echo "31b571bf6894a248831ff937bb49f7754509fe93bbd2517c9c73c4144c0e97dc" > $FILE +fi + +avalanche subnet create localnet --force --custom --genesis genesis.json --vm custom_evm.bin --config .avalanche-cli.json --teleporter=false + +# configure and add chain.json +avalanche subnet configure localnet --chain-config chain.json --config .avalanche-cli.json +avalanche subnet configure localnet --subnet-config subnet.json --config .avalanche-cli.json +# avalanche subnet configure localnet --per-node-chain-config node_config.json --config .avalanche-cli.json + +# use the same avalanchego version as the one used in subnet-evm +# use tee to keep showing outut while storing in a var +OUTPUT=$(avalanche subnet deploy localnet -l --avalanchego-version v1.11.2 --config .avalanche-cli.json | tee /dev/fd/2) + +setStatus diff --git a/scripts/run_local_anr.sh b/scripts/run_local_anr.sh new file mode 100755 index 0000000000..77ad47af28 --- /dev/null +++ b/scripts/run_local_anr.sh @@ -0,0 +1,126 @@ +#!/usr/bin/env bash +set -e + +if ! [[ "$0" =~ scripts/run_local_anr.sh ]]; then + echo "must be run from repository root" + exit 255 +fi + +VERSION='v1.9.7' +ANR_VERSION='8438e423db523743c48bd178bc20642f9c3ba049' + +# Load the versions +SUBNET_EVM_PATH=$( + cd "$(dirname "${BASH_SOURCE[0]}")" + cd .. && pwd +) + +# Load the constants +source "$SUBNET_EVM_PATH"/scripts/constants.sh + + +############################ +# download avalanchego +# https://github.com/ava-labs/avalanchego/releases +GOARCH=$(go env GOARCH) +GOOS=$(go env GOOS) +BASEDIR=/tmp/subnet-evm-runner +mkdir -p ${BASEDIR} +AVAGO_DOWNLOAD_URL=https://github.com/ava-labs/avalanchego/releases/download/${VERSION}/avalanchego-linux-${GOARCH}-${VERSION}.tar.gz +AVAGO_DOWNLOAD_PATH=${BASEDIR}/avalanchego-linux-${GOARCH}-${VERSION}.tar.gz +if [[ ${GOOS} == "darwin" ]]; then + AVAGO_DOWNLOAD_URL=https://github.com/ava-labs/avalanchego/releases/download/${VERSION}/avalanchego-macos-${VERSION}.zip + AVAGO_DOWNLOAD_PATH=${BASEDIR}/avalanchego-macos-${VERSION}.zip +fi + +AVAGO_FILEPATH=${BASEDIR}/avalanchego-${VERSION} +if [[ ! -d ${AVAGO_FILEPATH} ]]; then + if [[ ! -f ${AVAGO_DOWNLOAD_PATH} ]]; then + echo "downloading avalanchego ${VERSION} at ${AVAGO_DOWNLOAD_URL} to ${AVAGO_DOWNLOAD_PATH}" + curl -L ${AVAGO_DOWNLOAD_URL} -o ${AVAGO_DOWNLOAD_PATH} + fi + echo "extracting downloaded avalanchego to ${AVAGO_FILEPATH}" + if [[ ${GOOS} == "linux" ]]; then + mkdir -p ${AVAGO_FILEPATH} && tar xzvf ${AVAGO_DOWNLOAD_PATH} --directory ${AVAGO_FILEPATH} --strip-components 1 + elif [[ ${GOOS} == "darwin" ]]; then + unzip ${AVAGO_DOWNLOAD_PATH} -d ${AVAGO_FILEPATH} + mv ${AVAGO_FILEPATH}/build/* ${AVAGO_FILEPATH} + rm -rf ${AVAGO_FILEPATH}/build/ + fi + find ${BASEDIR}/avalanchego-${VERSION} +fi + +AVALANCHEGO_PATH=${AVAGO_FILEPATH}/avalanchego +AVALANCHEGO_PLUGIN_DIR=${AVAGO_FILEPATH}/plugins + + +################################# +# compile subnet-evm +# Check if SUBNET_EVM_COMMIT is set, if not retrieve the last commit from the repo. +# This is used in the Dockerfile to allow a commit hash to be passed in without +# including the .git/ directory within the Docker image. +subnet_evm_commit=${SUBNET_EVM_COMMIT:-$(git rev-list -1 HEAD)} + +# Build Subnet EVM, which is run as a subprocess +echo "Building Subnet EVM Version: $subnet_evm_version; GitCommit: $subnet_evm_commit" +go build \ + -ldflags "-X github.com/ava-labs/subnet_evm/plugin/evm.GitCommit=$subnet_evm_commit -X github.com/ava-labs/subnet_evm/plugin/evm.Version=$subnet_evm_version" \ + -o $AVALANCHEGO_PLUGIN_DIR/srEXiWaHuhNyGwPUi444Tu47ZEDwxTWrbQiuD7FmgSAQ6X7Dy \ + "plugin/"*.go + + +export CHAIN_ID=99999 +echo "creating genesis" + +cp genesis.json $BASEDIR/genesis.json + +################################# +# download avalanche-network-runner +# https://github.com/ava-labs/avalanche-network-runner +ANR_REPO_PATH=github.com/ava-labs/avalanche-network-runner +# version set +go install -v ${ANR_REPO_PATH}@${ANR_VERSION} + +################################# +# run "avalanche-network-runner" server +GOPATH=$(go env GOPATH) +if [[ -z ${GOBIN+x} ]]; then + # no gobin set + BIN=${GOPATH}/bin/avalanche-network-runner +else + # gobin set + BIN=${GOBIN}/avalanche-network-runner +fi +echo "launch avalanche-network-runner in the background" +$BIN server \ + --log-level debug \ + --port=":12342" \ + --grpc-gateway-port=":12343" & +PID=${!} + +CHAIN_CONFIG_PATH=${BASEDIR}/chain_config.json + +cat <$CHAIN_CONFIG_PATH +{ + "local-txs-enabled": true, + "priority-regossip-frequency": "1s", + "tx-regossip-max-size": 32, + "priority-regossip-max-txs": 500, + "priority-regossip-txs-per-address": 200, + "priority-regossip-addresses": ["0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", "0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC"] +} +EOF + +$BIN control start \ + --log-level debug \ + --endpoint="0.0.0.0:12342" \ + --number-of-nodes=5 \ + --dial-timeout 30s \ + --avalanchego-path ${AVALANCHEGO_PATH} \ + --plugin-dir ${AVALANCHEGO_PLUGIN_DIR} \ + --blockchain-specs '[{"vm_name": "subnetevm", "genesis": "/tmp/subnet-evm-runner/genesis.json", "chain_config": "'$CHAIN_CONFIG_PATH'"}]' + # --blockchain-specs '[{"vm_name": "subnetevm", "genesis": "/tmp/subnet-evm.genesis.json", "chain_config": "'$CHAIN_CONFIG_PATH'", "network_upgrade": "'$NETWORK_UPGRADE_PATH'", "subnet_config": "'$SUBNET_CONFIG_PATH'"}]' + + + +echo "pkill -P ${PID} && kill -2 ${PID} && pkill -9 -f srEXiWaHuhNyGwPUi444Tu47ZEDwxTWrbQiuD7FmgSAQ6X7Dy" > kill.sh diff --git a/scripts/show_logs.sh b/scripts/show_logs.sh new file mode 100755 index 0000000000..a844323393 --- /dev/null +++ b/scripts/show_logs.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +set -e + +source ./scripts/utils.sh +showLogs "$1" "$2" diff --git a/scripts/upgrade_local.sh b/scripts/upgrade_local.sh new file mode 100755 index 0000000000..bfa56aeb7f --- /dev/null +++ b/scripts/upgrade_local.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -e +source ./scripts/utils.sh + +./scripts/build.sh custom_evm.bin + +avalanche network stop --snapshot-name snap1 + +avalanche subnet upgrade vm localnet --binary custom_evm.bin --local + +# utse tee to keep showing outut while storing in a var +OUTPUT=$(avalanche network start --avalanchego-version v1.11.2 --snapshot-name snap1 --config .avalanche-cli.json | tee /dev/fd/2) + +setStatus diff --git a/scripts/utils.sh b/scripts/utils.sh new file mode 100644 index 0000000000..8bcf1fd722 --- /dev/null +++ b/scripts/utils.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash + +set -e + +function setStatus() { + cat <local_status.sh +export CHAIN_ID=$(echo "$OUTPUT" | awk -F'|' '/node1/{print $4}' | awk -F'/' '{print $6}') +export LOGS_PATH="$(echo "$OUTPUT" | awk -F': ' '/Node logs directory: /{print $2}')" +EOF + + cat <~/.hubblenet.json +{ + "chain_id": "$(echo "$OUTPUT" | awk -F'|' '/node1/{print $4}' | awk -F'/' '{print $6}')" +} +EOF +} + +function showLogs() { + if ! command -v multitail &>/dev/null; then + echo "multitail could not be found; please install using 'brew install multitail'" + exit + fi + + source local_status.sh + if [ -z "$1" ]; then + # Define colors and nodes + + colors=("magenta" "green" "white" "yellow" "cyan") + nodes=("1" "2" "3" "4" "5") + + # Use for loop to iterate through nodes + for index in ${!nodes[*]} + do + node=${nodes[$index]} + color=${colors[$index]} + logs_path=$(echo $LOGS_PATH | sed -e "s//$node/g") + # Add multitail command for each node + cmd_part+=" -ci $color --label \"[node$node]\" -I ${logs_path}/$CHAIN_ID.log" + done + + # Execute multitail with the generated command parts + eval "multitail -D $cmd_part" + + else + if [ -z "$2" ]; then + # from the beginning + tail -f -n +1 "${LOGS_PATH//$1}/$CHAIN_ID.log" + else + grep --color=auto -i "$2" "${LOGS_PATH//$1}/$CHAIN_ID.log" + fi + fi + +} diff --git a/subnet.json b/subnet.json new file mode 100644 index 0000000000..c1f28a5a9b --- /dev/null +++ b/subnet.json @@ -0,0 +1,3 @@ +{ + "proposerMinBlockDelay": 200000000 +} diff --git a/tests/orderbook/abi/AMM.json b/tests/orderbook/abi/AMM.json new file mode 100644 index 0000000000..879faa24dd --- /dev/null +++ b/tests/orderbook/abi/AMM.json @@ -0,0 +1,1094 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "_clearingHouse", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "version", + "type": "uint8" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "asks", + "outputs": [ + { + "internalType": "uint256", + "name": "nextTick", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "asksHead", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "bids", + "outputs": [ + { + "internalType": "uint256", + "name": "nextTick", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "bidsHead", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "clearingHouse", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "cumulativePremiumFraction", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "fundingPeriod", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getAcceptableBounds", + "outputs": [ + { + "internalType": "uint256", + "name": "upperBound", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "lowerBound", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getAcceptableBoundsForLiquidation", + "outputs": [ + { + "internalType": "uint256", + "name": "upperBound", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "lowerBound", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + } + ], + "name": "getNotionalPositionAndUnrealizedPnl", + "outputs": [ + { + "internalType": "uint256", + "name": "notionalPosition", + "type": "uint256" + }, + { + "internalType": "int256", + "name": "unrealizedPnl", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "int256", + "name": "positionSize", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "openNotional", + "type": "uint256" + }, + { + "internalType": "int256", + "name": "unrealizedPnl", + "type": "int256" + }, + { + "internalType": "int256", + "name": "baseAssetQuantity", + "type": "int256" + } + ], + "name": "getOpenNotionalWhileReducingPosition", + "outputs": [ + { + "internalType": "uint256", + "name": "remainOpenNotional", + "type": "uint256" + }, + { + "internalType": "int256", + "name": "realizedPnl", + "type": "int256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "int256", + "name": "margin", + "type": "int256" + }, + { + "internalType": "enum IClearingHouse.Mode", + "name": "mode", + "type": "uint8" + } + ], + "name": "getOptimalPnl", + "outputs": [ + { + "internalType": "uint256", + "name": "notionalPosition", + "type": "uint256" + }, + { + "internalType": "int256", + "name": "unrealizedPnl", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + } + ], + "name": "getPendingFundingPayment", + "outputs": [ + { + "internalType": "int256", + "name": "takerFundingPayment", + "type": "int256" + }, + { + "internalType": "int256", + "name": "latestCumulativePremiumFraction", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "openNotional", + "type": "uint256" + }, + { + "internalType": "int256", + "name": "size", + "type": "int256" + }, + { + "internalType": "int256", + "name": "margin", + "type": "int256" + } + ], + "name": "getPositionMetadata", + "outputs": [ + { + "internalType": "uint256", + "name": "notionalPos", + "type": "uint256" + }, + { + "internalType": "int256", + "name": "uPnl", + "type": "int256" + }, + { + "internalType": "int256", + "name": "marginFraction", + "type": "int256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "getUnderlyingPrice", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "governance", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "impactMarginNotional", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "_name", + "type": "string" + }, + { + "internalType": "address", + "name": "_underlyingAsset", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_minSizeRequirement", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_governance", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_pricePrecision", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_ticks", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "interestRate", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "lastPrice", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "int256", + "name": "fillAmount", + "type": "int256" + } + ], + "name": "liquidatePosition", + "outputs": [ + { + "internalType": "int256", + "name": "realizedPnl", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "quoteAsset", + "type": "uint256" + }, + { + "internalType": "int256", + "name": "size", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "openNotional", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "longOpenInterestNotional", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "marginAccount", + "outputs": [ + { + "internalType": "contract IMarginAccount", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "maxFundingRate", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "maxLiquidationPriceSpread", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "maxLiquidationRatio", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "maxOracleSpreadRatio", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "midPrice", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "minSizeRequirement", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "multiplier", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "nextFundingTime", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "openInterestNotional", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "int256", + "name": "fillAmount", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "fulfillPrice", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "is2ndTrade", + "type": "bool" + } + ], + "name": "openPosition", + "outputs": [ + { + "internalType": "int256", + "name": "realizedPnl", + "type": "int256" + }, + { + "internalType": "bool", + "name": "isPositionIncreased", + "type": "bool" + }, + { + "internalType": "int256", + "name": "size", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "openNotional", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "openInterest", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "oracle", + "outputs": [ + { + "internalType": "contract IOracle", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "piData", + "outputs": [ + { + "internalType": "int256", + "name": "piTwap", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "accTime", + "type": "uint256" + }, + { + "internalType": "int256", + "name": "piLast", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "lastTS", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "positions", + "outputs": [ + { + "internalType": "int256", + "name": "size", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "openNotional", + "type": "uint256" + }, + { + "internalType": "int256", + "name": "lastPremiumFraction", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "liquidationThreshold", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "samplePI", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + }, + { + "internalType": "int256", + "name": "premiumIndex", + "type": "int256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_fundingPeriod", + "type": "uint256" + }, + { + "internalType": "int256", + "name": "_maxFundingRate", + "type": "int256" + } + ], + "name": "setFundingParams", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "__governance", + "type": "address" + } + ], + "name": "setGovernace", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_impactMarginNotional", + "type": "uint256" + } + ], + "name": "setImpactMarginNotional", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "int256", + "name": "_interestRate", + "type": "int256" + } + ], + "name": "setInterestRate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_maxLiquidationRatio", + "type": "uint256" + } + ], + "name": "setLiquidationSizeRatio", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_minSizeRequirement", + "type": "uint256" + } + ], + "name": "setMinSizeRequirement", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_pricePrecision", + "type": "uint256" + } + ], + "name": "setPricePrecision", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_maxOracleSpreadRatio", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_maxLiquidationPriceSpread", + "type": "uint256" + } + ], + "name": "setPriceSpreadParams", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_ticks", + "type": "address" + } + ], + "name": "setTicks", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "settleFunding", + "outputs": [ + { + "internalType": "int256", + "name": "premiumFraction", + "type": "int256" + }, + { + "internalType": "int256", + "name": "underlyingPrice", + "type": "int256" + }, + { + "internalType": "int256", + "name": "", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "shortOpenInterestNotional", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tick", + "type": "uint256" + }, + { + "internalType": "int256", + "name": "quantity", + "type": "int256" + } + ], + "name": "signalAddLiquidity", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tick", + "type": "uint256" + }, + { + "internalType": "int256", + "name": "quantity", + "type": "int256" + } + ], + "name": "signalRemoveLiquidity", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "startFunding", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "ticks", + "outputs": [ + { + "internalType": "contract ITicks", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_orderBook", + "type": "address" + } + ], + "name": "toggleWhitelistedOrderBook", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "underlyingAsset", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + } + ], + "name": "updatePosition", + "outputs": [ + { + "internalType": "bool", + "name": "isUpdated", + "type": "bool" + }, + { + "internalType": "int256", + "name": "fundingPayment", + "type": "int256" + }, + { + "internalType": "int256", + "name": "latestCumulativePremiumFraction", + "type": "int256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "whitelistedOrderBooks", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/tests/orderbook/abi/ClearingHouse.json b/tests/orderbook/abi/ClearingHouse.json new file mode 100644 index 0000000000..fa1503e755 --- /dev/null +++ b/tests/orderbook/abi/ClearingHouse.json @@ -0,0 +1,1264 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "idx", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "takerFundingPayment", + "type": "int256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "cumulativePremiumFraction", + "type": "int256" + } + ], + "name": "FundingPaid", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "idx", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "premiumFraction", + "type": "int256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "underlyingPrice", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "cumulativePremiumFraction", + "type": "int256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "nextFundingTime", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + } + ], + "name": "FundingRateUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "idx", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "amm", + "type": "address" + } + ], + "name": "MarketAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "nextSampleTime", + "type": "uint256" + } + ], + "name": "NotifyNextPISample", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "idx", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + } + ], + "name": "PISampleSkipped", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "idx", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "premiumIndex", + "type": "int256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + } + ], + "name": "PISampledUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "idx", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "baseAsset", + "type": "int256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "realizedPnl", + "type": "int256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "size", + "type": "int256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "openNotional", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "fee", + "type": "int256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "name": "PositionLiquidated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "idx", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "baseAsset", + "type": "int256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "realizedPnl", + "type": "int256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "size", + "type": "int256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "openNotional", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "fee", + "type": "int256" + }, + { + "indexed": false, + "internalType": "enum IClearingHouse.OrderExecutionMode", + "name": "mode", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "name": "PositionModified", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "referrer", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "referralBonus", + "type": "uint256" + } + ], + "name": "ReferralBonusAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "inputs": [], + "name": "LIQUIDATION_FAILED", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "amms", + "outputs": [ + { + "internalType": "contract IAMM", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + } + ], + "name": "assertMarginRequirement", + "outputs": [], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "bool", + "name": "includeFundingPayments", + "type": "bool" + }, + { + "internalType": "enum IClearingHouse.Mode", + "name": "mode", + "type": "uint8" + } + ], + "name": "calcMarginFraction", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "defaultOrderBook", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "feeSink", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getAMMs", + "outputs": [ + { + "internalType": "contract IAMM[]", + "name": "", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getAmmsLength", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "bool", + "name": "includeFundingPayments", + "type": "bool" + }, + { + "internalType": "enum IClearingHouse.Mode", + "name": "mode", + "type": "uint8" + } + ], + "name": "getNotionalPositionAndMargin", + "outputs": [ + { + "internalType": "uint256", + "name": "notionalPosition", + "type": "uint256" + }, + { + "internalType": "int256", + "name": "margin", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "bool", + "name": "includeFundingPayments", + "type": "bool" + }, + { + "internalType": "enum IClearingHouse.Mode", + "name": "mode", + "type": "uint8" + } + ], + "name": "getNotionalPositionAndMarginVanilla", + "outputs": [ + { + "internalType": "uint256", + "name": "notionalPosition", + "type": "uint256" + }, + { + "internalType": "int256", + "name": "margin", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + } + ], + "name": "getTotalFunding", + "outputs": [ + { + "internalType": "int256", + "name": "totalFunding", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "int256", + "name": "margin", + "type": "int256" + }, + { + "internalType": "enum IClearingHouse.Mode", + "name": "mode", + "type": "uint8" + } + ], + "name": "getTotalNotionalPositionAndUnrealizedPnl", + "outputs": [ + { + "internalType": "uint256", + "name": "notionalPosition", + "type": "uint256" + }, + { + "internalType": "int256", + "name": "unrealizedPnl", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getUnderlyingPrice", + "outputs": [ + { + "internalType": "uint256[]", + "name": "prices", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "governance", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "hubbleReferral", + "outputs": [ + { + "internalType": "contract IHubbleReferral", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_governance", + "type": "address" + }, + { + "internalType": "address", + "name": "_feeSink", + "type": "address" + }, + { + "internalType": "address", + "name": "_marginAccount", + "type": "address" + }, + { + "internalType": "address", + "name": "_defaultOrderBook", + "type": "address" + }, + { + "internalType": "address", + "name": "_vusd", + "type": "address" + }, + { + "internalType": "address", + "name": "_hubbleReferral", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + } + ], + "name": "isAboveMaintenanceMargin", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "isWhitelistedOrderBook", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "juror", + "outputs": [ + { + "internalType": "contract IJuror", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "lastFundingPaid", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "lastFundingTime", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "internalType": "enum IClearingHouse.OrderExecutionMode", + "name": "mode", + "type": "uint8" + } + ], + "internalType": "struct IClearingHouse.Instruction", + "name": "instruction", + "type": "tuple" + }, + { + "internalType": "int256", + "name": "liquidationAmount", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + } + ], + "name": "liquidate", + "outputs": [ + { + "internalType": "uint256", + "name": "openInterest", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "int256", + "name": "toLiquidate", + "type": "int256" + } + ], + "name": "liquidateSingleAmm", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "liquidationPenalty", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "maintenanceMargin", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "makerFee", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "marginAccount", + "outputs": [ + { + "internalType": "contract IMarginAccount", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "minAllowableMargin", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "nextSampleTime", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "internalType": "enum IClearingHouse.OrderExecutionMode", + "name": "mode", + "type": "uint8" + } + ], + "internalType": "struct IClearingHouse.Instruction[2]", + "name": "orders", + "type": "tuple[2]" + }, + { + "internalType": "int256", + "name": "fillAmount", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "fulfillPrice", + "type": "uint256" + } + ], + "name": "openComplementaryPositions", + "outputs": [ + { + "internalType": "uint256", + "name": "openInterest", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "internalType": "enum IClearingHouse.OrderExecutionMode", + "name": "mode", + "type": "uint8" + } + ], + "internalType": "struct IClearingHouse.Instruction", + "name": "order", + "type": "tuple" + }, + { + "internalType": "int256", + "name": "fillAmount", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "fulfillPrice", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "is2ndTrade", + "type": "bool" + } + ], + "name": "openPosition", + "outputs": [ + { + "internalType": "uint256", + "name": "openInterest", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "orderBook", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "paused", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "referralShare", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "samplePI", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_feeSink", + "type": "address" + } + ], + "name": "setFeeSink", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "__governance", + "type": "address" + } + ], + "name": "setGovernace", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_juror", + "type": "address" + } + ], + "name": "setJuror", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_orderBook", + "type": "address" + }, + { + "internalType": "bool", + "name": "_status", + "type": "bool" + } + ], + "name": "setOrderBookWhitelist", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "int256", + "name": "_maintenanceMargin", + "type": "int256" + }, + { + "internalType": "int256", + "name": "_minAllowableMargin", + "type": "int256" + }, + { + "internalType": "int256", + "name": "_takerFee", + "type": "int256" + }, + { + "internalType": "int256", + "name": "_makerFee", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "_referralShare", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_tradingFeeDiscount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_liquidationPenalty", + "type": "uint256" + } + ], + "name": "setParams", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_referral", + "type": "address" + } + ], + "name": "setReferral", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "settleFunding", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "takerFee", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "tradingFeeDiscount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + } + ], + "name": "updatePositions", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "vusd", + "outputs": [ + { + "internalType": "contract VUSD", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_amm", + "type": "address" + } + ], + "name": "whitelistAmm", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/tests/orderbook/abi/IHubbleReferral.json b/tests/orderbook/abi/IHubbleReferral.json new file mode 100644 index 0000000000..d28de711cd --- /dev/null +++ b/tests/orderbook/abi/IHubbleReferral.json @@ -0,0 +1,40 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + } + ], + "name": "hasReferrer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + } + ], + "name": "traderToReferrer", + "outputs": [ + { + "internalType": "address", + "name": "referrer", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/tests/orderbook/abi/IOC.json b/tests/orderbook/abi/IOC.json new file mode 100644 index 0000000000..d3e208233e --- /dev/null +++ b/tests/orderbook/abi/IOC.json @@ -0,0 +1,511 @@ +[ + { + "anonymous": false, + "inputs": [], + "name": "EIP712DomainChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "version", + "type": "uint8" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "name": "OrderCancelled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "uint8", + "name": "orderType", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "expireAt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "int256", + "name": "baseAssetQuantity", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "salt", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "reduceOnly", + "type": "bool" + } + ], + "indexed": false, + "internalType": "struct IImmediateOrCancelOrders.Order", + "name": "order", + "type": "tuple" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "name": "OrderPlaced", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "inputs": [], + "name": "defaultOrderBook", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "eip712Domain", + "outputs": [ + { + "internalType": "bytes1", + "name": "fields", + "type": "bytes1" + }, + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "version", + "type": "string" + }, + { + "internalType": "uint256", + "name": "chainId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "verifyingContract", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "salt", + "type": "bytes32" + }, + { + "internalType": "uint256[]", + "name": "extensions", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "expirationCap", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint8", + "name": "orderType", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "expireAt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "int256", + "name": "baseAssetQuantity", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "salt", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "reduceOnly", + "type": "bool" + } + ], + "internalType": "struct IImmediateOrCancelOrders.Order", + "name": "order", + "type": "tuple" + } + ], + "name": "getOrderHash", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "governance", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_governance", + "type": "address" + }, + { + "internalType": "address", + "name": "_defaultOrderBook", + "type": "address" + }, + { + "internalType": "address", + "name": "_juror", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "juror", + "outputs": [ + { + "internalType": "contract IJuror", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + } + ], + "name": "orderStatus", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "blockPlaced", + "type": "uint256" + }, + { + "internalType": "int256", + "name": "filledAmount", + "type": "int256" + }, + { + "internalType": "enum IOrderHandler.OrderStatus", + "name": "status", + "type": "uint8" + } + ], + "internalType": "struct IImmediateOrCancelOrders.OrderInfo", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "paused", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint8", + "name": "orderType", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "expireAt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "int256", + "name": "baseAssetQuantity", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "salt", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "reduceOnly", + "type": "bool" + } + ], + "internalType": "struct IImmediateOrCancelOrders.Order[]", + "name": "orders", + "type": "tuple[]" + } + ], + "name": "placeOrders", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "referral", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_expirationCap", + "type": "uint256" + } + ], + "name": "setExpirationCap", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "__governance", + "type": "address" + } + ], + "name": "setGovernace", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_juror", + "type": "address" + } + ], + "name": "setJuror", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_referral", + "type": "address" + } + ], + "name": "setReferral", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "encodedOrder", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "metadata", + "type": "bytes" + } + ], + "name": "updateOrder", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/tests/orderbook/abi/Juror.json b/tests/orderbook/abi/Juror.json new file mode 100644 index 0000000000..2f773c3a93 --- /dev/null +++ b/tests/orderbook/abi/Juror.json @@ -0,0 +1,986 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "_clearingHouse", + "type": "address" + }, + { + "internalType": "address", + "name": "_defaultOrderBook", + "type": "address" + }, + { + "internalType": "address", + "name": "_governance", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "clearingHouse", + "outputs": [ + { + "internalType": "contract IClearingHouse", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint8", + "name": "orderType", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "expireAt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "int256", + "name": "baseAssetQuantity", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "salt", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "reduceOnly", + "type": "bool" + } + ], + "internalType": "struct IImmediateOrCancelOrders.Order", + "name": "order", + "type": "tuple" + } + ], + "name": "getIOCOrderHash", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "bool", + "name": "includeFundingPayments", + "type": "bool" + }, + { + "internalType": "uint8", + "name": "mode", + "type": "uint8" + } + ], + "name": "getNotionalPositionAndMargin", + "outputs": [ + { + "internalType": "uint256", + "name": "notionalPosition", + "type": "uint256" + }, + { + "internalType": "int256", + "name": "margin", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "int256", + "name": "baseAssetQuantity", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "upperBound", + "type": "uint256" + } + ], + "name": "getRequiredMargin", + "outputs": [ + { + "internalType": "uint256", + "name": "requiredMargin", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "governance", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "iocOrderBook", + "outputs": [ + { + "internalType": "contract ImmediateOrCancelOrders", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "isTradingAuthority", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "limitOrderBook", + "outputs": [ + { + "internalType": "contract LimitOrderBook", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "orderBook", + "outputs": [ + { + "internalType": "contract OrderBook", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "referral", + "outputs": [ + { + "internalType": "contract IHubbleReferral", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "__governance", + "type": "address" + } + ], + "name": "setGovernace", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_iocOrderBook", + "type": "address" + } + ], + "name": "setIOCOrderBook", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_limitOrderBook", + "type": "address" + } + ], + "name": "setLimitOrderBook", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_referral", + "type": "address" + } + ], + "name": "setReferral", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "int256", + "name": "baseAssetQuantity", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "salt", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "reduceOnly", + "type": "bool" + }, + { + "internalType": "bool", + "name": "postOnly", + "type": "bool" + } + ], + "internalType": "struct ILimitOrderBook.Order", + "name": "order", + "type": "tuple" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "bool", + "name": "assertLowMargin", + "type": "bool" + } + ], + "name": "validateCancelLimitOrder", + "outputs": [ + { + "internalType": "string", + "name": "err", + "type": "string" + }, + { + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "int256", + "name": "unfilledAmount", + "type": "int256" + }, + { + "internalType": "address", + "name": "amm", + "type": "address" + } + ], + "internalType": "struct IOrderHandler.CancelOrderRes", + "name": "res", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint8", + "name": "orderType", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "expireAt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "int256", + "name": "baseAssetQuantity", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "salt", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "reduceOnly", + "type": "bool" + } + ], + "internalType": "struct IImmediateOrCancelOrders.Order", + "name": "order", + "type": "tuple" + }, + { + "internalType": "enum Juror.Side", + "name": "side", + "type": "uint8" + }, + { + "internalType": "int256", + "name": "fillAmount", + "type": "int256" + } + ], + "name": "validateExecuteIOCOrder", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "int256", + "name": "baseAssetQuantity", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "blockPlaced", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "internalType": "string", + "name": "err", + "type": "string" + } + ], + "internalType": "struct Juror.Metadata", + "name": "metadata", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "int256", + "name": "baseAssetQuantity", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "salt", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "reduceOnly", + "type": "bool" + }, + { + "internalType": "bool", + "name": "postOnly", + "type": "bool" + } + ], + "internalType": "struct ILimitOrderBook.Order", + "name": "order", + "type": "tuple" + }, + { + "internalType": "enum Juror.Side", + "name": "side", + "type": "uint8" + }, + { + "internalType": "int256", + "name": "fillAmount", + "type": "int256" + } + ], + "name": "validateExecuteLimitOrder", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "int256", + "name": "baseAssetQuantity", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "blockPlaced", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "internalType": "string", + "name": "err", + "type": "string" + } + ], + "internalType": "struct Juror.Metadata", + "name": "metadata", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "liquidationAmount", + "type": "uint256" + } + ], + "name": "validateLiquidationOrderAndDetermineFillPrice", + "outputs": [ + { + "internalType": "string", + "name": "err", + "type": "string" + }, + { + "internalType": "enum IJuror.BadElement", + "name": "element", + "type": "uint8" + }, + { + "components": [ + { + "components": [ + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "internalType": "enum IClearingHouse.OrderExecutionMode", + "name": "mode", + "type": "uint8" + } + ], + "internalType": "struct IClearingHouse.Instruction", + "name": "instruction", + "type": "tuple" + }, + { + "internalType": "uint8", + "name": "orderType", + "type": "uint8" + }, + { + "internalType": "bytes", + "name": "encodedOrder", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "fillPrice", + "type": "uint256" + }, + { + "internalType": "int256", + "name": "fillAmount", + "type": "int256" + } + ], + "internalType": "struct IOrderHandler.LiquidationMatchingValidationRes", + "name": "res", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "orderType", + "type": "uint8" + }, + { + "internalType": "bytes", + "name": "orderData", + "type": "bytes" + }, + { + "internalType": "enum Juror.Side", + "name": "side", + "type": "uint8" + }, + { + "internalType": "int256", + "name": "fillAmount", + "type": "int256" + } + ], + "name": "validateOrder", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "int256", + "name": "baseAssetQuantity", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "blockPlaced", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "internalType": "string", + "name": "err", + "type": "string" + } + ], + "internalType": "struct Juror.Metadata", + "name": "metadata", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes[2]", + "name": "data", + "type": "bytes[2]" + }, + { + "internalType": "int256", + "name": "fillAmount", + "type": "int256" + } + ], + "name": "validateOrdersAndDetermineFillPrice", + "outputs": [ + { + "internalType": "string", + "name": "err", + "type": "string" + }, + { + "internalType": "enum IJuror.BadElement", + "name": "element", + "type": "uint8" + }, + { + "components": [ + { + "components": [ + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "internalType": "enum IClearingHouse.OrderExecutionMode", + "name": "mode", + "type": "uint8" + } + ], + "internalType": "struct IClearingHouse.Instruction[2]", + "name": "instructions", + "type": "tuple[2]" + }, + { + "internalType": "uint8[2]", + "name": "orderTypes", + "type": "uint8[2]" + }, + { + "internalType": "bytes[2]", + "name": "encodedOrders", + "type": "bytes[2]" + }, + { + "internalType": "uint256", + "name": "fillPrice", + "type": "uint256" + } + ], + "internalType": "struct IOrderHandler.MatchingValidationRes", + "name": "res", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint8", + "name": "orderType", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "expireAt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "int256", + "name": "baseAssetQuantity", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "salt", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "reduceOnly", + "type": "bool" + } + ], + "internalType": "struct IImmediateOrCancelOrders.Order", + "name": "order", + "type": "tuple" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "validatePlaceIOCOrder", + "outputs": [ + { + "internalType": "string", + "name": "err", + "type": "string" + }, + { + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "int256", + "name": "baseAssetQuantity", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "salt", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "reduceOnly", + "type": "bool" + }, + { + "internalType": "bool", + "name": "postOnly", + "type": "bool" + } + ], + "internalType": "struct ILimitOrderBook.Order", + "name": "order", + "type": "tuple" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "validatePlaceLimitOrder", + "outputs": [ + { + "internalType": "string", + "name": "err", + "type": "string" + }, + { + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "reserveAmount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "amm", + "type": "address" + } + ], + "internalType": "struct IOrderHandler.PlaceOrderRes", + "name": "res", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/tests/orderbook/abi/LimitOrderBook.json b/tests/orderbook/abi/LimitOrderBook.json new file mode 100644 index 0000000000..72f59fded3 --- /dev/null +++ b/tests/orderbook/abi/LimitOrderBook.json @@ -0,0 +1,784 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "_clearingHouse", + "type": "address" + }, + { + "internalType": "address", + "name": "_marginAccount", + "type": "address" + }, + { + "internalType": "address", + "name": "_trustedForwarder", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "int256", + "name": "baseAssetQuantity", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "salt", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "reduceOnly", + "type": "bool" + }, + { + "internalType": "bool", + "name": "postOnly", + "type": "bool" + } + ], + "indexed": false, + "internalType": "struct ILimitOrderBook.Order", + "name": "order", + "type": "tuple" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "name": "OrderAccepted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bool", + "name": "isAutoCancelled", + "type": "bool" + } + ], + "name": "OrderCancelAccepted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "string", + "name": "err", + "type": "string" + } + ], + "name": "OrderCancelRejected", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "int256", + "name": "baseAssetQuantity", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "salt", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "reduceOnly", + "type": "bool" + }, + { + "internalType": "bool", + "name": "postOnly", + "type": "bool" + } + ], + "indexed": false, + "internalType": "struct ILimitOrderBook.Order", + "name": "order", + "type": "tuple" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "string", + "name": "err", + "type": "string" + } + ], + "name": "OrderRejected", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "int256", + "name": "baseAssetQuantity", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "salt", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "reduceOnly", + "type": "bool" + }, + { + "internalType": "bool", + "name": "postOnly", + "type": "bool" + } + ], + "internalType": "struct ILimitOrderBook.Order[]", + "name": "orders", + "type": "tuple[]" + } + ], + "name": "cancelOrders", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "int256", + "name": "baseAssetQuantity", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "salt", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "reduceOnly", + "type": "bool" + }, + { + "internalType": "bool", + "name": "postOnly", + "type": "bool" + } + ], + "internalType": "struct ILimitOrderBook.Order[]", + "name": "orders", + "type": "tuple[]" + } + ], + "name": "cancelOrdersWithLowMargin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "clearingHouse", + "outputs": [ + { + "internalType": "contract IClearingHouse", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "defaultOrderBook", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "int256", + "name": "baseAssetQuantity", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "salt", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "reduceOnly", + "type": "bool" + }, + { + "internalType": "bool", + "name": "postOnly", + "type": "bool" + } + ], + "internalType": "struct ILimitOrderBook.Order", + "name": "order", + "type": "tuple" + } + ], + "name": "getOrderHash", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "governance", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_governance", + "type": "address" + }, + { + "internalType": "address", + "name": "_defaultOrderBook", + "type": "address" + }, + { + "internalType": "address", + "name": "_juror", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "forwarder", + "type": "address" + } + ], + "name": "isTrustedForwarder", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "juror", + "outputs": [ + { + "internalType": "contract IJuror", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "longOpenOrdersAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "marginAccount", + "outputs": [ + { + "internalType": "contract IMarginAccount", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "orderInfo", + "outputs": [ + { + "internalType": "uint256", + "name": "blockPlaced", + "type": "uint256" + }, + { + "internalType": "int256", + "name": "filledAmount", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "reservedMargin", + "type": "uint256" + }, + { + "internalType": "enum IOrderHandler.OrderStatus", + "name": "status", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + } + ], + "name": "orderStatus", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "blockPlaced", + "type": "uint256" + }, + { + "internalType": "int256", + "name": "filledAmount", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "reservedMargin", + "type": "uint256" + }, + { + "internalType": "enum IOrderHandler.OrderStatus", + "name": "status", + "type": "uint8" + } + ], + "internalType": "struct ILimitOrderBook.OrderInfo", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "paused", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "ammIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "int256", + "name": "baseAssetQuantity", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "salt", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "reduceOnly", + "type": "bool" + }, + { + "internalType": "bool", + "name": "postOnly", + "type": "bool" + } + ], + "internalType": "struct ILimitOrderBook.Order[]", + "name": "orders", + "type": "tuple[]" + } + ], + "name": "placeOrders", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "reduceOnlyAmount", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "__governance", + "type": "address" + } + ], + "name": "setGovernace", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_juror", + "type": "address" + } + ], + "name": "setJuror", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "shortOpenOrdersAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "encodedOrder", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "metadata", + "type": "bytes" + } + ], + "name": "updateOrder", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/tests/orderbook/abi/MarginAccount.json b/tests/orderbook/abi/MarginAccount.json new file mode 100644 index 0000000000..deda936e25 --- /dev/null +++ b/tests/orderbook/abi/MarginAccount.json @@ -0,0 +1,1119 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "_trustedForwarder", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "enum IMarginAccount.LiquidationStatus", + "name": "", + "type": "uint8" + } + ], + "name": "NOT_LIQUIDATABLE", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "version", + "type": "uint8" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "idx", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "seizeAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "repayAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "name": "MarginAccountLiquidated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "idx", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "name": "MarginAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "MarginReleased", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "idx", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "name": "MarginRemoved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "MarginReserved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "indexed": false, + "internalType": "int256", + "name": "realizedPnl", + "type": "int256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "name": "PnLRealized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "seized", + "type": "uint256[]" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "repayAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "name": "SettledBadDebt", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "idx", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "addMargin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "idx", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "addMarginFor", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "idx", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_weight", + "type": "uint256" + } + ], + "name": "changeCollateralWeight", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "clearingHouse", + "outputs": [ + { + "internalType": "contract IClearingHouse", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "credit", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + } + ], + "name": "getAvailableMargin", + "outputs": [ + { + "internalType": "int256", + "name": "availableMargin", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "idx", + "type": "uint256" + } + ], + "name": "getCollateralToken", + "outputs": [ + { + "internalType": "contract IERC20", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + } + ], + "name": "getNormalizedMargin", + "outputs": [ + { + "internalType": "int256", + "name": "weighted", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + } + ], + "name": "getSpotCollateralValue", + "outputs": [ + { + "internalType": "int256", + "name": "spot", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "governance", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_governance", + "type": "address" + }, + { + "internalType": "address", + "name": "_vusd", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "insuranceFund", + "outputs": [ + { + "internalType": "contract IInsuranceFund", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "bool", + "name": "includeFunding", + "type": "bool" + } + ], + "name": "isLiquidatable", + "outputs": [ + { + "internalType": "enum IMarginAccount.LiquidationStatus", + "name": "_isLiquidatable", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "repayAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "incentivePerDollar", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "forwarder", + "type": "address" + } + ], + "name": "isTrustedForwarder", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "juror", + "outputs": [ + { + "internalType": "contract IJuror", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "uint256", + "name": "repay", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "idx", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "minSeizeAmount", + "type": "uint256" + } + ], + "name": "liquidateExactRepay", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "uint256", + "name": "maxRepay", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "idx", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "seize", + "type": "uint256" + } + ], + "name": "liquidateExactSeize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "uint256", + "name": "maxRepay", + "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "idxs", + "type": "uint256[]" + } + ], + "name": "liquidateFlexible", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "liquidationIncentive", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "margin", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "marginAccountHelper", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "minAllowableMargin", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "oracle", + "outputs": [ + { + "internalType": "contract IOracle", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "paused", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "int256", + "name": "realizedPnl", + "type": "int256" + } + ], + "name": "realizePnL", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "releaseMargin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "idx", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "removeMargin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "idx", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "trader", + "type": "address" + } + ], + "name": "removeMarginFor", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "reserveMargin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "reservedMargin", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "__governance", + "type": "address" + } + ], + "name": "setGovernace", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_juror", + "type": "address" + } + ], + "name": "setJuror", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IOracle", + "name": "_oracle", + "type": "address" + } + ], + "name": "setOracle", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + } + ], + "name": "settleBadDebt", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "supportedAssets", + "outputs": [ + { + "components": [ + { + "internalType": "contract IERC20", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "weight", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "decimals", + "type": "uint8" + } + ], + "internalType": "struct IMarginAccount.Collateral[]", + "name": "", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "supportedAssetsLen", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "supportedCollateral", + "outputs": [ + { + "internalType": "contract IERC20", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "weight", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "decimals", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_registry", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_liquidationIncentive", + "type": "uint256" + } + ], + "name": "syncDeps", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_settler", + "type": "address" + } + ], + "name": "toggleTrustedSettler", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_orderBook", + "type": "address" + } + ], + "name": "toggleWhitelistedOrderBook", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferOutVusd", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "trustedSettlers", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_minAllowableMargin", + "type": "uint256" + } + ], + "name": "updateParams", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "vusd", + "outputs": [ + { + "internalType": "contract IERC20FlexibleSupply", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + } + ], + "name": "weightedAndSpotCollateral", + "outputs": [ + { + "internalType": "int256", + "name": "weighted", + "type": "int256" + }, + { + "internalType": "int256", + "name": "spot", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_coin", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_weight", + "type": "uint256" + } + ], + "name": "whitelistCollateral", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "whitelistedOrderBooks", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } +] diff --git a/tests/orderbook/abi/MarginAccountHelper.json b/tests/orderbook/abi/MarginAccountHelper.json new file mode 100644 index 0000000000..ffa769d5b9 --- /dev/null +++ b/tests/orderbook/abi/MarginAccountHelper.json @@ -0,0 +1,372 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "addVUSDMarginWithReserve", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "approveToken", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "depositToInsuranceFund", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "governance", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "hgt", + "outputs": [ + { + "internalType": "contract IHGT", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_governance", + "type": "address" + }, + { + "internalType": "address", + "name": "_vusd", + "type": "address" + }, + { + "internalType": "address", + "name": "_marginAccount", + "type": "address" + }, + { + "internalType": "address", + "name": "_insuranceFund", + "type": "address" + }, + { + "internalType": "address", + "name": "_hgt", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "insuranceFund", + "outputs": [ + { + "internalType": "contract IInsuranceFund", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "marginAccount", + "outputs": [ + { + "internalType": "contract IMarginAccount", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "paused", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "removeMarginInUSD", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "__governance", + "type": "address" + } + ], + "name": "setGovernace", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_hgt", + "type": "address" + } + ], + "name": "setHGT", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_registry", + "type": "address" + } + ], + "name": "syncDeps", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "vusd", + "outputs": [ + { + "internalType": "contract IVUSD", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "shares", + "type": "uint256" + } + ], + "name": "withdrawFromInsuranceFund", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "shares", + "type": "uint256" + }, + { + "internalType": "uint16", + "name": "dstChainId", + "type": "uint16" + }, + { + "internalType": "uint16", + "name": "secondHopChainId", + "type": "uint16" + }, + { + "internalType": "uint256", + "name": "amountMin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "dstPoolId", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "adapterParams", + "type": "bytes" + } + ], + "name": "withdrawFromInsuranceFundToChain", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "tokenIdx", + "type": "uint256" + }, + { + "internalType": "uint16", + "name": "dstChainId", + "type": "uint16" + }, + { + "internalType": "uint16", + "name": "secondHopChainId", + "type": "uint16" + }, + { + "internalType": "uint256", + "name": "amountMin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "dstPoolId", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "adapterParams", + "type": "bytes" + } + ], + "name": "withdrawMarginToChain", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } +] diff --git a/tests/orderbook/abi/Oracle.json b/tests/orderbook/abi/Oracle.json new file mode 100644 index 0000000000..f51ba1e0e9 --- /dev/null +++ b/tests/orderbook/abi/Oracle.json @@ -0,0 +1,146 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "_governance", + "type": "address" + }, + { + "internalType": "address", + "name": "_redStoneAdapter", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "aggregatorMap", + "outputs": [ + { + "internalType": "bytes32", + "name": "redstoneFeedId", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "aggregator", + "type": "address" + }, + { + "internalType": "uint256", + "name": "heartbeat", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "underlying", + "type": "address" + } + ], + "name": "getUnderlyingPrice", + "outputs": [ + { + "internalType": "int256", + "name": "answer", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "governance", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "redStoneAdapter", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "underlying", + "type": "address" + }, + { + "internalType": "address", + "name": "aggregator", + "type": "address" + }, + { + "internalType": "uint256", + "name": "heartbeat", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "redstoneFeedId", + "type": "bytes32" + } + ], + "name": "setAggregator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "__governance", + "type": "address" + } + ], + "name": "setGovernace", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_redStoneAdapter", + "type": "address" + } + ], + "name": "setRedStoneAdapterAddress", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/tests/orderbook/abi/OrderBook.json b/tests/orderbook/abi/OrderBook.json new file mode 100644 index 0000000000..2f0474b469 --- /dev/null +++ b/tests/orderbook/abi/OrderBook.json @@ -0,0 +1,551 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "_clearingHouse", + "type": "address" + }, + { + "internalType": "address", + "name": "_trustedForwarder", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "string", + "name": "err", + "type": "string" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "toLiquidate", + "type": "uint256" + } + ], + "name": "LiquidationError", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "string", + "name": "err", + "type": "string" + } + ], + "name": "MatchingValidationError", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "fillAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "openInterestNotional", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bool", + "name": "isLiquidation", + "type": "bool" + } + ], + "name": "OrderMatched", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "string", + "name": "err", + "type": "string" + } + ], + "name": "OrderMatchingError", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "authority", + "type": "address" + } + ], + "name": "TradingAuthorityRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "authority", + "type": "address" + } + ], + "name": "TradingAuthorityWhitelisted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "inputs": [], + "name": "clearingHouse", + "outputs": [ + { + "internalType": "contract IClearingHouse", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes[2]", + "name": "data", + "type": "bytes[2]" + }, + { + "internalType": "int256", + "name": "fillAmount", + "type": "int256" + } + ], + "name": "executeMatchedOrders", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "governance", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_governance", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "isTradingAuthority", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "forwarder", + "type": "address" + } + ], + "name": "isTrustedForwarder", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "isValidator", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "juror", + "outputs": [ + { + "internalType": "contract IJuror", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "liquidationAmount", + "type": "uint256" + } + ], + "name": "liquidateAndExecuteOrder", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "name": "orderHandlers", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "err", + "type": "string" + } + ], + "name": "parseMatchingError", + "outputs": [ + { + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "internalType": "string", + "name": "reason", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "paused", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "referral", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "authority", + "type": "address" + } + ], + "name": "revokeTradingAuthority", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "__governance", + "type": "address" + } + ], + "name": "setGovernace", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_juror", + "type": "address" + } + ], + "name": "setJuror", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "orderType", + "type": "uint8" + }, + { + "internalType": "address", + "name": "handler", + "type": "address" + } + ], + "name": "setOrderHandler", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_referral", + "type": "address" + } + ], + "name": "setReferral", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trader", + "type": "address" + }, + { + "internalType": "address", + "name": "authority", + "type": "address" + } + ], + "name": "setTradingAuthority", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "validator", + "type": "address" + }, + { + "internalType": "bool", + "name": "status", + "type": "bool" + } + ], + "name": "setValidatorStatus", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "authority", + "type": "address" + } + ], + "name": "whitelistTradingAuthority", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } +] diff --git a/tests/orderbook/abi/Ticks.json b/tests/orderbook/abi/Ticks.json new file mode 100644 index 0000000000..6207231fbc --- /dev/null +++ b/tests/orderbook/abi/Ticks.json @@ -0,0 +1,117 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "amm", + "type": "address" + }, + { + "internalType": "int256", + "name": "quoteQuantity", + "type": "int256" + } + ], + "name": "getBaseQuote", + "outputs": [ + { + "internalType": "uint256", + "name": "rate", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_amm", + "type": "address" + }, + { + "internalType": "bool", + "name": "isBid", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "tick", + "type": "uint256" + } + ], + "name": "getPrevTick", + "outputs": [ + { + "internalType": "uint256", + "name": "prevTick", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "amm", + "type": "address" + }, + { + "internalType": "int256", + "name": "baseAssetQuantity", + "type": "int256" + } + ], + "name": "getQuote", + "outputs": [ + { + "internalType": "uint256", + "name": "rate", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_amm", + "type": "address" + } + ], + "name": "sampleImpactAsk", + "outputs": [ + { + "internalType": "uint256", + "name": "impactAsk", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_amm", + "type": "address" + } + ], + "name": "sampleImpactBid", + "outputs": [ + { + "internalType": "uint256", + "name": "impactBid", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/tests/orderbook/bibliophile/variablesReadFromSlotTests.js b/tests/orderbook/bibliophile/variablesReadFromSlotTests.js new file mode 100644 index 0000000000..7ce837f1ff --- /dev/null +++ b/tests/orderbook/bibliophile/variablesReadFromSlotTests.js @@ -0,0 +1,475 @@ +const { ethers, BigNumber } = require('ethers'); +const utils = require('../utils.js'); +const chai = require('chai'); +const { assert, expect } = chai; +let chaiHttp = require('chai-http'); + +chai.use(chaiHttp); + +const { + _1e6, + _1e18, + addMargin, + alice, + bob, + cancelOrderFromLimitOrderV2, + charlie, + clearingHouse, + getAMMContract, + getIOCOrder, + getOrderV2, + getRandomSalt, + getRequiredMarginForLongOrder, + getRequiredMarginForShortOrder, + governance, + ioc, + limitOrderBook, + multiplyPrice, + multiplySize, + orderBook, + placeOrderFromLimitOrderV2, + placeIOCOrder, + provider, + removeAllAvailableMargin, + url, +} = utils; + + + +describe('Testing variables read from slots by precompile', function () { + context("Clearing house contract variables", function () { + // vars read from slot + // minAllowableMargin, maintenanceMargin, takerFee, amms + it("should read the correct value from contracts", async function () { + method = "testing_getClearingHouseVars" + params =[ charlie.address ] + response = await makehttpCall(method, params) + result = response.body.result + + actualMaintenanceMargin = await clearingHouse.maintenanceMargin() + actualMinAllowableMargin = await clearingHouse.minAllowableMargin() + actualTakerFee = await clearingHouse.takerFee() + actualAmms = await clearingHouse.getAMMs() + + expect(result.maintenance_margin).to.equal(actualMaintenanceMargin.toNumber()) + expect(result.min_allowable_margin).to.equal(actualMinAllowableMargin.toNumber()) + expect(result.taker_fee).to.equal(actualTakerFee.toNumber()) + expect(result.amms.length).to.equal(actualAmms.length) + for(let i = 0; i < result.amms.length; i++) { + expect(result.amms[i].toLowerCase()).to.equal(actualAmms[i].toLowerCase()) + } + newMaintenanceMargin = BigNumber.from(20000) + newMinAllowableMargin = BigNumber.from(40000) + newTakerFee = BigNumber.from(10000) + makerFee = await clearingHouse.makerFee() + referralShare = await clearingHouse.referralShare() + tradingFeeDiscount = await clearingHouse.tradingFeeDiscount() + liquidationPenalty = await clearingHouse.liquidationPenalty() + tx = await clearingHouse.connect(governance).setParams( + newMaintenanceMargin, + newMinAllowableMargin, + newTakerFee, + makerFee, + referralShare, + tradingFeeDiscount, + liquidationPenalty + ) + await tx.wait() + + response = await makehttpCall(method, params) + result = response.body.result + + expect(result.maintenance_margin).to.equal(newMaintenanceMargin.toNumber()) + expect(result.min_allowable_margin).to.equal(newMinAllowableMargin.toNumber()) + expect(result.taker_fee).to.equal(newTakerFee.toNumber()) + + // revert config + tx = await clearingHouse.connect(governance).setParams( + actualMaintenanceMargin, + actualMinAllowableMargin, + actualTakerFee, + makerFee, + referralShare, + tradingFeeDiscount, + liquidationPenalty + ) + await tx.wait() + }) + }) + context("Margin account contract variables", function () { + // vars read from slot + // margin, reservedMargin + it("should read the correct value from contracts", async function () { + // zero balance + method ="testing_getMarginAccountVars" + params =[ 0, charlie.address ] + response = await makehttpCall(method, params) + expect(response.body.result.margin).to.equal(0) + expect(response.body.result.reserved_margin).to.equal(0) + + // add balance for order and then place + longOrder = getOrderV2(0, charlie.address, multiplySize(0.1), multiplyPrice(2000), BigNumber.from(Date.now())) + requiredMargin = await getRequiredMarginForLongOrder(longOrder) + await addMargin(charlie, requiredMargin) + await placeOrderFromLimitOrderV2(longOrder, charlie) + + method ="testing_getMarginAccountVars" + params =[ 0, charlie.address ] + response = await makehttpCall(method, params) + + //cleanup + await cancelOrderFromLimitOrderV2(longOrder, charlie) + await removeAllAvailableMargin(charlie) + + expect(response.body.result.margin).to.equal(requiredMargin.toNumber()) + expect(response.body.result.reserved_margin).to.equal(requiredMargin.toNumber()) + }) + }) + context("AMM contract variables", function () { + // vars read from slot + // positions, cumulativePremiumFraction, maxOracleSpreadRatio, maxLiquidationRatio, minSizeRequirement, oracle, underlyingAsset, + // maxLiquidationPriceSpread, redStoneAdapter, redStoneFeedId, impactMarginNotional, lastTradePrice, bids, asks, bidsHead, asksHead + let ammIndex = 0 + let method ="testing_getAMMVars" + let ammAddress + + this.beforeAll(async function () { + amms = await clearingHouse.getAMMs() + ammAddress = amms[ammIndex] + }) + context("when variables have default value after setup", async function () { + it("should read the correct value of variables from contracts", async function () { + // maxOracleSpreadRatio, maxLiquidationRatio, minSizeRequirement, oracle, underlyingAsset, maxLiquidationPriceSpread + params =[ ammAddress, ammIndex, charlie.address ] + response = await makehttpCall(method, params) + + amm = new ethers.Contract(ammAddress, require('../abi/AMM.json'), provider) + actualMaxOracleSpreadRatio = await amm.maxOracleSpreadRatio() + actualOracleAddress = await amm.oracle() + actualMaxLiquidationRatio = await amm.maxLiquidationRatio() + actualMinSizeRequirement = await amm.minSizeRequirement() + actualUnderlyingAssetAddress = await amm.underlyingAsset() + actualMaxLiquidationPriceSpread = await amm.maxLiquidationPriceSpread() + + result = response.body.result + expect(result.max_oracle_spread_ratio).to.equal(actualMaxOracleSpreadRatio.toNumber()) + expect(result.oracle_address.toLowerCase()).to.equal(actualOracleAddress.toString().toLowerCase()) + expect(result.max_liquidation_ratio).to.equal(actualMaxLiquidationRatio.toNumber()) + expect(String(result.min_size_requirement)).to.equal(actualMinSizeRequirement.toString()) + expect(result.underlying_asset_address.toLowerCase()).to.equal(actualUnderlyingAssetAddress.toString().toLowerCase()) + expect(result.max_liquidation_price_spread).to.equal(actualMaxLiquidationPriceSpread.toNumber()) + }) + }) + context("when variables dont have default value after setup", async function () { + // positions, cumulativePremiumFraction, redStoneAdapter, redStoneFeedId, impactMarginNotional, lastTradePrice, bids, asks, bidsHead, asksHead + context("variables which need set config before reading", async function () { + let amm, oracleAddress, redStoneAdapterAddress, impactMarginNotional + this.beforeAll(async function () { + amm = await getAMMContract(ammIndex) + oracleAddress = await amm.oracle() + oracle = new ethers.Contract(oracleAddress, require("../abi/Oracle.json"), provider); + marginAccount = new ethers.Contract(await amm.marginAccount(), require("../abi/MarginAccount.json"), provider); + + redStoneAdapterAddress = await oracle.redStoneAdapter() + impactMarginNotional = await amm.impactMarginNotional() + }) + this.afterAll(async function () { + await oracle.connect(governance).setRedStoneAdapterAddress(redStoneAdapterAddress) + await marginAccount.connect(governance).setOracle(oracleAddress) + await amm.connect(governance).setImpactMarginNotional(impactMarginNotional) + }) + it("should read the correct value from contracts", async function () { + newOracleAddress = alice.address + newRedStoneAdapterAddress = bob.address + newImpactMarginNotional = BigNumber.from(100000) + + tx = await oracle.connect(governance).setRedStoneAdapterAddress(newRedStoneAdapterAddress) + tx = await amm.connect(governance).setImpactMarginNotional(newImpactMarginNotional) + await tx.wait() + + params =[ ammAddress, ammIndex, charlie.address ] + response = await makehttpCall(method, params) + result = response.body.result + + expect(result.red_stone_adapter_address.toLowerCase()).to.equal(newRedStoneAdapterAddress.toLowerCase()) + expect(result.impact_margin_notional).to.equal(newImpactMarginNotional.toNumber()) + + // setOracle + tx = await marginAccount.connect(governance).setOracle(newOracleAddress) + await tx.wait() + response = await makehttpCall(method, params) + result = response.body.result + expect(result.oracle_address.toLowerCase()).to.equal(newOracleAddress.toLowerCase()) + expect(result.red_stone_adapter_address.toLowerCase()).to.equal('0x' + '0'.repeat(40)) // red stone adapter should be zero in new oracle + expect(result.impact_margin_notional).to.equal(newImpactMarginNotional.toNumber()) + }) + }) + context("variables which need place order before reading", async function () { + //bids, asks, bidsHead, asksHead + let longOrderBaseAssetQuantity = multiplySize(0.1) // 0.1 ether + let shortOrderBaseAssetQuantity = multiplySize(-0.1) // 0.1 ether + let longOrderPrice = multiplyPrice(1799) + let shortOrderPrice = multiplyPrice(1801) + let longOrder = getOrderV2(ammIndex, alice.address, longOrderBaseAssetQuantity, longOrderPrice, BigNumber.from(Date.now()), false) + let shortOrder = getOrderV2(ammIndex, bob.address, shortOrderBaseAssetQuantity, shortOrderPrice, BigNumber.from(Date.now()), false) + + this.beforeAll(async function () { + requiredMarginAlice = await getRequiredMarginForLongOrder(longOrder) + await addMargin(alice, requiredMarginAlice) + await placeOrderFromLimitOrderV2(longOrder, alice) + requiredMarginBob = await getRequiredMarginForShortOrder(shortOrder) + await addMargin(bob, requiredMarginBob) + await placeOrderFromLimitOrderV2(shortOrder, bob) + }) + + this.afterAll(async function () { + await cancelOrderFromLimitOrderV2(longOrder, alice) + await cancelOrderFromLimitOrderV2(shortOrder, bob) + await removeAllAvailableMargin(alice) + await removeAllAvailableMargin(bob) + }) + + it("should read the correct values from contract", async function () { + params =[ ammAddress, ammIndex, alice.address ] + response = await makehttpCall(method, params) + result = response.body.result + expect(result.asks_head).to.equal(shortOrderPrice.toNumber()) + expect(result.bids_head).to.equal(longOrderPrice.toNumber()) + expect(String(result.bids_head_size)).to.equal(longOrderBaseAssetQuantity.toString()) + expect(String(result.asks_head_size)).to.equal(shortOrderBaseAssetQuantity.abs().toString()) + }) + }) + context("variables which need position before reading", async function () { + let longOrderBaseAssetQuantity = multiplySize(0.1) // 0.1 ether + let shortOrderBaseAssetQuantity = multiplySize(-0.1) // 0.1 ether + let orderPrice = multiplyPrice(2000) + let longOrder = getOrderV2(ammIndex, alice.address, longOrderBaseAssetQuantity, orderPrice, BigNumber.from(Date.now()), false) + let shortOrder = getOrderV2(ammIndex, bob.address, shortOrderBaseAssetQuantity, orderPrice, BigNumber.from(Date.now()), false) + + this.beforeAll(async function () { + requiredMarginAlice = await getRequiredMarginForLongOrder(longOrder) + await addMargin(alice, requiredMarginAlice) + await placeOrderFromLimitOrderV2(longOrder, alice) + requiredMarginBob = await getRequiredMarginForShortOrder(shortOrder) + await addMargin(bob, requiredMarginBob) + await placeOrderFromLimitOrderV2(shortOrder, bob) + }) + + this.afterAll(async function () { + oppositeLongOrder = getOrderV2(ammIndex, bob.address, longOrderBaseAssetQuantity, orderPrice, BigNumber.from(Date.now()), true) + oppositeShortOrder = getOrderV2(ammIndex, alice.address, shortOrderBaseAssetQuantity, orderPrice, BigNumber.from(Date.now()), true) + await placeOrderFromLimitOrderV2(oppositeLongOrder, bob) + await placeOrderFromLimitOrderV2(oppositeShortOrder, alice) + await utils.waitForOrdersToMatch() + await removeAllAvailableMargin(alice) + await removeAllAvailableMargin(bob) + }) + + it("should read the correct values from contract", async function () { + params =[ ammAddress, ammIndex, alice.address ] + resultAlice = (await makehttpCall(method, params)).body.result + params =[ ammAddress, ammIndex, bob.address ] + resultBob = (await makehttpCall(method, params)).body.result + + expect(String(resultAlice.position.size)).to.equal(longOrderBaseAssetQuantity.toString()) + expect(String(resultAlice.position.open_notional)).to.equal(longOrderBaseAssetQuantity.mul(orderPrice).div(_1e18).toString()) + expect(String(resultBob.position.size)).to.equal(shortOrderBaseAssetQuantity.toString()) + expect(String(resultBob.position.open_notional)).to.equal(shortOrderBaseAssetQuantity.mul(orderPrice).abs().div(_1e18).toString()) + expect(resultAlice.last_price).to.equal(orderPrice.toNumber()) + expect(resultBob.last_price).to.equal(orderPrice.toNumber()) + }) + }) + }) + }) + context("IOC order contract variables", function () { + let method ="testing_getIOCOrdersVars" + let longOrderBaseAssetQuantity = multiplySize(0.1) // 0.1 ether + let shortOrderBaseAssetQuantity = multiplySize(-0.1) // 0.1 ether + let orderPrice = multiplyPrice(2000) + let market = BigNumber.from(0) + + + context("variable which have default value after setup", async function () { + //ioc expiration cap + it("should read the correct value from contracts", async function () { + params = [ "0xe97a0702264091714ea19b481c1fd12d9686cb4602efbfbec41ec5ea5410da84"] + + result = (await makehttpCall(method, params)).body.result + actualExpirationCap = await ioc.expirationCap() + expect(result.ioc_expiration_cap).to.eq(actualExpirationCap.toNumber()) + }) + }) + context("variable which need place order before reading", async function () { + //blockPlaced, filledAmount, orderStatus + context("for a long IOC order", async function () { + it("returns correct value when order is not placed", async function () { + latestBlockNumber = await provider.getBlockNumber() + lastTimestamp = (await provider.getBlock(latestBlockNumber)).timestamp + expireAt = lastTimestamp + 6 + longIOCOrder = getIOCOrder(expireAt, market, charlie.address, longOrderBaseAssetQuantity, orderPrice, getRandomSalt(), false) + orderHash = await ioc.getOrderHash(longIOCOrder) + params = [ orderHash ] + result = (await makehttpCall(method, params)).body.result + expect(result.order_details.block_placed).to.eq(0) + expect(result.order_details.filled_amount).to.eq(0) + expect(result.order_details.order_status).to.eq(0) + }) + it("returns correct value when order is placed", async function () { + let charlieBalance = _1e6.mul(150) + await addMargin(charlie, charlieBalance) + + //placing order + latestBlockNumber = await provider.getBlockNumber() + lastTimestamp = (await provider.getBlock(latestBlockNumber)).timestamp + expireAt = lastTimestamp + 6 + longIOCOrder = getIOCOrder(expireAt, market, charlie.address, longOrderBaseAssetQuantity, orderPrice, getRandomSalt(), false) + orderHash = await ioc.getOrderHash(longIOCOrder) + params = [ orderHash ] + txDetails = await placeIOCOrder(longIOCOrder, charlie) + result = (await makehttpCall(method, params)).body.result + + //cleanup + await removeAllAvailableMargin(charlie) + + actualBlockPlaced = txDetails.txReceipt.blockNumber + expect(result.order_details.block_placed).to.eq(actualBlockPlaced) + expect(result.order_details.filled_amount).to.eq(0) + expect(result.order_details.order_status).to.eq(1) + + }) + }) + context("for a short IOC order", async function () { + it("returns correct value when order is not placed", async function () { + latestBlockNumber = await provider.getBlockNumber() + lastTimestamp = (await provider.getBlock(latestBlockNumber)).timestamp + expireAt = lastTimestamp + 6 + shortIOCOrder = getIOCOrder(expireAt, market, charlie.address, shortOrderBaseAssetQuantity, orderPrice, getRandomSalt(), false) + orderHash = await ioc.getOrderHash(shortIOCOrder) + params = [ orderHash ] + result = (await makehttpCall(method, params)).body.result + expect(result.order_details.block_placed).to.eq(0) + expect(result.order_details.filled_amount).to.eq(0) + expect(result.order_details.order_status).to.eq(0) + }) + it("returns correct value when order is placed", async function () { + let charlieBalance = _1e6.mul(150) + await addMargin(charlie, charlieBalance) + + //placing order + latestBlockNumber = await provider.getBlockNumber() + lastTimestamp = (await provider.getBlock(latestBlockNumber)).timestamp + expireAt = lastTimestamp + 6 + shortIOCOrder = getIOCOrder(expireAt, market, charlie.address, shortOrderBaseAssetQuantity, orderPrice, getRandomSalt(), false) + orderHash = await ioc.getOrderHash(shortIOCOrder) + params = [ orderHash ] + txDetails = await placeIOCOrder(shortIOCOrder, charlie) + result = (await makehttpCall(method, params)).body.result + + //cleanup + await removeAllAvailableMargin(charlie) + + actualBlockPlaced = txDetails.txReceipt.blockNumber + expect(result.order_details.block_placed).to.eq(actualBlockPlaced) + expect(result.order_details.filled_amount).to.eq(0) + expect(result.order_details.order_status).to.eq(1) + }) + }) + }) + }) + context("order book contract variables", function () { + let method ="testing_getOrderBookVars" + let traderAddress = alice.address + let senderAddress = charlie.address + let longOrderBaseAssetQuantity = multiplySize(0.1) // 0.1 ether + let shortOrderBaseAssetQuantity = multiplySize(-0.1) // 0.1 ether + let orderPrice = multiplyPrice(2000) + let market = BigNumber.from(0) + + context("variables which dont need place order before reading", async function () { + let params = [ traderAddress, senderAddress, "0xe97a0702264091714ea19b481c1fd12d9686cb4602efbfbec41ec5ea5410da84" ] + //isTradingAuthority + it("should return false when sender is not a tradingAuthority for an address", async function () { + result = (await makehttpCall(method, params)).body.result + expect(result.is_trading_authority).to.eq(false) + }) + // need to implement adding trading authority for an address + it.skip("should return true when sender is a tradingAuthority for an address", async function () { + await orderBook.connect(alice).setTradingAuthority(traderAddress, senderAddress) + result = (await makehttpCall(method, params)).body.result + expect(result.is_trading_authority).to.eq(true) + }) + }) + context("variables which need place order before reading", async function () { + context("for a long limit order", async function () { + let longOrder = getOrderV2(market, traderAddress, longOrderBaseAssetQuantity, orderPrice, getRandomSalt()) + it("returns correct value when order is not placed", async function () { + orderHash = await limitOrderBook.getOrderHash(longOrder) + params = [ traderAddress, senderAddress, orderHash ] + result = (await makehttpCall(method, params)).body.result + expect(result.order_details.block_placed).to.eq(0) + expect(result.order_details.filled_amount).to.eq(0) + expect(result.order_details.order_status).to.eq(0) + }) + it("returns correct value when order is placed", async function () { + requiredMargin = await getRequiredMarginForLongOrder(longOrder) + await addMargin(alice, requiredMargin) + orderHash = await limitOrderBook.getOrderHash(longOrder) + const {txReceipt} = await placeOrderFromLimitOrderV2(longOrder, alice) + params = [ traderAddress, traderAddress, orderHash ] + result = (await makehttpCall(method, params)).body.result + // cleanup + await cancelOrderFromLimitOrderV2(longOrder, alice) + await removeAllAvailableMargin(alice) + + expectedBlockPlaced = txReceipt.blockNumber + expect(result.order_details.block_placed).to.eq(expectedBlockPlaced) + expect(result.order_details.filled_amount).to.eq(0) + expect(result.order_details.order_status).to.eq(1) + }) + }) + context("for a short limit order", async function () { + let shortOrder = getOrderV2(market, traderAddress, shortOrderBaseAssetQuantity, orderPrice, getRandomSalt()) + it("returns correct value when order is not placed", async function () { + orderHash = await limitOrderBook.getOrderHash(shortOrder) + params = [ traderAddress, traderAddress, orderHash ] + result = (await makehttpCall(method, params)).body.result + expect(result.order_details.block_placed).to.eq(0) + expect(result.order_details.filled_amount).to.eq(0) + expect(result.order_details.order_status).to.eq(0) + }) + it("returns correct value when order is placed", async function () { + requiredMargin = await getRequiredMarginForShortOrder(shortOrder) + await addMargin(alice, requiredMargin) + + orderHash = await limitOrderBook.getOrderHash(shortOrder) + const { txReceipt } = await placeOrderFromLimitOrderV2(shortOrder, alice) + params = [ traderAddress, traderAddress, orderHash ] + result = (await makehttpCall(method, params)).body.result + // cleanup + await cancelOrderFromLimitOrderV2(shortOrder, alice) + await removeAllAvailableMargin(alice) + + expectedBlockPlaced = txReceipt.blockNumber + expect(result.order_details.block_placed).to.eq(expectedBlockPlaced) + expect(result.order_details.filled_amount).to.eq(0) + expect(result.order_details.order_status).to.eq(1) + }) + }) + }) + }) +}) + +async function makehttpCall(method, params=[]) { + body = { + "jsonrpc":"2.0", + "id" :1, + "method" : method, + "params" : params + } + + const serverUrl = url.split("/").slice(0, 3).join("/") + path = "/".concat(url.split("/").slice(3).join("/")) + return chai.request(serverUrl) + .post(path) + .send(body) +} diff --git a/tests/orderbook/get_events.js b/tests/orderbook/get_events.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/orderbook/juror/JurorTests.js b/tests/orderbook/juror/JurorTests.js new file mode 100644 index 0000000000..22d319e7d8 --- /dev/null +++ b/tests/orderbook/juror/JurorTests.js @@ -0,0 +1,1114 @@ +const { expect } = require("chai") +const { BigNumber } = require("ethers") + +const gasLimit = 5e6 // subnet genesis file only allows for this much + +const { + addMargin, + alice, + bob, + cancelOrderFromLimitOrderV2, + charlie, + clearingHouse, + getEventsFromLimitOrderBookTx, + getMinSizeRequirement, + getOrderV2, + getRandomSalt, + getRequiredMarginForLongOrder, + getRequiredMarginForShortOrder, + juror, + marginAccount, + multiplyPrice, + multiplySize, + orderBook, + placeOrderFromLimitOrderV2, + removeAllAvailableMargin, + waitForOrdersToMatch, +} = require("../utils") + +describe("Juror tests", async function() { + context("Alice is a new user and tries to place a valid longOrder", async function() { + // Alice is a new user and tries to place a valid longOrder - should fail + // After user adds margin and tries to place a valid order - should succeed + // check if margin is reserved + // User tries to place same order again - should fail + // Cancel order - should succeed + // try cancel same order again - should fail + // available margin should be amount deposited + let longOrderBaseAssetQuantity = multiplySize(0.1) // 0.1 ether + let orderPrice = multiplyPrice(1800) + let market = BigNumber.from(0) + let longOrder = getOrderV2(market, alice.address, longOrderBaseAssetQuantity, orderPrice, getRandomSalt()) + + it("should fail as trader has not margin", async function() { + await removeAllAvailableMargin(alice) + output = await juror.validatePlaceLimitOrder(longOrder, alice.address) + expect(output.err).to.equal("insufficient margin") + expectedOrderHash = await limitOrderBook.getOrderHash(longOrder) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.reserveAmount.toNumber()).to.equal(0) + expectedAmmAddress = await clearingHouse.amms(market) + expect(output.res.amm).to.equal(expectedAmmAddress) + }) + it("should succeed after trader deposits margin and return reserve margin", async function() { + totalRequiredMargin = await getRequiredMarginForLongOrder(longOrder) + await addMargin(alice, totalRequiredMargin) + output = await juror.validatePlaceLimitOrder(longOrder, alice.address) + expect(output.err).to.equal("") + expectedOrderHash = await limitOrderBook.getOrderHash(longOrder) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.reserveAmount.toNumber()).to.equal(totalRequiredMargin.toNumber()) + expectedAmmAddress = await clearingHouse.amms(market) + expect(output.res.amm).to.equal(expectedAmmAddress) + + // place the order + output = await placeOrderFromLimitOrderV2(longOrder, alice) + orderStatus = await limitOrderBook.orderStatus(expectedOrderHash) + expect(orderStatus.status).to.equal(1) + expect(orderStatus.reservedMargin.toNumber()).to.equal(totalRequiredMargin.toNumber()) + expect(orderStatus.blockPlaced.toNumber()).to.equal(output.txReceipt.blockNumber) + expect(orderStatus.filledAmount.toNumber()).to.equal(0) + }) + it("should emit OrderRejected if trader tries to place same order again", async function() { + output = await juror.validatePlaceLimitOrder(longOrder, alice.address) + expect(output.err).to.equal("order already exists") + expectedOrderHash = await limitOrderBook.getOrderHash(longOrder) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.reserveAmount.toNumber()).to.equal(0) + expectedAmmAddress = await clearingHouse.amms(market) + expect(output.res.amm).to.equal(expectedAmmAddress) + + output = await placeOrderFromLimitOrderV2(longOrder, alice) + limitOrderBookLogWithEvent = (await getEventsFromLimitOrderBookTx(output.txReceipt.transactionHash))[0] + expect(limitOrderBookLogWithEvent.event).to.equal("OrderRejected") + expect(limitOrderBookLogWithEvent.args.err).to.equal("order already exists") + expect(limitOrderBookLogWithEvent.args.orderHash).to.equal(expectedOrderHash) + expect(limitOrderBookLogWithEvent.args.trader).to.equal(alice.address) + }) + it("should succeed if trader cancels order", async function() { + output = await juror.validateCancelLimitOrder(longOrder, alice.address, false) + expect(output.err).to.equal("") + expectedOrderHash = await limitOrderBook.getOrderHash(longOrder) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.unfilledAmount.toString()).to.equal(longOrder.baseAssetQuantity.toString()) + expect(output.res.amm).to.equal(await clearingHouse.amms(market)) + + await cancelOrderFromLimitOrderV2(longOrder, alice) + orderStatus = await limitOrderBook.orderStatus(expectedOrderHash) + expect(orderStatus.status).to.equal(3) + expect(orderStatus.reservedMargin.toNumber()).to.equal(0) + expect(orderStatus.blockPlaced.toNumber()).to.equal(0) + expect(orderStatus.filledAmount.toNumber()).to.equal(0) + }) + it("should fail if trader tries to cancel same order again", async function() { + output = await juror.validateCancelLimitOrder(longOrder, alice.address, false) + expect(output.err).to.equal("Cancelled") + expectedOrderHash = await limitOrderBook.getOrderHash(longOrder) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.unfilledAmount.toString()).to.equal("0") + expect(output.res.amm).to.equal("0x0000000000000000000000000000000000000000") + + output = await cancelOrderFromLimitOrderV2(longOrder, alice) + limitOrderBookLogWithEvent = (await getEventsFromLimitOrderBookTx(output.txReceipt.transactionHash))[0] + expect(limitOrderBookLogWithEvent.event).to.equal("OrderCancelRejected") + expect(limitOrderBookLogWithEvent.args.err).to.equal("Cancelled") + expect(limitOrderBookLogWithEvent.args.orderHash).to.equal(expectedOrderHash) + expect(limitOrderBookLogWithEvent.args.trader).to.equal(alice.address) + }) + it("should have available margin equal to amount deposited", async function() { + margin = await marginAccount.getAvailableMargin(alice.address) + expect(margin.toNumber()).to.equal(totalRequiredMargin.toNumber()) + }) + }) + context("Bob is a new user and trades via a trading authority", async function() { + // Bob is also a new user and trades via a trading authority + // Trading authority tries to place a valid shortOrder from bob without authorization - should fail + // bob authorizes trading authority to place orders on his behalf + // trading authority tries to place a valid shortOrder from bob with authorization - should succeed + // Place same order again via trading authority - should fail + // Cancel order via trading authority - should succeed + // Cancel same order again via trading authority - should fail + // available margin should be amount deposited + let shortOrderBaseAssetQuantity = multiplySize(-0.1) // 0.1 ether + let orderPrice = multiplyPrice(1800) + let market = BigNumber.from(0) + let shortOrder = getOrderV2(market, bob.address, shortOrderBaseAssetQuantity, orderPrice, getRandomSalt()) + let tradingAuthority = charlie + + it("should fail as trader has no margin", async function() { + await removeAllAvailableMargin(bob) + output = await juror.validatePlaceLimitOrder(shortOrder, bob.address) + expect(output.err).to.equal("insufficient margin") + expectedOrderHash = await limitOrderBook.getOrderHash(shortOrder) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.reserveAmount.toNumber()).to.equal(0) + expectedAmmAddress = await clearingHouse.amms(market) + expect(output.res.amm).to.equal(expectedAmmAddress) + }) + it("after depositing margin, it should fail if trading authority tries to place order without authorization", async function() { + totalRequiredMargin = await getRequiredMarginForShortOrder(shortOrder) + await addMargin(bob, totalRequiredMargin) + const tx = await orderBook.connect(bob).revokeTradingAuthority(tradingAuthority.address) + await tx.wait() + + output = await juror.validatePlaceLimitOrder(shortOrder, tradingAuthority.address) + expect(output.err).to.equal("no trading authority") + expectedOrderHash = await limitOrderBook.getOrderHash(shortOrder) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.reserveAmount.toNumber()).to.equal(0) + expectedAmmAddress = await clearingHouse.amms(market) + expect(output.res.amm).to.equal("0x0000000000000000000000000000000000000000") + }) + it("should succeed if trading authority tries to place order with authorization", async function() { + const tx = await orderBook.connect(bob).whitelistTradingAuthority(tradingAuthority.address) + await tx.wait() + + output = await juror.validatePlaceLimitOrder(shortOrder, tradingAuthority.address) + expect(output.err).to.equal("") + expectedOrderHash = await limitOrderBook.getOrderHash(shortOrder) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.reserveAmount.toNumber()).to.equal(totalRequiredMargin.toNumber()) + expectedAmmAddress = await clearingHouse.amms(market) + expect(output.res.amm).to.equal(expectedAmmAddress) + + // place the order + output = await placeOrderFromLimitOrderV2(shortOrder, tradingAuthority) + orderStatus = await limitOrderBook.orderStatus(expectedOrderHash) + expect(orderStatus.status).to.equal(1) + expect(orderStatus.reservedMargin.toNumber()).to.equal(totalRequiredMargin.toNumber()) + expect(orderStatus.blockPlaced.toNumber()).to.equal(output.txReceipt.blockNumber) + expect(orderStatus.filledAmount.toNumber()).to.equal(0) + }) + it("should emit OrderRejected if trading authority tries to place same order again", async function() { + output = await juror.validatePlaceLimitOrder(shortOrder, tradingAuthority.address) + expect(output.err).to.equal("order already exists") + expectedOrderHash = await limitOrderBook.getOrderHash(shortOrder) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.reserveAmount.toNumber()).to.equal(0) + expectedAmmAddress = await clearingHouse.amms(market) + expect(output.res.amm).to.equal(expectedAmmAddress) + + output = await placeOrderFromLimitOrderV2(shortOrder, tradingAuthority) + limitOrderBookLogWithEvent = (await getEventsFromLimitOrderBookTx(output.txReceipt.transactionHash))[0] + expect(limitOrderBookLogWithEvent.event).to.equal("OrderRejected") + expect(limitOrderBookLogWithEvent.args.err).to.equal("order already exists") + expect(limitOrderBookLogWithEvent.args.orderHash).to.equal(expectedOrderHash) + expect(limitOrderBookLogWithEvent.args.trader).to.equal(shortOrder.trader) + }) + it("should succeed if trading authority cancels order", async function() { + output = await juror.validateCancelLimitOrder(shortOrder, tradingAuthority.address, false) + expect(output.err).to.equal("") + expectedOrderHash = await limitOrderBook.getOrderHash(shortOrder) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.unfilledAmount.toString()).to.equal(shortOrder.baseAssetQuantity.toString()) + expect(output.res.amm).to.equal(await clearingHouse.amms(market)) + + await cancelOrderFromLimitOrderV2(shortOrder, tradingAuthority) + orderStatus = await limitOrderBook.orderStatus(expectedOrderHash) + expect(orderStatus.status).to.equal(3) + expect(orderStatus.reservedMargin.toNumber()).to.equal(0) + expect(orderStatus.blockPlaced.toNumber()).to.equal(0) + expect(orderStatus.filledAmount.toNumber()).to.equal(0) + }) + it("should fail if trading authority tries to cancel same order again", async function() { + output = await juror.validateCancelLimitOrder(shortOrder, tradingAuthority.address, false) + expect(output.err).to.equal("Cancelled") + expectedOrderHash = await limitOrderBook.getOrderHash(shortOrder) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.unfilledAmount.toString()).to.equal("0") + expect(output.res.amm).to.equal("0x0000000000000000000000000000000000000000") + + output = await cancelOrderFromLimitOrderV2(shortOrder, tradingAuthority) + limitOrderBookLogWithEvent = (await getEventsFromLimitOrderBookTx(output.txReceipt.transactionHash))[0] + expect(limitOrderBookLogWithEvent.event).to.equal("OrderCancelRejected") + expect(limitOrderBookLogWithEvent.args.err).to.equal("Cancelled") + expect(limitOrderBookLogWithEvent.args.orderHash).to.equal(expectedOrderHash) + expect(limitOrderBookLogWithEvent.args.trader).to.equal(shortOrder.trader) + }) + it("should have available margin equal to amount deposited", async function() { + margin = await marginAccount.getAvailableMargin(bob.address) + expect(margin.toNumber()).to.equal(totalRequiredMargin.toNumber()) + }) + }) + + context("Market maker is trying to place/cancel orders", async function() { + // Market maker tries to place a valid postonly longOrder 1 - should pass + // Market maker tries to place a valid postonly shortOrder1 - should pass + // Market maker tries to place same order again - should fail + // Market maker tries to place postonly longOrder2 with higher or same price - should fail + // Market maker tries to place postonly longOrder2 with lower price - should succeed + // Market maker tries to cancel longOrder1 and longOrder2 - should pass + // Market maker tries to cancel same longOrders - should fail + + // Market maker tries to place same order again - should fail + // Market maker tries to place postonly shortOrder2 with lower or same price - should fail + // Market maker tries to place postonly shortOrder2 with higher price - should succeed(cancel order for cleanup) + // Market maker tries to cancel shortOrder1 and shortOrder2 - should pass + // Market maker tries to cancel same shortOrders - should fail + let marketMaker = alice + let shortOrderBaseAssetQuantity = multiplySize(-0.1) // 0.1 ether + let longOrderBaseAssetQuantity = multiplySize(0.1) // 0.1 ether + let longOrderPrice = multiplyPrice(1799) + let shortOrderPrice = multiplyPrice(1801) + let market = BigNumber.from(0) + let longOrder = getOrderV2(market, marketMaker.address, longOrderBaseAssetQuantity, longOrderPrice, getRandomSalt(), false, true) + let shortOrder = getOrderV2(market, marketMaker.address, shortOrderBaseAssetQuantity, shortOrderPrice, getRandomSalt(), false, true) + + + this.beforeAll(async function() { + await addMargin(marketMaker, multiplyPrice(150000)) + }) + this.afterAll(async function() { + await removeAllAvailableMargin(marketMaker) + }) + + context("should succeed when market maker tries to place valid postonly orders in blank orderbook", async function() { + it("should succeed if market maker tries to place a valid postonly longOrder", async function() { + totalRequiredMargin = await getRequiredMarginForLongOrder(longOrder) + output = await juror.validatePlaceLimitOrder(longOrder, marketMaker.address) + expect(output.err).to.equal("") + expectedOrderHash = await limitOrderBook.getOrderHash(longOrder) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.reserveAmount.toNumber()).to.equal(totalRequiredMargin.toNumber()) + expectedAmmAddress = await clearingHouse.amms(market) + expect(output.res.amm).to.equal(expectedAmmAddress) + + // place the order + output = await placeOrderFromLimitOrderV2(longOrder, marketMaker) + orderStatus = await limitOrderBook.orderStatus(expectedOrderHash) + expect(orderStatus.status).to.equal(1) + expect(orderStatus.reservedMargin.toNumber()).to.equal(totalRequiredMargin.toNumber()) + expect(orderStatus.blockPlaced.toNumber()).to.equal(output.txReceipt.blockNumber) + expect(orderStatus.filledAmount.toNumber()).to.equal(0) + }) + it("should succeed if market maker tries to place a valid postonly shortOrder", async function() { + totalRequiredMargin = await getRequiredMarginForShortOrder(shortOrder) + output = await juror.validatePlaceLimitOrder(shortOrder, marketMaker.address) + expect(output.err).to.equal("") + expectedOrderHash = await limitOrderBook.getOrderHash(shortOrder) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.reserveAmount.toNumber()).to.equal(totalRequiredMargin.toNumber()) + expectedAmmAddress = await clearingHouse.amms(market) + expect(output.res.amm).to.equal(expectedAmmAddress) + + // place the order + output = await placeOrderFromLimitOrderV2(shortOrder, marketMaker) + orderStatus = await limitOrderBook.orderStatus(expectedOrderHash) + expect(orderStatus.status).to.equal(1) + expect(orderStatus.reservedMargin.toNumber()).to.equal(totalRequiredMargin.toNumber()) + expect(orderStatus.blockPlaced.toNumber()).to.equal(output.txReceipt.blockNumber) + expect(orderStatus.filledAmount.toNumber()).to.equal(0) + }) + }) + context("should emit OrderRejected if market maker tries to place same orders again", async function() { + it("should emit OrderRejected if market maker tries to place same longOrder again", async function() { + output = await juror.validatePlaceLimitOrder(longOrder, marketMaker.address) + expect(output.err).to.equal("order already exists") + expectedOrderHash = await limitOrderBook.getOrderHash(longOrder) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.reserveAmount.toNumber()).to.equal(0) + expectedAmmAddress = await clearingHouse.amms(market) + expect(output.res.amm).to.equal(expectedAmmAddress) + + output = await placeOrderFromLimitOrderV2(longOrder, marketMaker) + limitOrderBookLogWithEvent = (await getEventsFromLimitOrderBookTx(output.txReceipt.transactionHash))[0] + expect(limitOrderBookLogWithEvent.event).to.equal("OrderRejected") + expect(limitOrderBookLogWithEvent.args.err).to.equal("order already exists") + expect(limitOrderBookLogWithEvent.args.orderHash).to.equal(expectedOrderHash) + expect(limitOrderBookLogWithEvent.args.trader).to.equal(longOrder.trader) + }) + + it("should emit OrderRejected if market maker tries to place same shortOrder again", async function() { + output = await juror.validatePlaceLimitOrder(shortOrder, marketMaker.address) + expect(output.err).to.equal("order already exists") + expectedOrderHash = await limitOrderBook.getOrderHash(shortOrder) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.reserveAmount.toNumber()).to.equal(0) + expectedAmmAddress = await clearingHouse.amms(market) + expect(output.res.amm).to.equal(expectedAmmAddress) + + output = await placeOrderFromLimitOrderV2(shortOrder, marketMaker) + limitOrderBookLogWithEvent = (await getEventsFromLimitOrderBookTx(output.txReceipt.transactionHash))[0] + expect(limitOrderBookLogWithEvent.event).to.equal("OrderRejected") + expect(limitOrderBookLogWithEvent.args.err).to.equal("order already exists") + expect(limitOrderBookLogWithEvent.args.orderHash).to.equal(expectedOrderHash) + expect(limitOrderBookLogWithEvent.args.trader).to.equal(shortOrder.trader) + }) + }) + context("when postonly order have potential matches in orderbook", async function() { + // longOrder and shortOrder are present in orderbook. + // asksHead = 1801 * 1e6 + // bidsHead = 1799 * 1e6 + it("should fail if market maker tries to place a postonly longOrder2 with higher or same price as shortOrder", async function() { + samePrice = shortOrder.price + longOrder2 = getOrderV2(market, marketMaker.address, longOrderBaseAssetQuantity, samePrice, getRandomSalt(), false, true) + output = await juror.validatePlaceLimitOrder(longOrder2, marketMaker.address) + expect(output.err).to.equal("crossing market") + expectedOrderHash = await limitOrderBook.getOrderHash(longOrder2) + expect(output.orderHash).to.equal(expectedOrderHash) + totalRequiredMarginForLongOrder2 = await getRequiredMarginForLongOrder(longOrder2) + expect(output.res.reserveAmount.toNumber()).to.equal(totalRequiredMarginForLongOrder2.toNumber()) + expectedAmmAddress = await clearingHouse.amms(market) + expect(output.res.amm).to.equal(expectedAmmAddress) + + // place the order + output = await placeOrderFromLimitOrderV2(longOrder2, marketMaker) + limitOrderBookLogWithEvent = (await getEventsFromLimitOrderBookTx(output.txReceipt.transactionHash))[0] + expect(limitOrderBookLogWithEvent.event).to.equal("OrderRejected") + expect(limitOrderBookLogWithEvent.args.err).to.equal("crossing market") + expect(limitOrderBookLogWithEvent.args.orderHash).to.equal(expectedOrderHash) + expect(limitOrderBookLogWithEvent.args.trader).to.equal(longOrder2.trader) + + higherPrice = shortOrderPrice.add(1) + longOrder3 = getOrderV2(market, marketMaker.address, longOrderBaseAssetQuantity, higherPrice, getRandomSalt(), false, true) + output = await juror.validatePlaceLimitOrder(longOrder3, marketMaker.address) + expect(output.err).to.equal("crossing market") + expectedOrderHash = await limitOrderBook.getOrderHash(longOrder3) + expect(output.orderHash).to.equal(expectedOrderHash) + totalRequiredMarginForLongOrder3 = await getRequiredMarginForLongOrder(longOrder3) + expect(output.res.reserveAmount.toNumber()).to.equal(totalRequiredMarginForLongOrder3.toNumber()) + expectedAmmAddress = await clearingHouse.amms(market) + expect(output.res.amm).to.equal(expectedAmmAddress) + + // place the order + output = await placeOrderFromLimitOrderV2(longOrder3, marketMaker) + limitOrderBookLogWithEvent = (await getEventsFromLimitOrderBookTx(output.txReceipt.transactionHash))[0] + expect(limitOrderBookLogWithEvent.event).to.equal("OrderRejected") + expect(limitOrderBookLogWithEvent.args.err).to.equal("crossing market") + expect(limitOrderBookLogWithEvent.args.orderHash).to.equal(expectedOrderHash) + expect(limitOrderBookLogWithEvent.args.trader).to.equal(longOrder3.trader) + }) + it("should fail if market maker tries to place a postonly shortOrder2 with lower or same price as longOrder", async function() { + samePrice = longOrder.price + shortOrder2 = getOrderV2(market, marketMaker.address, shortOrderBaseAssetQuantity, samePrice, getRandomSalt(), false, true) + output = await juror.validatePlaceLimitOrder(shortOrder2, marketMaker.address) + expect(output.err).to.equal("crossing market") + expectedOrderHash = await limitOrderBook.getOrderHash(shortOrder2) + expect(output.orderHash).to.equal(expectedOrderHash) + totalRequiredMarginForShortOrder2 = await getRequiredMarginForShortOrder(shortOrder2) + expect(output.res.reserveAmount.toNumber()).to.equal(totalRequiredMarginForShortOrder2.toNumber()) + expectedAmmAddress = await clearingHouse.amms(market) + expect(output.res.amm).to.equal(expectedAmmAddress) + + // place the order + output = await placeOrderFromLimitOrderV2(shortOrder2, marketMaker) + limitOrderBookLogWithEvent = (await getEventsFromLimitOrderBookTx(output.txReceipt.transactionHash))[0] + expect(limitOrderBookLogWithEvent.event).to.equal("OrderRejected") + expect(limitOrderBookLogWithEvent.args.err).to.equal("crossing market") + expect(limitOrderBookLogWithEvent.args.orderHash).to.equal(expectedOrderHash) + expect(limitOrderBookLogWithEvent.args.trader).to.equal(shortOrder2.trader) + + + lowerPrice = longOrderPrice.sub(1) + shortOrder3 = getOrderV2(market, marketMaker.address, shortOrderBaseAssetQuantity, lowerPrice, getRandomSalt(), false, true) + output = await juror.validatePlaceLimitOrder(shortOrder3, marketMaker.address) + expect(output.err).to.equal("crossing market") + expectedOrderHash = await limitOrderBook.getOrderHash(shortOrder3) + expect(output.orderHash).to.equal(expectedOrderHash) + totalRequiredMarginForShortOrder3 = await getRequiredMarginForShortOrder(shortOrder3) + expect(output.res.reserveAmount.toNumber()).to.equal(totalRequiredMarginForShortOrder3.toNumber()) + expectedAmmAddress = await clearingHouse.amms(market) + + // place the order + output = await placeOrderFromLimitOrderV2(shortOrder3, marketMaker) + limitOrderBookLogWithEvent = (await getEventsFromLimitOrderBookTx(output.txReceipt.transactionHash))[0] + expect(limitOrderBookLogWithEvent.event).to.equal("OrderRejected") + expect(limitOrderBookLogWithEvent.args.err).to.equal("crossing market") + expect(limitOrderBookLogWithEvent.args.orderHash).to.equal(expectedOrderHash) + expect(limitOrderBookLogWithEvent.args.trader).to.equal(shortOrder3.trader) + }) + }) + context("when postonly order does not have potential matches in orderbook", async function() { + it("should succeed if market maker tries to place another postonly longOrder with lower price than all shortOrders", async function() { + lowerPrice = shortOrder.price.sub(1) + longOrder4 = getOrderV2(market, marketMaker.address, longOrderBaseAssetQuantity, lowerPrice, getRandomSalt(), false, true) + totalRequiredMargin = await getRequiredMarginForLongOrder(longOrder4) + output = await juror.validatePlaceLimitOrder(longOrder4, marketMaker.address) + expect(output.err).to.equal("") + expectedOrderHash = await limitOrderBook.getOrderHash(longOrder4) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.reserveAmount.toNumber()).to.equal(totalRequiredMargin.toNumber()) + expectedAmmAddress = await clearingHouse.amms(market) + expect(output.res.amm).to.equal(expectedAmmAddress) + + // place the order + output = await placeOrderFromLimitOrderV2(longOrder4, marketMaker) + orderStatus = await limitOrderBook.orderStatus(expectedOrderHash) + expect(orderStatus.status).to.equal(1) + expect(orderStatus.reservedMargin.toNumber()).to.equal(totalRequiredMargin.toNumber()) + expect(orderStatus.blockPlaced.toNumber()).to.equal(output.txReceipt.blockNumber) + expect(orderStatus.filledAmount.toNumber()).to.equal(0) + }) + it("should succeed if market maker tries to place another postonly shortOrder with higher price than all longOrders", async function() { + higherPrice = longOrder4.price.add(1) + shortOrder4 = getOrderV2(market, marketMaker.address, shortOrderBaseAssetQuantity, higherPrice, getRandomSalt(), false, true) + totalRequiredMargin = await getRequiredMarginForShortOrder(shortOrder4) + output = await juror.validatePlaceLimitOrder(shortOrder4, marketMaker.address) + expect(output.err).to.equal("") + expectedOrderHash = await limitOrderBook.getOrderHash(shortOrder4) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.reserveAmount.toNumber()).to.equal(totalRequiredMargin.toNumber()) + expectedAmmAddress = await clearingHouse.amms(market) + + // place the order + output = await placeOrderFromLimitOrderV2(shortOrder4, marketMaker) + orderStatus = await limitOrderBook.orderStatus(expectedOrderHash) + expect(orderStatus.status).to.equal(1) + expect(orderStatus.reservedMargin.toNumber()).to.equal(totalRequiredMargin.toNumber()) + expect(orderStatus.blockPlaced.toNumber()).to.equal(output.txReceipt.blockNumber) + expect(orderStatus.filledAmount.toNumber()).to.equal(0) + }) + }) + + context("should succeed when market maker tries to cancel postonly orders", async function() { + it("should succeed if market maker tries to cancel longOrder", async function() { + // cancel longOrder + output = await juror.validateCancelLimitOrder(longOrder, marketMaker.address, false) + expect(output.err).to.equal("") + expectedOrderHash = await limitOrderBook.getOrderHash(longOrder) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.unfilledAmount.toString()).to.equal(longOrder.baseAssetQuantity.toString()) + expect(output.res.amm).to.equal(await clearingHouse.amms(market)) + + await cancelOrderFromLimitOrderV2(longOrder, marketMaker) + orderStatus = await limitOrderBook.orderStatus(expectedOrderHash) + expect(orderStatus.status).to.equal(3) + expect(orderStatus.reservedMargin.toNumber()).to.equal(0) + expect(orderStatus.blockPlaced.toNumber()).to.equal(0) + expect(orderStatus.filledAmount.toNumber()).to.equal(0) + + // cancel longOrder4 + output = await juror.validateCancelLimitOrder(longOrder4, marketMaker.address, false) + expect(output.err).to.equal("") + expectedOrderHash = await limitOrderBook.getOrderHash(longOrder4) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.unfilledAmount.toString()).to.equal(longOrder4.baseAssetQuantity.toString()) + expect(output.res.amm).to.equal(await clearingHouse.amms(market)) + + await cancelOrderFromLimitOrderV2(longOrder4, marketMaker) + orderStatus = await limitOrderBook.orderStatus(expectedOrderHash) + expect(orderStatus.status).to.equal(3) + expect(orderStatus.reservedMargin.toNumber()).to.equal(0) + expect(orderStatus.blockPlaced.toNumber()).to.equal(0) + expect(orderStatus.filledAmount.toNumber()).to.equal(0) + }) + it("should succeed if market maker tries to cancel shortOrder", async function() { + // cancel shortOrder + output = await juror.validateCancelLimitOrder(shortOrder, marketMaker.address, false) + expect(output.err).to.equal("") + expectedOrderHash = await limitOrderBook.getOrderHash(shortOrder) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.unfilledAmount.toString()).to.equal(shortOrder.baseAssetQuantity.toString()) + expect(output.res.amm).to.equal(await clearingHouse.amms(market)) + + await cancelOrderFromLimitOrderV2(shortOrder, marketMaker) + orderStatus = await limitOrderBook.orderStatus(expectedOrderHash) + expect(orderStatus.status).to.equal(3) + expect(orderStatus.reservedMargin.toNumber()).to.equal(0) + expect(orderStatus.blockPlaced.toNumber()).to.equal(0) + expect(orderStatus.filledAmount.toNumber()).to.equal(0) + + // cancel shortOrder4 + output = await juror.validateCancelLimitOrder(shortOrder4, marketMaker.address, false) + expect(output.err).to.equal("") + expectedOrderHash = await limitOrderBook.getOrderHash(shortOrder4) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.unfilledAmount.toString()).to.equal(shortOrder4.baseAssetQuantity.toString()) + expect(output.res.amm).to.equal(await clearingHouse.amms(market)) + + await cancelOrderFromLimitOrderV2(shortOrder4, marketMaker) + orderStatus = await limitOrderBook.orderStatus(expectedOrderHash) + expect(orderStatus.status).to.equal(3) + expect(orderStatus.reservedMargin.toNumber()).to.equal(0) + expect(orderStatus.blockPlaced.toNumber()).to.equal(0) + expect(orderStatus.filledAmount.toNumber()).to.equal(0) + }) + }) + context("should fail if market maker tries to cancel same orders again", async function() { + it("should fail if market maker tries to cancel same longOrders again", async function() { + // cancel longOrder + output = await juror.validateCancelLimitOrder(longOrder, marketMaker.address, false) + expect(output.err).to.equal("Cancelled") + expectedOrderHash = await limitOrderBook.getOrderHash(longOrder) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.unfilledAmount.toString()).to.equal("0") + expect(output.res.amm).to.equal("0x0000000000000000000000000000000000000000") + + // cancel longOrder4 + output = await juror.validateCancelLimitOrder(longOrder4, marketMaker.address, false) + expect(output.err).to.equal("Cancelled") + expectedOrderHash = await limitOrderBook.getOrderHash(longOrder4) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.unfilledAmount.toString()).to.equal("0") + expect(output.res.amm).to.equal("0x0000000000000000000000000000000000000000") + }) + it("should fail if market maker tries to cancel same shortOrders again", async function() { + // cancel shortOrder + output = await juror.validateCancelLimitOrder(shortOrder, marketMaker.address, false) + expect(output.err).to.equal("Cancelled") + expectedOrderHash = await limitOrderBook.getOrderHash(shortOrder) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.unfilledAmount.toString()).to.equal("0") + + // cancel shortOrder4 + output = await juror.validateCancelLimitOrder(shortOrder4, marketMaker.address, false) + expect(output.err).to.equal("Cancelled") + expectedOrderHash = await limitOrderBook.getOrderHash(shortOrder4) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.unfilledAmount.toString()).to.equal("0") + }) + }) + }) + context("When users have positions and then try to place/cancel orders", async function() { + // Alice has long Position and bob has short position + // If reduceOnly order is longOrder - it should fail + // Alice tries to place a short reduceOnly order when she has an open shortOrder - it should fail + // when there is no open shortOrder for alice and alice tries to place a short reduceOnly order - it should succeed + // after placing short reduceOnly order, alice tries to place a normal shortOrder - it should fail + // if currentOrder size + (sum of size of all reduceOnly orders) > posSize of alice - it should fail + // if currentOrder size + (sum of size of all reduceOnly orders) < posSize of alice - it should succeed + // should succeed if alice tries to place a longOrder while having a open reduceOnlyShortOrder + // should fail if alice tries to post a postOnly + ReduceOnly shortOrder which crosses the market + // alice should be able to cancel all open orders for alice + let shortOrderBaseAssetQuantity = multiplySize(-0.1) // 0.1 ether + let longOrderBaseAssetQuantity = multiplySize(0.1) // 0.1 ether + let longOrderPrice = multiplyPrice(1800) + let shortOrderPrice = multiplyPrice(1800) + let market = BigNumber.from(0) + let longOrder = getOrderV2(market, alice.address, longOrderBaseAssetQuantity, longOrderPrice, getRandomSalt(), false, false) + let shortOrder = getOrderV2(market, bob.address, shortOrderBaseAssetQuantity, shortOrderPrice, getRandomSalt(), false, false) + + this.beforeAll(async function() { + await addMargin(alice, multiplyPrice(150000)) + await addMargin(bob, multiplyPrice(150000)) + await placeOrderFromLimitOrderV2(longOrder, alice) + await placeOrderFromLimitOrderV2(shortOrder, bob) + await waitForOrdersToMatch() + }) + this.afterAll(async function() { + let oppositeShortOrder = getOrderV2(market, alice.address, shortOrderBaseAssetQuantity, shortOrderPrice, getRandomSalt(), false, false) + let oppositeLongOrder = getOrderV2(market, bob.address, longOrderBaseAssetQuantity, longOrderPrice, getRandomSalt(), false, false) + await placeOrderFromLimitOrderV2(oppositeShortOrder, alice) + await placeOrderFromLimitOrderV2(oppositeLongOrder, bob) + await waitForOrdersToMatch() + await removeAllAvailableMargin(alice) + await removeAllAvailableMargin(bob) + }) + + context("try to place longOrder and shortOrder again", async function() { + it("should fail if alice tries to place longOrder again", async function() { + output = await juror.validatePlaceLimitOrder(longOrder, alice.address) + expect(output.err).to.equal("order already exists") + expectedOrderHash = await limitOrderBook.getOrderHash(longOrder) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.reserveAmount.toNumber()).to.equal(0) + expectedAmmAddress = await clearingHouse.amms(market) + expect(output.res.amm).to.equal(expectedAmmAddress) + + output = await placeOrderFromLimitOrderV2(longOrder, alice) + limitOrderBookLogWithEvent = (await getEventsFromLimitOrderBookTx(output.txReceipt.transactionHash))[0] + expect(limitOrderBookLogWithEvent.event).to.equal("OrderRejected") + expect(limitOrderBookLogWithEvent.args.err).to.equal("order already exists") + expect(limitOrderBookLogWithEvent.args.orderHash).to.equal(expectedOrderHash) + expect(limitOrderBookLogWithEvent.args.trader).to.equal(longOrder.trader) + }) + it('should fail if bob tries to place shortOrder again', async function() { + output = await juror.validatePlaceLimitOrder(shortOrder, bob.address) + expect(output.err).to.equal("order already exists") + expectedOrderHash = await limitOrderBook.getOrderHash(shortOrder) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.reserveAmount.toNumber()).to.equal(0) + expectedAmmAddress = await clearingHouse.amms(market) + expect(output.res.amm).to.equal(expectedAmmAddress) + + output = await placeOrderFromLimitOrderV2(shortOrder, bob) + limitOrderBookLogWithEvent = (await getEventsFromLimitOrderBookTx(output.txReceipt.transactionHash))[0] + expect(limitOrderBookLogWithEvent.event).to.equal("OrderRejected") + expect(limitOrderBookLogWithEvent.args.err).to.equal("order already exists") + expect(limitOrderBookLogWithEvent.args.orderHash).to.equal(expectedOrderHash) + expect(limitOrderBookLogWithEvent.args.trader).to.equal(shortOrder.trader) + }) + }) + context("try to cancel longOrder and shortOrder which are already filled", async function() { + it("should fail if alice tries to cancel longOrder", async function() { + output = await juror.validateCancelLimitOrder(longOrder, alice.address, false) + expect(output.err).to.equal("Filled") + expectedOrderHash = await limitOrderBook.getOrderHash(longOrder) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.unfilledAmount.toString()).to.equal("0") + expect(output.res.amm).to.equal("0x0000000000000000000000000000000000000000") + + await cancelOrderFromLimitOrderV2(longOrder, alice) + orderStatus = await limitOrderBook.orderStatus(expectedOrderHash) + expect(orderStatus.status).to.equal(2) + expect(orderStatus.reservedMargin.toNumber()).to.equal(0) + expect(orderStatus.blockPlaced.toNumber()).to.equal(0) + expect(orderStatus.filledAmount.toString()).to.equal(longOrder.baseAssetQuantity.toString()) + }) + it("should fail if bob tries to cancel shortOrder", async function() { + output = await juror.validateCancelLimitOrder(shortOrder, bob.address, false) + expect(output.err).to.equal("Filled") + expectedOrderHash = await limitOrderBook.getOrderHash(shortOrder) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.unfilledAmount.toString()).to.equal("0") + expect(output.res.amm).to.equal("0x0000000000000000000000000000000000000000") + + await cancelOrderFromLimitOrderV2(shortOrder, bob) + orderStatus = await limitOrderBook.orderStatus(expectedOrderHash) + expect(orderStatus.status).to.equal(2) + expect(orderStatus.reservedMargin.toNumber()).to.equal(0) + expect(orderStatus.blockPlaced.toNumber()).to.equal(0) + expect(orderStatus.filledAmount.toString()).to.equal(shortOrder.baseAssetQuantity.toString()) + }) + }) + context("alice has long position", async function() { + it("should fail if alice tries to place a long reduceOnly order", async function() { + //ensure position is created for alice + orderStatus = await limitOrderBook.orderStatus(await limitOrderBook.getOrderHash(longOrder)) + expect(orderStatus.status).to.equal(2) + expect(orderStatus.filledAmount.toString()).to.equal(longOrder.baseAssetQuantity.toString()) + expect(orderStatus.reservedMargin.toNumber()).to.equal(0) + expect(orderStatus.blockPlaced.toNumber()).to.equal(0) + + orderSize = longOrderBaseAssetQuantity.div(2) + let reduceOnlyLongOrder = getOrderV2(market, alice.address, orderSize, longOrderPrice, getRandomSalt(), true, false) + output = await juror.validatePlaceLimitOrder(reduceOnlyLongOrder, alice.address) + expect(output.err).to.equal("reduce only order must reduce position") + expectedOrderHash = await limitOrderBook.getOrderHash(reduceOnlyLongOrder) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.reserveAmount.toNumber()).to.equal(0) + expectedAmmAddress = await clearingHouse.amms(market) + expect(output.res.amm).to.equal(expectedAmmAddress) + + // place the order + output = await placeOrderFromLimitOrderV2(reduceOnlyLongOrder, alice) + limitOrderBookLogWithEvent = (await getEventsFromLimitOrderBookTx(output.txReceipt.transactionHash))[0] + expect(limitOrderBookLogWithEvent.event).to.equal("OrderRejected") + expect(limitOrderBookLogWithEvent.args.err).to.equal("reduce only order must reduce position") + expect(limitOrderBookLogWithEvent.args.orderHash).to.equal(expectedOrderHash) + expect(limitOrderBookLogWithEvent.args.trader).to.equal(reduceOnlyLongOrder.trader) + }) + it("should fail when alice has a open shortOrder and tries to place a short reduceOnly order", async function() { + let shortOrderBaseAssetQuantity = longOrderBaseAssetQuantity.div(2).mul(-1) + let shortOrder = getOrderV2(market, alice.address, shortOrderBaseAssetQuantity, shortOrderPrice, getRandomSalt(), false, false) + requiredMargin = await getRequiredMarginForShortOrder(shortOrder) + await addMargin(alice, requiredMargin) + await placeOrderFromLimitOrderV2(shortOrder, alice) + + let reduceOnlyShortOrder = getOrderV2(market, alice.address, shortOrderBaseAssetQuantity, shortOrderPrice, getRandomSalt(), true, false) + output = await juror.validatePlaceLimitOrder(reduceOnlyShortOrder, alice.address) + expect(output.err).to.equal("open orders") + expectedOrderHash = await limitOrderBook.getOrderHash(reduceOnlyShortOrder) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.reserveAmount.toNumber()).to.equal(0) + expectedAmmAddress = await clearingHouse.amms(market) + expect(output.res.amm).to.equal(expectedAmmAddress) + + // place the order + output = await placeOrderFromLimitOrderV2(reduceOnlyShortOrder, alice) + limitOrderBookLogWithEvent = (await getEventsFromLimitOrderBookTx(output.txReceipt.transactionHash))[0] + expect(limitOrderBookLogWithEvent.event).to.equal("OrderRejected") + expect(limitOrderBookLogWithEvent.args.err).to.equal("open orders") + expect(limitOrderBookLogWithEvent.args.orderHash).to.equal(expectedOrderHash) + expect(limitOrderBookLogWithEvent.args.trader).to.equal(reduceOnlyShortOrder.trader) + + await cancelOrderFromLimitOrderV2(shortOrder, alice) + }) + let reduceOnlyShortOrder + it("should succeed if alice tries to place a short reduceOnly order", async function() { + orderSize = longOrderBaseAssetQuantity.div(2).mul(-1) + reduceOnlyShortOrder = getOrderV2(market, alice.address, orderSize, shortOrderPrice, getRandomSalt(), true, false) + output = await juror.validatePlaceLimitOrder(reduceOnlyShortOrder, alice.address) + expect(output.err).to.equal("") + expectedOrderHash = await limitOrderBook.getOrderHash(reduceOnlyShortOrder) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.reserveAmount.toNumber()).to.equal(0) + expectedAmmAddress = await clearingHouse.amms(market) + expect(output.res.amm).to.equal(expectedAmmAddress) + + // place the order + output = await placeOrderFromLimitOrderV2(reduceOnlyShortOrder, alice) + orderStatus = await limitOrderBook.orderStatus(expectedOrderHash) + expect(orderStatus.status).to.equal(1) + expect(orderStatus.reservedMargin.toNumber()).to.equal(0) + expect(orderStatus.blockPlaced.toNumber()).to.equal(output.txReceipt.blockNumber) + expect(orderStatus.filledAmount.toNumber()).to.equal(0) + }) + it("should fail if alice tries to place a normal shortOrder(reduceOnly=false) to decrease her position after placing a short reduceOnly order", async function() { + let shortOrder2 = getOrderV2(market, alice.address, shortOrderBaseAssetQuantity, longOrderPrice, getRandomSalt(), false, false) + output = await juror.validatePlaceLimitOrder(shortOrder2, alice.address) + expect(output.err).to.equal("open reduce only orders") + expectedOrderHash = await limitOrderBook.getOrderHash(shortOrder2) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.reserveAmount.toNumber()).to.equal(0) + expectedAmmAddress = await clearingHouse.amms(market) + expect(output.res.amm).to.equal(expectedAmmAddress) + + // place the order + output = await placeOrderFromLimitOrderV2(shortOrder2, alice) + limitOrderBookLogWithEvent = (await getEventsFromLimitOrderBookTx(output.txReceipt.transactionHash))[0] + expect(limitOrderBookLogWithEvent.event).to.equal("OrderRejected") + expect(limitOrderBookLogWithEvent.args.err).to.equal("open reduce only orders") + expect(limitOrderBookLogWithEvent.args.orderHash).to.equal(expectedOrderHash) + expect(limitOrderBookLogWithEvent.args.trader).to.equal(shortOrder2.trader) + }) + it("should fail if alice tries to place a short reduceOnly order with size > posSize - reduceOnlyShortOrder.baseAssetQuantity", async function() { + minSizeRequirement = await getMinSizeRequirement(market) + let shortOrder3Size = longOrderBaseAssetQuantity.sub(reduceOnlyShortOrder.baseAssetQuantity.abs()).add(minSizeRequirement).mul(-1) + let shortOrder3 = getOrderV2(market, alice.address, shortOrder3Size, shortOrderPrice, getRandomSalt(), true, false) + output = await juror.validatePlaceLimitOrder(shortOrder3, alice.address) + expect(output.err).to.equal("net reduce only amount exceeded") + expectedOrderHash = await limitOrderBook.getOrderHash(shortOrder3) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.reserveAmount.toNumber()).to.equal(0) + expectedAmmAddress = await clearingHouse.amms(market) + expect(output.res.amm).to.equal(expectedAmmAddress) + + // place the order + output = await placeOrderFromLimitOrderV2(shortOrder3, alice) + limitOrderBookLogWithEvent = (await getEventsFromLimitOrderBookTx(output.txReceipt.transactionHash))[0] + expect(limitOrderBookLogWithEvent.event).to.equal("OrderRejected") + expect(limitOrderBookLogWithEvent.args.err).to.equal("net reduce only amount exceeded") + expect(limitOrderBookLogWithEvent.args.orderHash).to.equal(expectedOrderHash) + expect(limitOrderBookLogWithEvent.args.trader).to.equal(shortOrder3.trader) + }) + let reduceOnlyShortOrder2 + it("should succeed if alice tries to place a short reduceOnly order with size <= posSize - reduceOnlyShortOrder.baseAssetQuantity", async function() { + let reduceOnlyShortOrder2Size = longOrderBaseAssetQuantity.sub(reduceOnlyShortOrder.baseAssetQuantity.abs()).mul(-1) + reduceOnlyShortOrder2 = getOrderV2(market, alice.address, reduceOnlyShortOrder2Size, shortOrderPrice, getRandomSalt(), true, false) + output = await juror.validatePlaceLimitOrder(reduceOnlyShortOrder2, alice.address) + expect(output.err).to.equal("") + expectedOrderHash = await limitOrderBook.getOrderHash(reduceOnlyShortOrder2) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.reserveAmount.toNumber()).to.equal(0) + expectedAmmAddress = await clearingHouse.amms(market) + expect(output.res.amm).to.equal(expectedAmmAddress) + + // place the order + output = await placeOrderFromLimitOrderV2(reduceOnlyShortOrder2, alice) + orderStatus = await limitOrderBook.orderStatus(expectedOrderHash) + expect(orderStatus.status).to.equal(1) + expect(orderStatus.reservedMargin.toNumber()).to.equal(0) + expect(orderStatus.blockPlaced.toNumber()).to.equal(output.txReceipt.blockNumber) + expect(orderStatus.filledAmount.toNumber()).to.equal(0) + }) + it('should succeed if alice tries to cancel reduceOnlyShortOrder2', async function() { + output = await juror.validateCancelLimitOrder(reduceOnlyShortOrder2, alice.address, false) + expect(output.err).to.equal("") + expectedOrderHash = await limitOrderBook.getOrderHash(reduceOnlyShortOrder2) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.unfilledAmount.toString()).to.equal(reduceOnlyShortOrder2.baseAssetQuantity.toString()) + expect(output.res.amm).to.equal(await clearingHouse.amms(market)) + + await cancelOrderFromLimitOrderV2(reduceOnlyShortOrder2, alice) + orderStatus = await limitOrderBook.orderStatus(expectedOrderHash) + expect(orderStatus.status).to.equal(3) + expect(orderStatus.reservedMargin.toNumber()).to.equal(0) + expect(orderStatus.blockPlaced.toNumber()).to.equal(0) + expect(orderStatus.filledAmount.toNumber()).to.equal(0) + }) + let longOrderNormal + it('should succeed if alice tries to place a longOrder while having a open reduceOnlyShortOrder', async function() { + // so that longOrderNormal does not matches with reduceOnlyShortOrder + price = reduceOnlyShortOrder.price.sub(1) + longOrderNormal = getOrderV2(market, alice.address, longOrderBaseAssetQuantity, price, getRandomSalt(), false, false) + expectedReserveAmount = await getRequiredMarginForLongOrder(longOrderNormal) + output = await juror.validatePlaceLimitOrder(longOrderNormal, alice.address) + expect(output.err).to.equal("") + expectedOrderHash = await limitOrderBook.getOrderHash(longOrderNormal) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.reserveAmount.toNumber()).to.equal(expectedReserveAmount.toNumber()) + expectedAmmAddress = await clearingHouse.amms(market) + expect(output.res.amm).to.equal(expectedAmmAddress) + + // place the order + output = await placeOrderFromLimitOrderV2(longOrderNormal, alice) + orderStatus = await limitOrderBook.orderStatus(expectedOrderHash) + expect(orderStatus.status).to.equal(1) + expect(orderStatus.reservedMargin.toNumber()).to.equal(expectedReserveAmount.toNumber()) + expect(orderStatus.blockPlaced.toNumber()).to.equal(output.txReceipt.blockNumber) + expect(orderStatus.filledAmount.toNumber()).to.equal(0) + }) + it('should fail if alice tries to post a postOnly + ReduceOnly shortOrder which crosses the market', async function() { + crossingPrice = longOrderNormal.price + size = shortOrderBaseAssetQuantity.sub(reduceOnlyShortOrder.baseAssetQuantity) + shortReduceOnlyPostOnlyOrder = getOrderV2(market, alice.address, size, crossingPrice, getRandomSalt(), true, true) + output = await juror.validatePlaceLimitOrder(shortReduceOnlyPostOnlyOrder, alice.address) + expect(output.err).to.equal("crossing market") + expectedOrderHash = await limitOrderBook.getOrderHash(shortReduceOnlyPostOnlyOrder) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.reserveAmount.toNumber()).to.equal(0) + expectedAmmAddress = await clearingHouse.amms(market) + + // place the order + output = await placeOrderFromLimitOrderV2(shortReduceOnlyPostOnlyOrder, alice) + limitOrderBookLogWithEvent = (await getEventsFromLimitOrderBookTx(output.txReceipt.transactionHash))[0] + expect(limitOrderBookLogWithEvent.event).to.equal("OrderRejected") + expect(limitOrderBookLogWithEvent.args.err).to.equal("crossing market") + expect(limitOrderBookLogWithEvent.args.orderHash).to.equal(expectedOrderHash) + expect(limitOrderBookLogWithEvent.args.trader).to.equal(shortReduceOnlyPostOnlyOrder.trader) + }) + it('should succeed if alice tries to cancel reduceOnlyShortOrder and longOrderNormal', async function() { + // cancel reduceOnlyShortOrder + output = await juror.validateCancelLimitOrder(reduceOnlyShortOrder, alice.address, false) + expect(output.err).to.equal("") + expectedOrderHash = await limitOrderBook.getOrderHash(reduceOnlyShortOrder) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.unfilledAmount.toString()).to.equal(reduceOnlyShortOrder.baseAssetQuantity.toString()) + expect(output.res.amm).to.equal(await clearingHouse.amms(market)) + + await cancelOrderFromLimitOrderV2(reduceOnlyShortOrder, alice) + orderStatus = await limitOrderBook.orderStatus(expectedOrderHash) + expect(orderStatus.status).to.equal(3) + expect(orderStatus.reservedMargin.toNumber()).to.equal(0) + expect(orderStatus.blockPlaced.toNumber()).to.equal(0) + expect(orderStatus.filledAmount.toNumber()).to.equal(0) + + // cancel longOrderNormal + output = await juror.validateCancelLimitOrder(longOrderNormal, alice.address, false) + expect(output.err).to.equal("") + expectedOrderHash = await limitOrderBook.getOrderHash(longOrderNormal) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.unfilledAmount.toString()).to.equal(longOrderNormal.baseAssetQuantity.toString()) + expect(output.res.amm).to.equal(await clearingHouse.amms(market)) + + await cancelOrderFromLimitOrderV2(longOrderNormal, alice) + orderStatus = await limitOrderBook.orderStatus(expectedOrderHash) + expect(orderStatus.status).to.equal(3) + expect(orderStatus.reservedMargin.toNumber()).to.equal(0) + expect(orderStatus.blockPlaced.toNumber()).to.equal(0) + expect(orderStatus.filledAmount.toNumber()).to.equal(0) + }) + }) + context("bob has short position", async function() { + // Bob hash short Position + // Bob tries to close half of his position via ui(so places reduceOnly order) + // If reduceOnly order is shortOrder - it should fail + // If reduceOnly order is longOrder - it should succeed + // if there are open longOrders for bob reduceOnly order should fail + // if order"s size + openReduceOnlyAmount > posSize of bob - it should fail + // if currentOrder size + (sum of size of all reduceOnly orders) < posSize of bob - it should succeed + // should succeed if bob tries to place a shortOrder while having a open reduceOnlyLongOrder + // should fail if bob tries to post a postOnly + ReduceOnly longOrder which crosses the market + // bob should be able to cancel all open orders for bob + it("should fail if bob tries to place a short reduceOnly order", async function() { + //ensure position is created for bob + orderStatus = await limitOrderBook.orderStatus(await limitOrderBook.getOrderHash(shortOrder)) + expect(orderStatus.status).to.equal(2) + expect(orderStatus.filledAmount.toString()).to.equal(shortOrder.baseAssetQuantity.toString()) + expect(orderStatus.reservedMargin.toNumber()).to.equal(0) + expect(orderStatus.blockPlaced.toNumber()).to.equal(0) + + orderSize = shortOrderBaseAssetQuantity.div(2) + let reduceOnlyShortOrder = getOrderV2(market, bob.address, orderSize, shortOrderPrice, getRandomSalt(), true, false) + output = await juror.validatePlaceLimitOrder(reduceOnlyShortOrder, bob.address) + expect(output.err).to.equal("reduce only order must reduce position") + expectedOrderHash = await limitOrderBook.getOrderHash(reduceOnlyShortOrder) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.reserveAmount.toNumber()).to.equal(0) + expectedAmmAddress = await clearingHouse.amms(market) + expect(output.res.amm).to.equal(expectedAmmAddress) + + // place the order + output = await placeOrderFromLimitOrderV2(reduceOnlyShortOrder, bob) + limitOrderBookLogWithEvent = (await getEventsFromLimitOrderBookTx(output.txReceipt.transactionHash))[0] + expect(limitOrderBookLogWithEvent.event).to.equal("OrderRejected") + expect(limitOrderBookLogWithEvent.args.err).to.equal("reduce only order must reduce position") + expect(limitOrderBookLogWithEvent.args.orderHash).to.equal(expectedOrderHash) + expect(limitOrderBookLogWithEvent.args.trader).to.equal(reduceOnlyShortOrder.trader) + }) + it("should fail when bob has a open longOrder and tries to place a long reduceOnly order", async function() { + let longOrderSize = shortOrderBaseAssetQuantity.div(2).mul(-1) + let longOrder = getOrderV2(market, bob.address, longOrderSize, longOrderPrice, getRandomSalt(), false, false) + requiredMargin = await getRequiredMarginForLongOrder(longOrder) + await addMargin(bob, requiredMargin) + await placeOrderFromLimitOrderV2(longOrder, bob) + + let reduceOnlyLongOrder = getOrderV2(market, bob.address, longOrderBaseAssetQuantity, longOrderPrice, getRandomSalt(), true, false) + output = await juror.validatePlaceLimitOrder(reduceOnlyLongOrder, bob.address) + expect(output.err).to.equal("open orders") + expectedOrderHash = await limitOrderBook.getOrderHash(reduceOnlyLongOrder) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.reserveAmount.toNumber()).to.equal(0) + expectedAmmAddress = await clearingHouse.amms(market) + expect(output.res.amm).to.equal(expectedAmmAddress) + + // place the order + output = await placeOrderFromLimitOrderV2(reduceOnlyLongOrder, bob) + limitOrderBookLogWithEvent = (await getEventsFromLimitOrderBookTx(output.txReceipt.transactionHash))[0] + expect(limitOrderBookLogWithEvent.event).to.equal("OrderRejected") + expect(limitOrderBookLogWithEvent.args.err).to.equal("open orders") + expect(limitOrderBookLogWithEvent.args.orderHash).to.equal(expectedOrderHash) + expect(limitOrderBookLogWithEvent.args.trader).to.equal(reduceOnlyLongOrder.trader) + + await cancelOrderFromLimitOrderV2(longOrder, bob) + }) + let reduceOnlyLongOrder + it('should succeed if bob tries to place a long reduceOnly order', async function() { + orderSize = shortOrderBaseAssetQuantity.div(2).mul(-1) + reduceOnlyLongOrder = getOrderV2(market, bob.address, orderSize, longOrderPrice, getRandomSalt(), true, false) + output = await juror.validatePlaceLimitOrder(reduceOnlyLongOrder, bob.address) + expect(output.err).to.equal("") + expectedOrderHash = await limitOrderBook.getOrderHash(reduceOnlyLongOrder) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.reserveAmount.toNumber()).to.equal(0) + expectedAmmAddress = await clearingHouse.amms(market) + expect(output.res.amm).to.equal(expectedAmmAddress) + + // place the order + output = await placeOrderFromLimitOrderV2(reduceOnlyLongOrder, bob) + orderStatus = await limitOrderBook.orderStatus(expectedOrderHash) + expect(orderStatus.status).to.equal(1) + expect(orderStatus.reservedMargin.toNumber()).to.equal(0) + expect(orderStatus.blockPlaced.toNumber()).to.equal(output.txReceipt.blockNumber) + expect(orderStatus.filledAmount.toNumber()).to.equal(0) + }) + it("should fail if bob tries to place a normal longOrder(reduceOnly=false) to decrease his position after placing a long reduceOnly order", async function() { + let longOrder2 = getOrderV2(market, bob.address, longOrderBaseAssetQuantity, shortOrderPrice, getRandomSalt(), false, false) + output = await juror.validatePlaceLimitOrder(longOrder2, bob.address) + expect(output.err).to.equal("open reduce only orders") + expectedOrderHash = await limitOrderBook.getOrderHash(longOrder2) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.reserveAmount.toNumber()).to.equal(0) + expectedAmmAddress = await clearingHouse.amms(market) + expect(output.res.amm).to.equal(expectedAmmAddress) + + // place the order + output = await placeOrderFromLimitOrderV2(longOrder2, bob) + limitOrderBookLogWithEvent = (await getEventsFromLimitOrderBookTx(output.txReceipt.transactionHash))[0] + expect(limitOrderBookLogWithEvent.event).to.equal("OrderRejected") + expect(limitOrderBookLogWithEvent.args.err).to.equal("open reduce only orders") + expect(limitOrderBookLogWithEvent.args.orderHash).to.equal(expectedOrderHash) + expect(limitOrderBookLogWithEvent.args.trader).to.equal(longOrder2.trader) + }) + it('should fail if bob tries to place a long reduceOnly order with size > posSize - reduceOnlyLongOrder.baseAssetQuantity', async function() { + minSizeRequirement = await getMinSizeRequirement(market) + let longOrder3Size = shortOrderBaseAssetQuantity.abs().sub(reduceOnlyLongOrder.baseAssetQuantity).add(minSizeRequirement) + let longOrder3 = getOrderV2(market, bob.address, longOrder3Size, longOrderPrice, getRandomSalt(), true, false) + output = await juror.validatePlaceLimitOrder(longOrder3, bob.address) + expect(output.err).to.equal("net reduce only amount exceeded") + expectedOrderHash = await limitOrderBook.getOrderHash(longOrder3) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.reserveAmount.toNumber()).to.equal(0) + expectedAmmAddress = await clearingHouse.amms(market) + expect(output.res.amm).to.equal(expectedAmmAddress) + + // place the order + output = await placeOrderFromLimitOrderV2(longOrder3, bob) + limitOrderBookLogWithEvent = (await getEventsFromLimitOrderBookTx(output.txReceipt.transactionHash))[0] + expect(limitOrderBookLogWithEvent.event).to.equal("OrderRejected") + expect(limitOrderBookLogWithEvent.args.err).to.equal("net reduce only amount exceeded") + expect(limitOrderBookLogWithEvent.args.orderHash).to.equal(expectedOrderHash) + expect(limitOrderBookLogWithEvent.args.trader).to.equal(longOrder3.trader) + }) + let reduceOnlyLongOrder2 + it('should succeed if bob tries to place a long reduceOnly order with size <= posSize - reduceOnlyLongOrder.baseAssetQuantity', async function() { + let reduceOnlyLongOrder2Size = shortOrderBaseAssetQuantity.abs().sub(reduceOnlyLongOrder.baseAssetQuantity) + reduceOnlyLongOrder2 = getOrderV2(market, bob.address, reduceOnlyLongOrder2Size, longOrderPrice, getRandomSalt(), true, false) + output = await juror.validatePlaceLimitOrder(reduceOnlyLongOrder2, bob.address) + expect(output.err).to.equal("") + expectedOrderHash = await limitOrderBook.getOrderHash(reduceOnlyLongOrder2) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.reserveAmount.toNumber()).to.equal(0) + expectedAmmAddress = await clearingHouse.amms(market) + expect(output.res.amm).to.equal(expectedAmmAddress) + + // place the order + output = await placeOrderFromLimitOrderV2(reduceOnlyLongOrder2, bob) + orderStatus = await limitOrderBook.orderStatus(expectedOrderHash) + expect(orderStatus.status).to.equal(1) + expect(orderStatus.reservedMargin.toNumber()).to.equal(0) + expect(orderStatus.blockPlaced.toNumber()).to.equal(output.txReceipt.blockNumber) + expect(orderStatus.filledAmount.toNumber()).to.equal(0) + }) + it('should succeed if bob tries to cancel reduceOnlyShortOrder2', async function() { + output = await juror.validateCancelLimitOrder(reduceOnlyLongOrder2, bob.address, false) + expect(output.err).to.equal("") + expectedOrderHash = await limitOrderBook.getOrderHash(reduceOnlyLongOrder2) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.unfilledAmount.toString()).to.equal(reduceOnlyLongOrder2.baseAssetQuantity.toString()) + expect(output.res.amm).to.equal(await clearingHouse.amms(market)) + + await cancelOrderFromLimitOrderV2(reduceOnlyLongOrder2, bob) + orderStatus = await limitOrderBook.orderStatus(expectedOrderHash) + expect(orderStatus.status).to.equal(3) + expect(orderStatus.reservedMargin.toNumber()).to.equal(0) + expect(orderStatus.blockPlaced.toNumber()).to.equal(0) + expect(orderStatus.filledAmount.toNumber()).to.equal(0) + }) + let shortOrderNormal + it('should succeed if bob tries to place a shortOrder while having a open reduceOnlyLongOrder', async function() { + // so that shortOrderNormal does not matches with reduceOnlyLongOrder + price = reduceOnlyLongOrder.price.add(1) + shortOrderNormal = getOrderV2(market, bob.address, shortOrderBaseAssetQuantity, price, getRandomSalt(), false, false) + expectedReserveAmount = await getRequiredMarginForShortOrder(shortOrderNormal) + output = await juror.validatePlaceLimitOrder(shortOrderNormal, bob.address) + expect(output.err).to.equal("") + expectedOrderHash = await limitOrderBook.getOrderHash(shortOrderNormal) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.reserveAmount.toNumber()).to.equal(expectedReserveAmount.toNumber()) + expectedAmmAddress = await clearingHouse.amms(market) + expect(output.res.amm).to.equal(expectedAmmAddress) + + // place the order + output = await placeOrderFromLimitOrderV2(shortOrderNormal, bob) + orderStatus = await limitOrderBook.orderStatus(expectedOrderHash) + expect(orderStatus.status).to.equal(1) + expect(orderStatus.reservedMargin.toNumber()).to.equal(expectedReserveAmount.toNumber()) + expect(orderStatus.blockPlaced.toNumber()).to.equal(output.txReceipt.blockNumber) + expect(orderStatus.filledAmount.toNumber()).to.equal(0) + }) + it('should fail if bob tries to post a postOnly + ReduceOnly longOrder which crosses the market', async function() { + crossingPrice = shortOrderNormal.price + size = longOrderBaseAssetQuantity.sub(reduceOnlyLongOrder.baseAssetQuantity) + longReduceOnlyPostOnlyOrder = getOrderV2(market, bob.address, size, crossingPrice, getRandomSalt(), true, true) + output = await juror.validatePlaceLimitOrder(longReduceOnlyPostOnlyOrder, bob.address) + expect(output.err).to.equal("crossing market") + expectedOrderHash = await limitOrderBook.getOrderHash(longReduceOnlyPostOnlyOrder) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.reserveAmount.toNumber()).to.equal(0) + expectedAmmAddress = await clearingHouse.amms(market) + + // place the order + output = await placeOrderFromLimitOrderV2(longReduceOnlyPostOnlyOrder, bob) + limitOrderBookLogWithEvent = (await getEventsFromLimitOrderBookTx(output.txReceipt.transactionHash))[0] + expect(limitOrderBookLogWithEvent.event).to.equal("OrderRejected") + expect(limitOrderBookLogWithEvent.args.err).to.equal("crossing market") + expect(limitOrderBookLogWithEvent.args.orderHash).to.equal(expectedOrderHash) + expect(limitOrderBookLogWithEvent.args.trader).to.equal(longReduceOnlyPostOnlyOrder.trader) + }) + + it('should succeed if bob tries to cancel reduceOnlyLongOrder and shortOrderNormal', async function() { + // cancel reduceOnlyLongOrder + output = await juror.validateCancelLimitOrder(reduceOnlyLongOrder, bob.address, false) + expect(output.err).to.equal("") + expectedOrderHash = await limitOrderBook.getOrderHash(reduceOnlyLongOrder) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.unfilledAmount.toString()).to.equal(reduceOnlyLongOrder.baseAssetQuantity.toString()) + expect(output.res.amm).to.equal(await clearingHouse.amms(market)) + + await cancelOrderFromLimitOrderV2(reduceOnlyLongOrder, bob) + orderStatus = await limitOrderBook.orderStatus(expectedOrderHash) + expect(orderStatus.status).to.equal(3) + expect(orderStatus.reservedMargin.toNumber()).to.equal(0) + expect(orderStatus.blockPlaced.toNumber()).to.equal(0) + expect(orderStatus.filledAmount.toNumber()).to.equal(0) + + // cancel shortOrderNormal + output = await juror.validateCancelLimitOrder(shortOrderNormal, bob.address, false) + expect(output.err).to.equal("") + expectedOrderHash = await limitOrderBook.getOrderHash(shortOrderNormal) + expect(output.orderHash).to.equal(expectedOrderHash) + expect(output.res.unfilledAmount.toString()).to.equal(shortOrderNormal.baseAssetQuantity.toString()) + expect(output.res.amm).to.equal(await clearingHouse.amms(market)) + + await cancelOrderFromLimitOrderV2(shortOrderNormal, bob) + orderStatus = await limitOrderBook.orderStatus(expectedOrderHash) + expect(orderStatus.status).to.equal(3) + expect(orderStatus.reservedMargin.toNumber()).to.equal(0) + expect(orderStatus.blockPlaced.toNumber()).to.equal(0) + expect(orderStatus.filledAmount.toNumber()).to.equal(0) + }) + }) + }) +}) diff --git a/tests/orderbook/juror/getNotionalPositionAndMarginTests.js b/tests/orderbook/juror/getNotionalPositionAndMarginTests.js new file mode 100644 index 0000000000..95b2d2f17e --- /dev/null +++ b/tests/orderbook/juror/getNotionalPositionAndMarginTests.js @@ -0,0 +1,234 @@ +const { BigNumber } = require('ethers'); +const { expect } = require('chai'); + +const utils = require('../utils') + +const { + _1e6, + _1e18, + addMargin, + alice, + charlie, + clearingHouse, + getOrderV2, + getMakerFee, + getRandomSalt, + getTakerFee, + juror, + multiplyPrice, + multiplySize, + placeOrder, + placeOrderFromLimitOrderV2, + removeAllAvailableMargin, + waitForOrdersToMatch +} = utils + +// Testing juror precompile contract + +describe('Testing getNotionalPositionAndMargin',async function () { + aliceInitialMargin = multiplyPrice(BigNumber.from(600000)) + charlieInitialMargin = multiplyPrice(BigNumber.from(600000)) + aliceOrderPrice = multiplyPrice(1800) + charlieOrderPrice = multiplyPrice(1800) + aliceOrderSize = multiplySize(0.1) + charlieOrderSize = multiplySize(-0.1) + market = BigNumber.from(0) + + context('When position and margin are 0', async function () { + it('should return 0 as notionalPosition and 0 as margin', async function () { + await removeAllAvailableMargin(alice) + result = await juror.getNotionalPositionAndMargin(alice.address, false, 0) + expect(result.notionalPosition.toString()).to.equal("0") + expect(result.margin.toString()).to.equal("0") + }) + }) + + context('When position is zero but margin is non zero', async function () { + context("when user never opened a position", async function () { + this.afterAll(async function () { + await removeAllAvailableMargin(alice) + }) + it('should return 0 as notionalPosition and amount deposited as margin for trader', async function () { + await addMargin(alice, aliceInitialMargin) + + result = await juror.getNotionalPositionAndMargin(alice.address, false, 0) + expect(result.notionalPosition.toString()).to.equal("0") + expect(result.margin.toString()).to.equal(aliceInitialMargin.toString()) + }) + }) + context('when user opens and closes whole position', async function () { + this.afterAll(async function () { + await removeAllAvailableMargin(alice) + await removeAllAvailableMargin(charlie) + }) + + it('returns 0 as position and amountDeposited - ordersFee as margin', async function () { + await addMargin(alice, aliceInitialMargin) + await addMargin(charlie, charlieInitialMargin) + //create position + + longOrder = getOrderV2(market, alice.address, aliceOrderSize, aliceOrderPrice, getRandomSalt()) + await placeOrderFromLimitOrderV2(longOrder, alice) + shortOrder = getOrderV2(market, charlie.address, charlieOrderSize, charlieOrderPrice, getRandomSalt()) + await placeOrderFromLimitOrderV2(shortOrder, charlie) + await waitForOrdersToMatch() + // close position; charlie is taker for 2nd order + oppositeLongOrder = getOrderV2(market, charlie.address, aliceOrderSize, aliceOrderPrice, getRandomSalt()) + await placeOrderFromLimitOrderV2(oppositeLongOrder, charlie) + oppositeShortOrder = getOrderV2(market, alice.address, charlieOrderSize, charlieOrderPrice, getRandomSalt()) + await placeOrderFromLimitOrderV2(oppositeShortOrder, alice) + await waitForOrdersToMatch() + + makerFee = await getMakerFee() + takerFee = await getTakerFee() + + resultCharlie = await juror.getNotionalPositionAndMargin(charlie.address, false, 0) + charlieOrder1Fee = makerFee.mul(charlieOrderSize.abs()).mul(charlieOrderPrice).div(_1e18).div(_1e6) + charlieOrder2Fee = takerFee.mul(charlieOrderSize.abs()).mul(charlieOrderPrice).div(_1e18).div(_1e6) + expectedCharlieMargin = charlieInitialMargin.sub(charlieOrder1Fee).sub(charlieOrder2Fee) + expect(resultCharlie.notionalPosition.toString()).to.equal("0") + expect(resultCharlie.margin.toString()).to.equal(expectedCharlieMargin.toString()) + + resultAlice = await juror.getNotionalPositionAndMargin(alice.address, false, 0) + aliceOrder1Fee = takerFee.mul(aliceOrderSize.abs()).mul(aliceOrderPrice).div(_1e18).div(_1e6) + aliceOrder2Fee = makerFee.mul(aliceOrderSize.abs()).mul(aliceOrderPrice).div(_1e18).div(_1e6) + expectedAliceMargin = aliceInitialMargin.sub(aliceOrder1Fee).sub(aliceOrder2Fee) + expect(resultAlice.notionalPosition.toString()).to.equal("0") + expect(resultAlice.margin.toString()).to.equal(expectedAliceMargin.toString()) + }) + }) + }) + + context('When position and margin are both non zero', async function () { + //create position + let aliceOrder1 = getOrderV2(market, alice.address, aliceOrderSize, aliceOrderPrice, getRandomSalt()) + let charlieOrder1 = getOrderV2(market, charlie.address, charlieOrderSize, charlieOrderPrice, getRandomSalt()) + // increase position + let aliceOrder2Size = multiplySize(0.2) + let charlieOrder2Size = multiplySize(-0.2) + let aliceOrder2 = getOrderV2(market, alice.address, aliceOrder2Size, aliceOrderPrice, getRandomSalt()) + let charlieOrder2 = getOrderV2(market, charlie.address, charlieOrder2Size, charlieOrderPrice, getRandomSalt()) + // decrease position + let aliceOrder3Size = multiplySize(-0.4) + let charlieOrder3Size = multiplySize(0.4) + let aliceOrder3 = getOrderV2(market, alice.address, aliceOrder3Size, aliceOrderPrice, getRandomSalt()) + let charlieOrder3 = getOrderV2(market, charlie.address, charlieOrder3Size, charlieOrderPrice, getRandomSalt()) + + let makerFee, takerFee + + this.beforeAll(async function () { + makerFee = await getMakerFee() + takerFee = await getTakerFee() + await addMargin(alice, aliceInitialMargin) + await addMargin(charlie, charlieInitialMargin) + // charlie places a short order and alice places a long order + await placeOrderFromLimitOrderV2(aliceOrder1, alice) + await placeOrderFromLimitOrderV2(charlieOrder1, charlie) + await waitForOrdersToMatch() + }) + + this.afterAll(async function () { + // charlie places a long order and alice places a short order + charlieTotalSize = charlieOrder1.baseAssetQuantity.add(charlieOrder2Size).add(charlieOrder3Size) + aliceTotalSize = aliceOrder1.baseAssetQuantity.add(aliceOrder2Size).add(aliceOrder3Size) + aliceCleanupOrder = getOrderV2(market, alice.address, charlieTotalSize, charlieOrderPrice, getRandomSalt()) + charlieCleanupOrder = getOrderV2(market, charlie.address, aliceTotalSize, aliceOrderPrice, getRandomSalt()) + aliceCleanupOrderMargin = await utils.getRequiredMarginForShortOrder(aliceCleanupOrder) + charlieCleanupOrderMargin = await utils.getRequiredMarginForShortOrder(charlieCleanupOrder) + await addMargin(alice, aliceCleanupOrderMargin) + await addMargin(charlie, charlieCleanupOrderMargin) + await placeOrderFromLimitOrderV2(aliceCleanupOrder, alice) + await placeOrderFromLimitOrderV2(charlieCleanupOrder, charlie) + await waitForOrdersToMatch() + await removeAllAvailableMargin(alice) + await removeAllAvailableMargin(charlie) + }) + + context('when user creates a position', async function () { + it('should return correct notional position and margin', async function () { + let resultCharlie = await juror.getNotionalPositionAndMargin(charlie.address, false, 0) + let charlieOrderFee = takerFee.mul(charlieOrderSize.abs()).mul(charlieOrderPrice).div(_1e18).div(_1e6) + // since there is no liquidity in the market, the optimal pnl will fall back to using underlying price + const amm = await utils.getAMMContract(market) + const underlyingPrice = await amm.getUnderlyingPrice() + let expectedCharlieNotionalPosition = charlieOrderSize.abs().mul(underlyingPrice).div(_1e18) + let uPnl = charlieOrderSize.abs().mul(charlieOrderPrice).div(_1e18).sub(expectedCharlieNotionalPosition) // short pos + let expectedCharlieMargin = charlieInitialMargin.sub(charlieOrderFee).add(uPnl) + expect(resultCharlie.notionalPosition.toString()).to.equal(expectedCharlieNotionalPosition.toString()) + expect(resultCharlie.margin.toString()).to.equal(expectedCharlieMargin.toString()) + + let resultAlice = await juror.getNotionalPositionAndMargin(alice.address, false, 0) + let aliceOrderFee = takerFee.mul(aliceOrderSize).mul(aliceOrderPrice).div(_1e18).div(_1e6) + let expectedAliceNotionalPosition = aliceOrderSize.abs().mul(underlyingPrice).div(_1e18) + let expectedAliceMargin = aliceInitialMargin.sub(aliceOrderFee).sub(uPnl) // - charlie's uPnL + expect(resultAlice.notionalPosition.toString()).to.equal(expectedAliceNotionalPosition.toString()) + expect(resultAlice.margin.toString()).to.equal(expectedAliceMargin.toString()) + }) + }) + + context('when user increases the position', async function () { + it('should return increased notional position and correct margin', async function () { + // increase position , charlie is taker for 2nd order + await placeOrderFromLimitOrderV2(aliceOrder2, alice) + await placeOrderFromLimitOrderV2(charlieOrder2, charlie) + await waitForOrdersToMatch() + // tests + let resultCharlie = await juror.getNotionalPositionAndMargin(charlie.address, false, 0) + let charlieOrder1Fee = makerFee.mul(charlieOrderSize.abs()).mul(charlieOrderPrice).div(_1e18).div(_1e6) + let charlieOrder2Fee = takerFee.mul(charlieOrder2Size.abs()).mul(charlieOrderPrice).div(_1e18).div(_1e6) + + const amm = await utils.getAMMContract(market) + const underlyingPrice = await amm.getUnderlyingPrice() + let { openNotional, size } = await amm.positions(charlie.address) + let expectedCharlieNotionalPosition = size.abs().mul(underlyingPrice).div(_1e18) + let uPnl = expectedCharlieNotionalPosition.sub(openNotional).mul(size.isNegative() ? -1 : 1) + + let expectedCharlieMargin = charlieInitialMargin.sub(charlieOrder1Fee).sub(charlieOrder2Fee).add(uPnl) + expect(resultCharlie.notionalPosition.toString()).to.equal(expectedCharlieNotionalPosition.toString()) + expect(resultCharlie.margin.toString()).to.equal(expectedCharlieMargin.toString()) + + let resultAlice = await juror.getNotionalPositionAndMargin(alice.address, false, 0) + let aliceOrder1Fee = takerFee.mul(aliceOrderSize).mul(aliceOrderPrice).div(_1e18).div(_1e6) + let aliceOrder2Fee = makerFee.mul(aliceOrder2Size).mul(aliceOrderPrice).div(_1e18).div(_1e6) + ;({ openNotional, size } = await amm.positions(charlie.address)) + let expectedAliceNotionalPosition = size.abs().mul(underlyingPrice).div(_1e18) + let expectedAliceMargin = aliceInitialMargin.sub(aliceOrder1Fee).sub(aliceOrder2Fee).sub(uPnl) + expect(resultAlice.notionalPosition.toString()).to.equal(expectedAliceNotionalPosition.toString()) + expect(resultAlice.margin.toString()).to.equal(expectedAliceMargin.toString()) + }) + }) + + context('when user decreases the position', async function () { + it('should returns decreased notional position and margin', async function () { + // increase position and charlie is maker for 3rd order + await placeOrderFromLimitOrderV2(charlieOrder3, charlie) + await placeOrderFromLimitOrderV2(aliceOrder3, alice) + await waitForOrdersToMatch() + let resultCharlie = await juror.getNotionalPositionAndMargin(charlie.address, false, 0) + let charlieOrder1Fee = makerFee.mul(charlieOrderSize.abs()).mul(charlieOrderPrice).div(_1e18).div(_1e6) + let charlieOrder2Fee = takerFee.mul(charlieOrder2Size.abs()).mul(charlieOrderPrice).div(_1e18).div(_1e6) + let charlieOrder3Fee = makerFee.mul(charlieOrder3Size.abs()).mul(charlieOrderPrice).div(_1e18).div(_1e6) + + const amm = await utils.getAMMContract(market) + const underlyingPrice = await amm.getUnderlyingPrice() + let { openNotional, size } = await amm.positions(charlie.address) + let expectedCharlieNotionalPosition = size.abs().mul(underlyingPrice).div(_1e18) + let uPnl = expectedCharlieNotionalPosition.sub(openNotional).mul(size.isNegative() ? -1 : 1) + let expectedCharlieMargin = charlieInitialMargin.sub(charlieOrder1Fee).sub(charlieOrder2Fee).sub(charlieOrder3Fee).add(uPnl) + + expect(resultCharlie.notionalPosition.toString()).to.equal(expectedCharlieNotionalPosition.toString()) + expect(resultCharlie.margin.toString()).to.equal(expectedCharlieMargin.toString()) + + let resultAlice = await juror.getNotionalPositionAndMargin(alice.address, false, 0) + let aliceOrder1Fee = takerFee.mul(aliceOrderSize.abs()).mul(aliceOrderPrice).div(_1e18).div(_1e6) + let aliceOrder2Fee = makerFee.mul(aliceOrder2Size.abs()).mul(aliceOrderPrice).div(_1e18).div(_1e6) + let aliceOrder3Fee = takerFee.mul(aliceOrder3Size.abs()).mul(aliceOrderPrice).div(_1e18).div(_1e6) + ;({ openNotional, size } = await amm.positions(charlie.address)) + let expectedAliceNotionalPosition = size.abs().mul(underlyingPrice).div(_1e18) + let expectedAliceMargin = aliceInitialMargin.sub(aliceOrder1Fee).sub(aliceOrder2Fee).sub(aliceOrder3Fee).sub(uPnl) + expect(resultAlice.notionalPosition.toString()).to.equal(expectedAliceNotionalPosition.toString()) + expect(resultAlice.margin.toString()).to.equal(expectedAliceMargin.toString()) + }) + }) + }) +}) diff --git a/tests/orderbook/juror/tick.js b/tests/orderbook/juror/tick.js new file mode 100644 index 0000000000..296c49618a --- /dev/null +++ b/tests/orderbook/juror/tick.js @@ -0,0 +1,217 @@ +const { expect } = require("chai"); +const { BigNumber } = require("ethers"); +const utils = require("../utils") + +const { + addMargin, + alice, + bnToFloat, + cancelV2Orders, + getAMMContract, + getOrderV2, + getRandomSalt, + limitOrderBook, + multiplyPrice, + multiplySize, + placeOrderFromLimitOrderV2, + placeV2Orders, + removeAllAvailableMargin, +} = utils + +describe("Testing Tick methods", async function() { + market = BigNumber.from(0) + initialMargin = multiplyPrice(500000) + let amm + + this.beforeAll(async function() { + amm = await getAMMContract(0) + }) + this.afterEach(async function() { + // get all OrderAccepted events + let filter = limitOrderBook.filters.OrderAccepted(alice.address) + let orderAcceptedEvents = await limitOrderBook.queryFilter(filter) + // console.log(orderAcceptedEvents) + + // get all OrderCancelAccepted events + filter = limitOrderBook.filters.OrderCancelAccepted(alice.address) + let orderCancelAccepted = await limitOrderBook.queryFilter(filter) + // console.log(orderCancelAccepted) + const openOrders = orderAcceptedEvents.filter(e => { + return orderCancelAccepted.filter(e2 => e2.args.orderHash == e.args.orderHash).length == 0 + }).map(e => e.args.order) + + console.log('openOrders', openOrders.length) + if (openOrders.length) { + const { txReceipt } = await cancelV2Orders(openOrders, alice) + // const orderRejected = txReceipt.events.filter(l => l.event == 'OrderCancelRejected') + // console.log(orderRejected.map(l => l.args)) + } + await removeAllAvailableMargin(alice) + }) + + // these 2 tests when run together have a problem that they dont account for live matching + it("bids", async function() { + expect((await amm.bidsHead()).toNumber()).to.equal(0) + let orderData = generateRandomArray(15) + orderData = orderData.map(a => { + return { price: multiplyPrice(a.price), size: multiplySize(a.size) } + }) + + + const orders = [] + let requiredMargin = BigNumber.from(0) + for (let i = 0; i < orderData.length; i++) { + let longOrder = getOrderV2(market, alice.address, orderData[i].size, orderData[i].price, getRandomSalt()) + requiredMargin = requiredMargin.add(await utils.getRequiredMarginForLongOrder(longOrder)) + orders.push(longOrder) + } + await addMargin(alice, requiredMargin) + + const { txReceipt } = await placeV2Orders(orders, alice) + txReceipt.events.forEach(e => prettyPrintEvents(e)) + + // sort orderData based on descending price + orderData = orderData + .reduce((accumulator, order) => { + // Find an existing order in the accumulator with the same price + const existingOrder = accumulator.find(item => item.price.eq(order.price)); + + if (existingOrder) { + // If the order exists, add the size + existingOrder.size = existingOrder.size.add(order.size); + } else { + // If the order doesn't exist, push it to the accumulator + accumulator.push(order); + } + + return accumulator; + }, []) + .sort((a, b) => (a.price.lt(b.price) ? 1 : -1)) + expect((await amm.bidsHead()).toString()).to.equal(orderData[0].price.toString()) + + for (let i = 0; i < orderData.length; i++) { + const { nextTick, amount } = await amm.bids(orderData[i].price) + expect(amount.toString()).to.equal(orderData[i].size.toString()) + expect(nextTick.toString()).to.equal(i == orderData.length-1 ? '0' : orderData[i+1].price.toString()) + } + }) + + it("asks", async function() { + expect((await amm.asksHead()).toNumber()).to.equal(0) + // let orderData = generateRandomArray(15) + let orderData = [ + { price: 2056, size: 0.5 }, + { price: 2075, size: 0.5 }, + { price: 2022, size: 0.2 }, + { price: 2040, size: 0.5 }, + { price: 2045, size: 0.4 }, + { price: 1955, size: 0.7 }, + { price: 2069, size: 0.7 }, + { price: 2050, size: 0.4 }, + { price: 1978, size: 0.3 }, + { price: 2044, size: 0.5 }, + { price: 2028, size: 0.4 }, + { price: 1993, size: 0.5 }, + { price: 2063, size: 1 }, + { price: 1943, size: 0.4 }, + { price: 2018, size: 0.5 } + ] + console.log(orderData) + orderData = orderData.map(a => { + return { price: multiplyPrice(a.price), size: multiplySize(a.size * -1) } + }) + + const orders = [] + let requiredMargin = BigNumber.from(0) + for (let i = 0; i < orderData.length; i++) { + let shortOrder = getOrderV2(market, alice.address, orderData[i].size, orderData[i].price, getRandomSalt()) + requiredMargin = requiredMargin.add(await utils.getRequiredMarginForShortOrder(shortOrder)) + orders.push(shortOrder) + } + await addMargin(alice, requiredMargin) + const { txReceipt } = await placeV2Orders(orders, alice) + txReceipt.events.forEach(e => prettyPrintEvents(e)) + + orderData = orderData + .reduce((accumulator, order) => { + // Find an existing order in the accumulator with the same price + const existingOrder = accumulator.find(item => item.price.eq(order.price)); + + if (existingOrder) { + // If the order exists, add the size + existingOrder.size = existingOrder.size.add(order.size); + } else { + // If the order doesn't exist, push it to the accumulator + accumulator.push(order); + } + + return accumulator; + }, []) + .sort((a, b) => (a.price.lt(b.price) ? -1 : 1)) + console.log(orderData.map(a => { return { price: bnToFloat(a.price), size: bnToFloat(a.size, 18) }})) + + console.log('asksHead', (await amm.asksHead()).toString()) + expect((await amm.asksHead()).toString()).to.equal(orderData[0].price.toString()) + + for (let i = 0; i < orderData.length; i++) { + const { nextTick, amount } = await amm.asks(orderData[i].price) + console.log({ + tick: bnToFloat(orderData[i].price), + storage: { + nextTick: bnToFloat(nextTick), + amount: bnToFloat(amount, 18), + }, + actual: { + nextTick: i == orderData.length-1 ? 0 : bnToFloat(orderData[i+1].price), + amount: bnToFloat(orderData[i].size.mul(-1), 18), + } + }) + expect(amount.toString()).to.equal(orderData[i].size.mul(-1).toString()) + expect(nextTick.toString()).to.equal(i == orderData.length-1 ? '0' : orderData[i+1].price.toString()) + } + }) +}) + +function prettyPrintEvents(event) { + // console.log(event.event) + if (event.event != 'OrderAccepted' && event.event != 'OrderRejected') return + const res = { + event: event.event, + args: { + order: { + price: bnToFloat(event.args.order.price), + size: bnToFloat(event.args.order.baseAssetQuantity, 18), + } + } + } + if (event.event == 'OrderRejected') { + res.args.err = event.args.err + } + console.log(res) +} + +function getRandomInt(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +function getRandomFloat(min, max, decimalPlaces) { + let rand = Math.random() * (max - min) + min; + let power = Math.pow(10, decimalPlaces); + return Math.round(rand * power) / power; +} + +function generateRandomArray(n) { + let arr = []; + for (let i = 0; i < n; i++) { + let price = getRandomInt(1900, 2100); + let size = getRandomFloat(0.1, 1, 1); // 1 decimal place + + // arr.push({ + // price: multiplyPrice(price), + // size: multiplySize(size) + // }); + + arr.push({ price, size }); + } + return arr; +} diff --git a/tests/orderbook/juror/validateLiquidationOrderAndDetermineFillPrice.js b/tests/orderbook/juror/validateLiquidationOrderAndDetermineFillPrice.js new file mode 100644 index 0000000000..864117118d --- /dev/null +++ b/tests/orderbook/juror/validateLiquidationOrderAndDetermineFillPrice.js @@ -0,0 +1,514 @@ +const { ethers, BigNumber } = require("ethers"); +const { expect, assert } = require("chai"); +const utils = require("../utils") + +const { + _1e6, + addMargin, + alice, + cancelOrderFromLimitOrderV2, + encodeLimitOrderV2, + encodeLimitOrderV2WithType, + getAMMContract, + getOrderV2, + getRandomSalt, + getRequiredMarginForLongOrder, + getRequiredMarginForShortOrder, + juror, + limitOrderBook, + multiplySize, + multiplyPrice, + placeOrderFromLimitOrderV2, + removeAllAvailableMargin, + waitForOrdersToMatch, +} = utils + +// Testing juror precompile contract +describe("Testing validateLiquidationOrderAndDetermineFillPrice",async function () { + let market = 0 + + context("when liquidation amount is <= zero", async function () { + it("returns error", async function () { + let order = new Uint8Array(1024); + let liquidationAmount = BigNumber.from(0) + output = await juror.validateLiquidationOrderAndDetermineFillPrice(order, liquidationAmount) + expect(output.err).to.equal("invalid fillAmount") + expect(output.element).to.equal(2) + expect(output.res.fillPrice.toNumber()).to.equal(0) + expect(output.res.fillAmount.toNumber()).to.equal(0) + }) + }) + context("when liquidation amount is > zero", async function () { + context("when order is invalid", async function () { + context("when order's status is not placed", async function () { + context("when order was never placed", async function () { + let liquidationAmount = multiplySize(0.1) + let orderPrice = multiplyPrice(2000) + let longOrderBaseAssetQuantity = multiplySize(0.1) // 0.1 ether + let shortOrderBaseAssetQuantity = multiplySize(-0.1) // short 0.1 ether + let longOrder = getOrderV2(BigNumber.from(market), alice.address, longOrderBaseAssetQuantity, orderPrice, getRandomSalt()) + let shortOrder = getOrderV2(BigNumber.from(market), alice.address, shortOrderBaseAssetQuantity, orderPrice, getRandomSalt()) + it("returns error for a longOrder", async function () { + output = await juror.validateLiquidationOrderAndDetermineFillPrice(encodeLimitOrderV2WithType(longOrder), liquidationAmount) + expect(output.err).to.equal("invalid order") + expect(output.element).to.equal(0) + expect(output.res.fillPrice.toNumber()).to.equal(0) + expect(output.res.fillAmount.toNumber()).to.equal(0) + }) + it("returns error for a shortOrder", async function () { + output = await juror.validateLiquidationOrderAndDetermineFillPrice(encodeLimitOrderV2WithType(shortOrder), liquidationAmount) + expect(output.err).to.equal("invalid order") + expect(output.element).to.equal(0) + expect(output.res.fillPrice.toNumber()).to.equal(0) + expect(output.res.fillAmount.toNumber()).to.equal(0) + }) + }) + context("when order was cancelled", async function () { + let liquidationAmount = multiplySize(0.1) + let orderPrice = multiplyPrice(2000) + let longOrderBaseAssetQuantity = multiplySize(0.1) // 0.1 ether + let longOrder = getOrderV2(BigNumber.from(market), alice.address, longOrderBaseAssetQuantity, orderPrice, getRandomSalt()) + let shortOrderBaseAssetQuantity = multiplySize(-0.1) // short 0.1 ether + let shortOrder = getOrderV2(BigNumber.from(market), alice.address, shortOrderBaseAssetQuantity, orderPrice, getRandomSalt()) + + this.beforeAll(async function () { + requiredMarginForLongOrder = await getRequiredMarginForLongOrder(longOrder) + requiredMarginForShortOrder = await getRequiredMarginForShortOrder(shortOrder) + await addMargin(alice, requiredMarginForShortOrder.add(requiredMarginForLongOrder)) + }) + this.afterAll(async function () { + await removeAllAvailableMargin(alice) + }) + it("returns error for a longOrder", async function () { + await placeOrderFromLimitOrderV2(longOrder, alice) + await cancelOrderFromLimitOrderV2(longOrder, alice) + + output = await juror.validateLiquidationOrderAndDetermineFillPrice(encodeLimitOrderV2WithType(longOrder), liquidationAmount) + expect(output.err).to.equal("invalid order") + expect(output.element).to.equal(0) + expect(output.res.fillPrice.toNumber()).to.equal(0) + expect(output.res.fillAmount.toNumber()).to.equal(0) + }) + it("returns error for a shortOrder", async function () { + await placeOrderFromLimitOrderV2(shortOrder, alice) + await cancelOrderFromLimitOrderV2(shortOrder, alice) + + output = await juror.validateLiquidationOrderAndDetermineFillPrice(encodeLimitOrderV2WithType(shortOrder), liquidationAmount) + expect(output.err).to.equal("invalid order") + expect(output.element).to.equal(0) + expect(output.res.fillPrice.toNumber()).to.equal(0) + expect(output.res.fillAmount.toNumber()).to.equal(0) + }) + }) + context("when order was filled", async function () { + let liquidationAmount = multiplySize(0.1) + let orderPrice = multiplyPrice(2000) + let longOrderBaseAssetQuantity = multiplySize(0.1) // 0.1 ether + let longOrder = getOrderV2(BigNumber.from(market), alice.address, longOrderBaseAssetQuantity, orderPrice, getRandomSalt()) + let shortOrderBaseAssetQuantity = multiplySize(-0.1) // short 0.1 ether + let shortOrder = getOrderV2(BigNumber.from(market), charlie.address, shortOrderBaseAssetQuantity, orderPrice, getRandomSalt()) + + this.beforeAll(async function () { + requiredMarginForLongOrder = await getRequiredMarginForLongOrder(longOrder) + requiredMarginForShortOrder = await getRequiredMarginForShortOrder(shortOrder) + await addMargin(alice, requiredMarginForLongOrder) + await addMargin(charlie, requiredMarginForShortOrder) + await placeOrderFromLimitOrderV2(longOrder, alice) + await placeOrderFromLimitOrderV2(shortOrder, charlie) + await waitForOrdersToMatch() + }) + this.afterAll(async function () { + // alice should short and charlie should long to clean + let aliceOppositeOrder = getOrderV2(BigNumber.from(market), alice.address, shortOrderBaseAssetQuantity, orderPrice, getRandomSalt()) + let charlieOppositeOrder = getOrderV2(BigNumber.from(market), charlie.address, longOrderBaseAssetQuantity, orderPrice, getRandomSalt()) + requiredMarginForAliceOppositeOrder = await getRequiredMarginForShortOrder(aliceOppositeOrder) + requiredMarginForCharlieOppositeOrder = await getRequiredMarginForLongOrder(charlieOppositeOrder) + await addMargin(alice, requiredMarginForAliceOppositeOrder) + await addMargin(charlie, requiredMarginForCharlieOppositeOrder) + await placeOrderFromLimitOrderV2(aliceOppositeOrder, alice) + await placeOrderFromLimitOrderV2(charlieOppositeOrder, charlie) + await waitForOrdersToMatch() + await removeAllAvailableMargin(alice) + await removeAllAvailableMargin(charlie) + }) + it("returns error for a longOrder", async function () { + output = await juror.validateLiquidationOrderAndDetermineFillPrice(encodeLimitOrderV2WithType(longOrder), liquidationAmount) + expect(output.err).to.equal("invalid order") + expect(output.element).to.equal(0) + expect(output.res.fillPrice.toNumber()).to.equal(0) + expect(output.res.fillAmount.toNumber()).to.equal(0) + }) + it('returns error for a shortOrder', async function () { + output = await juror.validateLiquidationOrderAndDetermineFillPrice(encodeLimitOrderV2WithType(shortOrder), liquidationAmount) + expect(output.err).to.equal("invalid order") + expect(output.element).to.equal(0) + expect(output.res.fillPrice.toNumber()).to.equal(0) + expect(output.res.fillAmount.toNumber()).to.equal(0) + }) + }) + }) + context("when order's status is placed", async function () { + context("when order's filled amount + liquidationAmount is > order's baseAssetQuantity", async function () { + let liquidationAmount = multiplySize(0.2) + let orderPrice = multiplyPrice(2000) + let longOrderBaseAssetQuantity = multiplySize(0.1) // 0.1 ether + let shortOrderBaseAssetQuantity = multiplySize(-0.1) // short 0.1 ether + let longOrder = getOrderV2(BigNumber.from(market), alice.address, longOrderBaseAssetQuantity, orderPrice, getRandomSalt()) + let shortOrder = getOrderV2(BigNumber.from(market), alice.address, shortOrderBaseAssetQuantity, orderPrice, getRandomSalt()) + + context("for a longOrder", async function () { + this.beforeAll(async function () { + requiredMarginForLongOrder = await getRequiredMarginForLongOrder(longOrder) + await addMargin(alice, requiredMarginForLongOrder) + await placeOrderFromLimitOrderV2(longOrder, alice) + }) + this.afterAll(async function () { + await cancelOrderFromLimitOrderV2(longOrder, alice) + await removeAllAvailableMargin(alice) + }) + it("returns error", async function () { + output = await juror.validateLiquidationOrderAndDetermineFillPrice(encodeLimitOrderV2WithType(longOrder), liquidationAmount) + expect(output.err).to.equal("overfill") + expect(output.element).to.equal(0) + expect(output.res.fillPrice.toNumber()).to.equal(0) + expect(output.res.fillAmount.toNumber()).to.equal(0) + }) + }) + context("for a shortOrder", async function () { + this.beforeAll(async function () { + requiredMarginForShortOrder = await getRequiredMarginForShortOrder(shortOrder) + await addMargin(alice, requiredMarginForShortOrder) + await placeOrderFromLimitOrderV2(shortOrder, alice) + }) + this.afterAll(async function () { + await cancelOrderFromLimitOrderV2(shortOrder, alice) + await removeAllAvailableMargin(alice) + }) + it("returns error", async function () { + output = await juror.validateLiquidationOrderAndDetermineFillPrice(encodeLimitOrderV2WithType(shortOrder), liquidationAmount) + expect(output.err).to.equal("overfill") + expect(output.element).to.equal(0) + expect(output.res.fillPrice.toNumber()).to.equal(0) + expect(output.res.fillAmount.toNumber()).to.equal(0) + }) + }) + }) + context("when order's filled amount + liquidationAmount is <= order's baseAssetQuantity", async function () { + it.skip("returns error if order is reduceOnly and liquidationAmount > currentPosition", async function () { + }) + }) + }) + }) + context("when order is valid", async function () { + context("when liquidationAmount is invalid", async function () { + context("When liquidation amount is not multiple of minSizeRequirement", async function () { + context("if liquidationAmount is greater than zero less than minSizeRequirement", async function () { + let shortOrderPrice = multiplyPrice(2001) + let longOrderPrice = multiplyPrice(1999) + let longOrderBaseAssetQuantity = multiplySize(0.1) // 0.1 ether + let shortOrderBaseAssetQuantity = multiplySize(-0.1) // short 0.1 ether + let longOrder = getOrderV2(BigNumber.from(market), alice.address, longOrderBaseAssetQuantity, longOrderPrice, getRandomSalt(), false) + let shortOrder = getOrderV2(BigNumber.from(market), alice.address, shortOrderBaseAssetQuantity, shortOrderPrice, getRandomSalt(), false) + let liquidationAmount + + this.beforeAll(async function () { + const amm = await getAMMContract(market) + minSizeRequirement = await amm.minSizeRequirement() + liquidationAmount = minSizeRequirement.div(BigNumber.from(2)) + requiredMarginForLongOrder = await getRequiredMarginForLongOrder(longOrder) + requiredMarginForShortOrder = await getRequiredMarginForShortOrder(shortOrder) + await addMargin(alice, requiredMarginForShortOrder.add(requiredMarginForLongOrder)) + await placeOrderFromLimitOrderV2(longOrder, alice) + await placeOrderFromLimitOrderV2(shortOrder, alice) + }) + this.afterAll(async function () { + await cancelOrderFromLimitOrderV2(longOrder, alice) + await cancelOrderFromLimitOrderV2(shortOrder, alice) + await removeAllAvailableMargin(alice) + }) + + it("returns error for a long order", async function () { + output = await juror.validateLiquidationOrderAndDetermineFillPrice(encodeLimitOrderV2WithType(longOrder), liquidationAmount) + expect(output.err).to.equal("not multiple") + expect(output.element).to.equal(2) + expect(output.res.fillPrice.toNumber()).to.equal(0) + expect(output.res.fillAmount.toNumber()).to.equal(0) + }) + it("returns error for a short order", async function () { + output = await juror.validateLiquidationOrderAndDetermineFillPrice(encodeLimitOrderV2WithType(shortOrder), liquidationAmount) + expect(output.err).to.equal("not multiple") + expect(output.element).to.equal(2) + expect(output.res.fillPrice.toNumber()).to.equal(0) + expect(output.res.fillAmount.toNumber()).to.equal(0) + }) + }) + context("if liquidationAmount is greater than minSizeRequirement but not a multiple", async function () { + let shortOrderPrice = multiplyPrice(2001) + let longOrderPrice = multiplyPrice(1999) + let longOrderBaseAssetQuantity = multiplySize(0.1) // 0.1 ether + let shortOrderBaseAssetQuantity = multiplySize(-0.1) // short 0.1 ether + let longOrder = getOrderV2(BigNumber.from(market), alice.address, longOrderBaseAssetQuantity, longOrderPrice, getRandomSalt(), false) + let shortOrder = getOrderV2(BigNumber.from(market), alice.address, shortOrderBaseAssetQuantity, shortOrderPrice, getRandomSalt(), false) + let liquidationAmount + + this.beforeAll(async function () { + const amm = await getAMMContract(market) + minSizeRequirement = await amm.minSizeRequirement() + liquidationAmount = minSizeRequirement.mul(3).div(2) + requiredMarginForLongOrder = await getRequiredMarginForLongOrder(longOrder) + requiredMarginForShortOrder = await getRequiredMarginForShortOrder(shortOrder) + await addMargin(alice, requiredMarginForShortOrder.add(requiredMarginForLongOrder)) + await placeOrderFromLimitOrderV2(longOrder, alice) + await placeOrderFromLimitOrderV2(shortOrder, alice) + }) + this.afterAll(async function () { + await cancelOrderFromLimitOrderV2(longOrder, alice) + await cancelOrderFromLimitOrderV2(shortOrder, alice) + await removeAllAvailableMargin(alice) + }) + + it("returns error for a long order", async function () { + output = await juror.validateLiquidationOrderAndDetermineFillPrice(encodeLimitOrderV2WithType(longOrder), liquidationAmount) + expect(output.err).to.equal("not multiple") + expect(output.element).to.equal(2) + expect(output.res.fillPrice.toNumber()).to.equal(0) + expect(output.res.fillAmount.toNumber()).to.equal(0) + }) + it("returns error for a short order", async function () { + output = await juror.validateLiquidationOrderAndDetermineFillPrice(encodeLimitOrderV2WithType(shortOrder), liquidationAmount) + expect(output.err).to.equal("not multiple") + expect(output.element).to.equal(2) + expect(output.res.fillPrice.toNumber()).to.equal(0) + expect(output.res.fillAmount.toNumber()).to.equal(0) + }) + }) + }) + }) + context("When liquidationAmount is valid", async function () { + let liquidationAmount = multiplySize(0.2) // 0.2 ether + let lowerBound, upperBound, liqLowerBound, liqUpperBound + + this.beforeAll(async function () { + const amm = await getAMMContract(market) + let oraclePrice = (await amm.getUnderlyingPrice()) + let maxLiquidationPriceSpread = await amm.maxLiquidationPriceSpread() + let oraclePriceSpreadThreshold = (await amm.maxOracleSpreadRatio()) + liqLowerBound = oraclePrice.mul(_1e6.sub(maxLiquidationPriceSpread)).div(_1e6) + liqUpperBound = oraclePrice.mul(_1e6.add(maxLiquidationPriceSpread)).div(_1e6) + upperBound = oraclePrice.mul(_1e6.add(oraclePriceSpreadThreshold)).div(_1e6) + lowerBound = oraclePrice.mul(_1e6.sub(oraclePriceSpreadThreshold)).div(_1e6) + }) + context("For a long order", async function () { + let longOrderBaseAssetQuantity = multiplySize(0.3) // long 0.3 ether + context("when price is less than liquidation lower bound price", async function () { + let longOrder + this.beforeEach(async function () { + longOrderPrice = liqLowerBound.sub(1) + longOrder = getOrderV2(BigNumber.from(market), alice.address, longOrderBaseAssetQuantity, longOrderPrice, getRandomSalt()) + requiredMargin = await getRequiredMarginForLongOrder(longOrder) + await addMargin(alice, requiredMargin) + await placeOrderFromLimitOrderV2(longOrder, alice) + }) + this.afterEach(async function () { + await cancelOrderFromLimitOrderV2(longOrder, alice) + await removeAllAvailableMargin(alice) + }) + + it("returns error", async function () { + output = await juror.validateLiquidationOrderAndDetermineFillPrice(encodeLimitOrderV2WithType(longOrder), liquidationAmount) + expect(output.err).to.equal("long price below lower bound") + expect(output.element).to.equal(0) + expect(output.res.fillPrice.toNumber()).to.equal(0) + expect(output.res.fillAmount.toNumber()).to.equal(0) + }) + }) + context("when price is more than upperBound", async function () { + let longOrder + this.beforeEach(async function () { + longOrderPrice = upperBound.add(BigNumber.from(1)) + longOrder = getOrderV2(BigNumber.from(market), alice.address, longOrderBaseAssetQuantity, longOrderPrice, getRandomSalt()) + requiredMargin = await getRequiredMarginForLongOrder(longOrder) + await addMargin(alice, requiredMargin) + await placeOrderFromLimitOrderV2(longOrder, alice) + }) + this.afterEach(async function () { + await cancelOrderFromLimitOrderV2(longOrder, alice) + await removeAllAvailableMargin(alice) + }) + it("returns upperBound as fillPrice", async function () { + output = await juror.validateLiquidationOrderAndDetermineFillPrice(encodeLimitOrderV2WithType(longOrder), liquidationAmount) + expect(output.err).to.equal("") + expect(output.element).to.equal(3) + expect(output.res.fillPrice.toString()).to.equal(upperBound.toString()) + expect(output.res.fillAmount.toString()).to.equal(liquidationAmount.toString()) + expect(output.res.instruction.mode).to.eq(1) + expectedOrderHash = await limitOrderBook.getOrderHash(longOrder) + expect(output.res.instruction.orderHash).to.eq(expectedOrderHash) + expect(output.res.encodedOrder).to.eq(encodeLimitOrderV2(longOrder)) + }) + }) + context("if price is between liqLowerBound and upperBound", async function () { + let longOrder1, longOrder2, longOrder3 + this.beforeEach(async function () { + longOrderPrice1 = upperBound.sub(BigNumber.from(1)) + longOrder1 = getOrderV2(BigNumber.from(market), alice.address, longOrderBaseAssetQuantity, longOrderPrice1, getRandomSalt()) + requiredMargin1 = await getRequiredMarginForLongOrder(longOrder1) + longOrderPrice2 = liqLowerBound + longOrder2 = getOrderV2(BigNumber.from(market), alice.address, longOrderBaseAssetQuantity, longOrderPrice2, getRandomSalt()) + requiredMargin2 = await getRequiredMarginForLongOrder(longOrder2) + longOrderPrice3 = upperBound.add(liqLowerBound).div(2) + longOrder3 = getOrderV2(BigNumber.from(market), alice.address, longOrderBaseAssetQuantity, longOrderPrice3, getRandomSalt(), false) + requiredMargin3 = await getRequiredMarginForLongOrder(longOrder3) + + await addMargin(alice, requiredMargin1.add(requiredMargin2).add(requiredMargin3)) + await placeOrderFromLimitOrderV2(longOrder1, alice) + await placeOrderFromLimitOrderV2(longOrder2, alice) + await placeOrderFromLimitOrderV2(longOrder3, alice) + }) + this.afterEach(async function () { + await cancelOrderFromLimitOrderV2(longOrder1, alice) + await cancelOrderFromLimitOrderV2(longOrder2, alice) + await cancelOrderFromLimitOrderV2(longOrder3, alice) + await removeAllAvailableMargin(alice) + }) + it("returns longOrder's price as fillPrice", async function () { + responseLongOrder1 = await juror.validateLiquidationOrderAndDetermineFillPrice(encodeLimitOrderV2WithType(longOrder1), liquidationAmount) + expect(responseLongOrder1.err).to.equal("") + expect(responseLongOrder1.element).to.equal(3) + expect(responseLongOrder1.res.fillPrice.toString()).to.equal(longOrder1.price.toString()) + expect(responseLongOrder1.res.fillAmount.toString()).to.equal(liquidationAmount.toString()) + expect(responseLongOrder1.res.instruction.mode).to.eq(1) + expectedOrderHash = await limitOrderBook.getOrderHash(longOrder1) + expect(responseLongOrder1.res.instruction.orderHash).to.eq(expectedOrderHash) + expect(responseLongOrder1.res.encodedOrder).to.eq(encodeLimitOrderV2(longOrder1)) + + responseLongOrder2 = await juror.validateLiquidationOrderAndDetermineFillPrice(encodeLimitOrderV2WithType(longOrder2), liquidationAmount) + expect(responseLongOrder2.err).to.equal("") + expect(responseLongOrder2.element).to.equal(3) + expect(responseLongOrder2.res.fillPrice.toString()).to.equal(longOrder2.price.toString()) + expect(responseLongOrder2.res.fillAmount.toString()).to.equal(liquidationAmount.toString()) + expect(responseLongOrder2.res.instruction.mode).to.eq(1) + expectedOrderHash = await limitOrderBook.getOrderHash(longOrder2) + expect(responseLongOrder2.res.instruction.orderHash).to.eq(expectedOrderHash) + expect(responseLongOrder2.res.encodedOrder).to.eq(encodeLimitOrderV2(longOrder2)) + + responseLongOrder3 = await juror.validateLiquidationOrderAndDetermineFillPrice(encodeLimitOrderV2WithType(longOrder3), liquidationAmount) + expect(responseLongOrder3.err).to.equal("") + expect(responseLongOrder3.element).to.equal(3) + expect(responseLongOrder3.res.fillPrice.toString()).to.equal(longOrder3.price.toString()) + expect(responseLongOrder3.res.fillAmount.toString()).to.equal(liquidationAmount.toString()) + expect(responseLongOrder3.res.instruction.mode).to.eq(1) + expectedOrderHash = await limitOrderBook.getOrderHash(longOrder3) + expect(responseLongOrder3.res.instruction.orderHash).to.eq(expectedOrderHash) + expect(responseLongOrder3.res.encodedOrder).to.eq(encodeLimitOrderV2(longOrder3)) + }) + }) + }) + context("For a short order", async function () { + let shortOrderBaseAssetQuantity = multiplySize(-0.4) // short 0.4 ether + context("when price is greater than liquidation upper bound price", async function () { + let shortOrder + this.beforeEach(async function () { + shortOrderPrice = liqUpperBound.add(1) + shortOrder = getOrderV2(BigNumber.from(market), alice.address, shortOrderBaseAssetQuantity, shortOrderPrice, getRandomSalt()) + requiredMargin = await getRequiredMarginForShortOrder(shortOrder) + await addMargin(alice, requiredMargin) + await placeOrderFromLimitOrderV2(shortOrder, alice) + }) + this.afterEach(async function () { + await cancelOrderFromLimitOrderV2(shortOrder, alice) + await removeAllAvailableMargin(alice) + }) + + it("returns error if price is more than liquidation upperBound", async function () { + output = await juror.validateLiquidationOrderAndDetermineFillPrice(encodeLimitOrderV2WithType(shortOrder), liquidationAmount) + expect(output.err).to.equal("short price above upper bound") + expect(output.element).to.equal(0) + expect(output.res.fillPrice.toNumber()).to.equal(0) + expect(output.res.fillAmount.toNumber()).to.equal(0) + }) + }) + context("when price is less than lowerBound", async function () { + this.beforeEach(async function () { + shortOrderPrice = lowerBound.sub(BigNumber.from(1)) + shortOrder = getOrderV2(BigNumber.from(market), alice.address, shortOrderBaseAssetQuantity, shortOrderPrice, getRandomSalt()) + requiredMargin = await getRequiredMarginForShortOrder(shortOrder) + await addMargin(alice, requiredMargin) + await placeOrderFromLimitOrderV2(shortOrder, alice) + }) + this.afterEach(async function () { + await cancelOrderFromLimitOrderV2(shortOrder, alice) + await removeAllAvailableMargin(alice) + }) + it("returns lower bound as fillPrice", async function () { + output = await juror.validateLiquidationOrderAndDetermineFillPrice(encodeLimitOrderV2WithType(shortOrder), liquidationAmount) + expect(output.err).to.equal("") + expect(output.element).to.equal(3) + expect(output.res.fillPrice.toString()).to.equal(lowerBound.toString()) + expect(output.res.fillAmount.toString()).to.equal(liquidationAmount.mul(-1).toString()) + expect(output.res.instruction.mode).to.eq(1) + expectedOrderHash = await limitOrderBook.getOrderHash(shortOrder) + expect(output.res.instruction.orderHash).to.eq(expectedOrderHash) + expect(output.res.encodedOrder).to.eq(encodeLimitOrderV2(shortOrder)) + }) + }) + context("if price is between lowerBound and liqUpperBound", async function () { + this.beforeEach(async function () { + shortOrderPrice1 = lowerBound.add(BigNumber.from(1)) + shortOrder1 = getOrderV2(BigNumber.from(market), alice.address, shortOrderBaseAssetQuantity, shortOrderPrice1, getRandomSalt()) + requiredMargin1 = await getRequiredMarginForShortOrder(shortOrder1) + shortOrderPrice2 = liqUpperBound + shortOrder2 = getOrderV2(BigNumber.from(market), alice.address, shortOrderBaseAssetQuantity, shortOrderPrice2, getRandomSalt()) + requiredMargin2 = await getRequiredMarginForShortOrder(shortOrder2) + shortOrderPrice3 = lowerBound.add(liqUpperBound).div(2) + shortOrder3 = getOrderV2(BigNumber.from(market), alice.address, shortOrderBaseAssetQuantity, shortOrderPrice3, getRandomSalt(), false) + requiredMargin3 = await getRequiredMarginForShortOrder(shortOrder3) + + await addMargin(alice, requiredMargin1.add(requiredMargin2).add(requiredMargin3)) + await placeOrderFromLimitOrderV2(shortOrder1, alice) + await placeOrderFromLimitOrderV2(shortOrder2, alice) + await placeOrderFromLimitOrderV2(shortOrder3, alice) + }) + this.afterEach(async function () { + await cancelOrderFromLimitOrderV2(shortOrder1, alice) + await cancelOrderFromLimitOrderV2(shortOrder2, alice) + await cancelOrderFromLimitOrderV2(shortOrder3, alice) + await removeAllAvailableMargin(alice) + }) + it("returns shortOrder's price as fillPrice if price is between lowerBound and upperBound", async function () { + output = await juror.validateLiquidationOrderAndDetermineFillPrice(encodeLimitOrderV2WithType(shortOrder1), liquidationAmount) + expect(output.err).to.equal("") + expect(output.element).to.equal(3) + expect(output.res.fillPrice.toString()).to.equal(shortOrder1.price.toString()) + expect(output.res.fillAmount.toString()).to.equal(liquidationAmount.mul(-1).toString()) + expect(output.res.instruction.mode).to.eq(1) + expectedOrderHash = await limitOrderBook.getOrderHash(shortOrder1) + expect(output.res.instruction.orderHash).to.eq(expectedOrderHash) + expect(output.res.encodedOrder).to.eq(encodeLimitOrderV2(shortOrder1)) + + output = await juror.validateLiquidationOrderAndDetermineFillPrice(encodeLimitOrderV2WithType(shortOrder2), liquidationAmount) + expect(output.err).to.equal("") + expect(output.element).to.equal(3) + expect(output.res.fillPrice.toString()).to.equal(shortOrder2.price.toString()) + expect(output.res.fillAmount.toString()).to.equal(liquidationAmount.mul(-1).toString()) + expect(output.res.instruction.mode).to.eq(1) + expectedOrderHash = await limitOrderBook.getOrderHash(shortOrder2) + expect(output.res.instruction.orderHash).to.eq(expectedOrderHash) + expect(output.res.encodedOrder).to.eq(encodeLimitOrderV2(shortOrder2)) + + output = await juror.validateLiquidationOrderAndDetermineFillPrice(encodeLimitOrderV2WithType(shortOrder3), liquidationAmount) + expect(output.err).to.equal("") + expect(output.element).to.equal(3) + expect(output.res.fillPrice.toString()).to.equal(shortOrder3.price.toString()) + expect(output.res.fillAmount.toString()).to.equal(liquidationAmount.mul(-1).toString()) + expect(output.res.instruction.mode).to.eq(1) + expectedOrderHash = await limitOrderBook.getOrderHash(shortOrder3) + expect(output.res.instruction.orderHash).to.eq(expectedOrderHash) + expect(output.res.encodedOrder).to.eq(encodeLimitOrderV2(shortOrder3)) + }) + }) + }) + }) + }) + }) +}) diff --git a/tests/orderbook/juror/validateOrdersAndDetermineFillPrice.js b/tests/orderbook/juror/validateOrdersAndDetermineFillPrice.js new file mode 100644 index 0000000000..04e6418c36 --- /dev/null +++ b/tests/orderbook/juror/validateOrdersAndDetermineFillPrice.js @@ -0,0 +1,686 @@ +const { ethers, BigNumber } = require("ethers") +const { expect, assert } = require("chai") +const utils = require("../utils") + +const { + _1e6, + addMargin, + alice, + bob, + cancelOrderFromLimitOrderV2, + disableValidatorMatching, + enableValidatorMatching, + encodeLimitOrderV2, + encodeLimitOrderV2WithType, + getAMMContract, + getOrderV2, + getRandomSalt, + getRequiredMarginForLongOrder, + getRequiredMarginForShortOrder, + juror, + multiplySize, + multiplyPrice, + orderBook, + placeOrderFromLimitOrderV2, + removeAllAvailableMargin, + waitForOrdersToMatch, +} = utils + +// Testing juror precompile contract +describe("Test validateOrdersAndDetermineFillPrice", function () { + let market = BigNumber.from(0) + let longOrderBaseAssetQuantity = multiplySize(0.1) // 0.1 ether + let shortOrderBaseAssetQuantity = multiplySize(-0.1) // 0.1 ether + let longOrderPrice = multiplyPrice(2000) + let shortOrderPrice = multiplyPrice(2000) + + context("when fillAmount is <= 0", async function () { + it("returns error when fillAmount=0", async function () { + output = await juror.validateOrdersAndDetermineFillPrice([1,1], 0) + expect(output.err).to.equal("invalid fillAmount") + expect(output.element).to.equal(2) + expect(output.res.fillPrice.toNumber()).to.equal(0) + }) + it("returns error when fillAmount<0", async function () { + let fillAmount = BigNumber.from("-1") + output = await juror.validateOrdersAndDetermineFillPrice([1,1], fillAmount) + expect(output.err).to.equal("invalid fillAmount") + expect(output.element).to.equal(2) + expect(output.res.fillPrice.toNumber()).to.equal(0) + }) + }) + context("when fillAmount is > 0", async function () { + context("when either longOrder or shortOrder is invalid", async function () { + context("when longOrder is invalid", async function () { + context("when longOrder's status is not placed", async function () { + context("when longOrder was never placed", async function () { + it("returns error", async function () { + let fillAmount = longOrderBaseAssetQuantity + let longOrder = getOrderV2(market, alice.address, longOrderBaseAssetQuantity, longOrderPrice, getRandomSalt()) + let output = await juror.validateOrdersAndDetermineFillPrice([encodeLimitOrderV2WithType(longOrder), encodeLimitOrderV2WithType(longOrder)], fillAmount) + expect(output.err).to.equal("invalid order") + expect(output.element).to.equal(0) + expect(output.res.fillPrice.toNumber()).to.equal(0) + }) + }) + context("if longOrder's status is cancelled", async function () { + let longOrder = getOrderV2(market, alice.address, longOrderBaseAssetQuantity, longOrderPrice, getRandomSalt()) + this.beforeAll(async function () { + requiredMargin = await getRequiredMarginForLongOrder(longOrder) + await addMargin(alice, requiredMargin) + await placeOrderFromLimitOrderV2(longOrder, alice) + await cancelOrderFromLimitOrderV2(longOrder, alice) + }) + this.afterAll(async function () { + await removeAllAvailableMargin(alice) + }) + it("returns error", async function () { + let fillAmount = longOrderBaseAssetQuantity + let output = await juror.validateOrdersAndDetermineFillPrice([encodeLimitOrderV2WithType(longOrder), encodeLimitOrderV2WithType(longOrder)], fillAmount) + expect(output.err).to.equal("invalid order") + expect(output.element).to.equal(0) + expect(output.res.fillPrice.toNumber()).to.equal(0) + }) + }) + context("if longOrder's status is filled", async function () { + let longOrder = getOrderV2(market, alice.address, longOrderBaseAssetQuantity, longOrderPrice, getRandomSalt()) + let shortOrder = getOrderV2(market, bob.address, shortOrderBaseAssetQuantity, shortOrderPrice, getRandomSalt()) + + this.beforeAll(async function () { + requiredMarginForLongOrder = await getRequiredMarginForLongOrder(longOrder) + await addMargin(alice, requiredMarginForLongOrder) + await placeOrderFromLimitOrderV2(longOrder, alice) + requiredMarginForShortOrder = await getRequiredMarginForShortOrder(shortOrder) + await addMargin(bob, requiredMarginForShortOrder) + await placeOrderFromLimitOrderV2(shortOrder, bob) + await waitForOrdersToMatch() + }) + this.afterAll(async function () { + aliceOppositeOrder = getOrderV2(market, alice.address, shortOrderBaseAssetQuantity, longOrderPrice, getRandomSalt(), true) + bobOppositeOrder = getOrderV2(market, bob.address, longOrderBaseAssetQuantity, shortOrderPrice, getRandomSalt(), true) + await placeOrderFromLimitOrderV2(aliceOppositeOrder, alice) + await placeOrderFromLimitOrderV2(bobOppositeOrder, bob) + await waitForOrdersToMatch() + await removeAllAvailableMargin(alice) + await removeAllAvailableMargin(bob) + }) + it("returns error", async function () { + let fillAmount = longOrderBaseAssetQuantity + let output = await juror.validateOrdersAndDetermineFillPrice([encodeLimitOrderV2WithType(longOrder), encodeLimitOrderV2WithType(shortOrder)], fillAmount) + expect(output.err).to.equal("invalid order") + expect(output.element).to.equal(0) + expect(output.res.fillPrice.toNumber()).to.equal(0) + }) + }) + }) + context("when longOrder's status is placed", async function () { + context("when longOrder's baseAssetQuantity is negative", async function () { + let shortOrder = getOrderV2(market, bob.address, shortOrderBaseAssetQuantity, shortOrderPrice, getRandomSalt()) + this.beforeAll(async function () { + requiredMargin = await getRequiredMarginForShortOrder(shortOrder) + await addMargin(bob, requiredMargin) + await placeOrderFromLimitOrderV2(shortOrder, bob) + }) + this.afterAll(async function () { + await cancelOrderFromLimitOrderV2(shortOrder, bob) + await removeAllAvailableMargin(bob) + }) + + it("returns error", async function () { + fillAmount = longOrderBaseAssetQuantity + let output = await juror.validateOrdersAndDetermineFillPrice([encodeLimitOrderV2WithType(shortOrder), encodeLimitOrderV2WithType(shortOrder)], fillAmount) + expect(output.err).to.equal("not long") + expect(output.element).to.equal(0) + expect(output.res.fillPrice.toNumber()).to.equal(0) + }) + }) + context("when longOrder's baseAssetQuantity is positive", async function () { + context("when longOrder's unfilled < fillAmount", async function () { + let longOrder = getOrderV2(market, alice.address, longOrderBaseAssetQuantity, longOrderPrice, getRandomSalt()) + this.beforeAll(async function () { + requiredMargin = await getRequiredMarginForLongOrder(longOrder) + await addMargin(alice, requiredMargin) + await placeOrderFromLimitOrderV2(longOrder, alice) + }) + this.afterAll(async function () { + await cancelOrderFromLimitOrderV2(longOrder, alice) + await removeAllAvailableMargin(alice) + }) + + it("returns error", async function () { + fillAmount = longOrderBaseAssetQuantity.mul(2) + let output = await juror.validateOrdersAndDetermineFillPrice([encodeLimitOrderV2WithType(longOrder), encodeLimitOrderV2WithType(longOrder)], fillAmount) + expect(output.err).to.equal("overfill") + expect(output.element).to.equal(0) + expect(output.res.fillPrice.toNumber()).to.equal(0) + }) + }) + context("when longOrder's unfilled > fillAmount", async function () { + context.skip("when order is reduceOnly", async function () { + it("returns error if fillAmount > currentPosition of longOrder trader", async function () { + }) + }) + }) + }) + }) + }) + context("when longOrder is valid", async function () { + context("when shortOrder is invalid", async function () { + context("when shortOrder's status is not placed", async function () { + let fillAmount = longOrderBaseAssetQuantity + context("if shortOrder was never placed", async function () { + let longOrder = getOrderV2(market, alice.address, longOrderBaseAssetQuantity, longOrderPrice, getRandomSalt()) + let shortOrder = getOrderV2(market, bob.address, shortOrderBaseAssetQuantity, shortOrderPrice, getRandomSalt()) + + this.beforeAll(async function () { + requiredMarginForLongOrder = await getRequiredMarginForLongOrder(longOrder) + await addMargin(alice, requiredMarginForLongOrder) + await placeOrderFromLimitOrderV2(longOrder, alice) + }) + this.afterAll(async function () { + await cancelOrderFromLimitOrderV2(longOrder, alice) + await removeAllAvailableMargin(alice) + }) + it("returns error", async function () { + output = await juror.validateOrdersAndDetermineFillPrice([encodeLimitOrderV2WithType(longOrder), encodeLimitOrderV2WithType(shortOrder)], fillAmount) + expect(output.err).to.equal("invalid order") + expect(output.element).to.equal(1) + expect(output.res.fillPrice.toNumber()).to.equal(0) + }) + }) + context("if shortOrder's status is cancelled", async function () { + let longOrder = getOrderV2(market, alice.address, longOrderBaseAssetQuantity, longOrderPrice, getRandomSalt()) + let shortOrder = getOrderV2(market, bob.address, shortOrderBaseAssetQuantity, shortOrderPrice, getRandomSalt()) + this.beforeAll(async function () { + //placing short order first to avoid matching. We can use disableValidatorMatching() also + requiredMarginForShortOrder = await getRequiredMarginForShortOrder(shortOrder) + await addMargin(bob, requiredMarginForShortOrder) + await placeOrderFromLimitOrderV2(shortOrder, bob) + await cancelOrderFromLimitOrderV2(shortOrder, bob) + requiredMarginForLongOrder = await getRequiredMarginForLongOrder(longOrder) + await addMargin(alice, requiredMarginForLongOrder) + await placeOrderFromLimitOrderV2(longOrder, alice) + }) + this.afterAll(async function () { + await cancelOrderFromLimitOrderV2(longOrder, alice) + await removeAllAvailableMargin(alice) + await removeAllAvailableMargin(bob) + }) + it("returns error", async function () { + output = await juror.validateOrdersAndDetermineFillPrice([encodeLimitOrderV2WithType(longOrder), encodeLimitOrderV2WithType(shortOrder)], fillAmount) + expect(output.err).to.equal("invalid order") + expect(output.element).to.equal(1) + expect(output.res.fillPrice.toNumber()).to.equal(0) + }) + }) + context("if shortOrder's status is filled", async function () { + let longOrder = getOrderV2(market, alice.address, longOrderBaseAssetQuantity, longOrderPrice, getRandomSalt()) + let shortOrder = getOrderV2(market, bob.address, shortOrderBaseAssetQuantity, shortOrderPrice, getRandomSalt()) + let longOrder2 = getOrderV2(market, alice.address, longOrderBaseAssetQuantity, longOrderPrice, getRandomSalt()) + this.beforeAll(async function () { + requiredMarginForLongOrder = await getRequiredMarginForLongOrder(longOrder) + await addMargin(alice, requiredMarginForLongOrder) + await placeOrderFromLimitOrderV2(longOrder, alice) + requiredMarginForShortOrder = await getRequiredMarginForShortOrder(shortOrder) + await addMargin(bob, requiredMarginForShortOrder) + await placeOrderFromLimitOrderV2(shortOrder, bob) + await waitForOrdersToMatch() + requiredMarginForLongOrder2 = await getRequiredMarginForLongOrder(longOrder2) + await addMargin(alice, requiredMarginForLongOrder2) + await placeOrderFromLimitOrderV2(longOrder2, alice) + }) + this.afterAll(async function () { + await cancelOrderFromLimitOrderV2(longOrder2, alice) + aliceOppositeOrder = getOrderV2(market, alice.address, shortOrderBaseAssetQuantity, longOrderPrice, getRandomSalt(), true) + requiredMarginForAliceOppositeOrder = await getRequiredMarginForShortOrder(aliceOppositeOrder) + bobOppositeOrder = getOrderV2(market, bob.address, longOrderBaseAssetQuantity, shortOrderPrice, getRandomSalt(), true) + requiredMarginForBobOppositeOrder = await getRequiredMarginForLongOrder(bobOppositeOrder) + await addMargin(alice, requiredMarginForAliceOppositeOrder) + await addMargin(bob, requiredMarginForBobOppositeOrder) + await placeOrderFromLimitOrderV2(aliceOppositeOrder, alice) + await placeOrderFromLimitOrderV2(bobOppositeOrder, bob) + await waitForOrdersToMatch() + await removeAllAvailableMargin(alice) + await removeAllAvailableMargin(bob) + }) + it("returns error", async function () { + output = await juror.validateOrdersAndDetermineFillPrice([encodeLimitOrderV2WithType(longOrder2), encodeLimitOrderV2WithType(shortOrder)], fillAmount) + expect(output.err).to.equal("invalid order") + expect(output.element).to.equal(1) + expect(output.res.fillPrice.toNumber()).to.equal(0) + }) + }) + }) + context("when shortOrder's status is placed", async function () { + context("when shortOrder's baseAssetQuantity is positive", async function () { + let longOrder = getOrderV2(market, alice.address, longOrderBaseAssetQuantity, longOrderPrice, getRandomSalt()) + this.beforeAll(async function () { + requiredMarginForLongOrder = await getRequiredMarginForLongOrder(longOrder) + await addMargin(alice, requiredMarginForLongOrder) + await placeOrderFromLimitOrderV2(longOrder, alice) + }) + this.afterAll(async function () { + await cancelOrderFromLimitOrderV2(longOrder, alice) + await removeAllAvailableMargin(alice) + }) + it("returns error", async function () { + fillAmount = longOrderBaseAssetQuantity + output = await juror.validateOrdersAndDetermineFillPrice([encodeLimitOrderV2WithType(longOrder), encodeLimitOrderV2WithType(longOrder)], fillAmount) + expect(output.err).to.equal("not short") + expect(output.element).to.equal(1) + expect(output.res.fillPrice.toNumber()).to.equal(0) + }) + }) + context("when shortOrder's baseAssetQuantity is negative", async function () { + context("when shortOrder's unfilled < fillAmount", async function () { + let newLongOrderPrice = multiplyPrice(1999) + let newShortOrderPrice = multiplyPrice(2001) + let longOrder = getOrderV2(market, alice.address, longOrderBaseAssetQuantity.mul(3), newLongOrderPrice, getRandomSalt()) + let shortOrder = getOrderV2(market, bob.address, shortOrderBaseAssetQuantity, newShortOrderPrice, getRandomSalt()) + + this.beforeAll(async function () { + requiredMarginForLongOrder = await getRequiredMarginForLongOrder(longOrder) + await addMargin(alice, requiredMarginForLongOrder) + await placeOrderFromLimitOrderV2(longOrder, alice) + requiredMarginForShortOrder = await getRequiredMarginForShortOrder(shortOrder) + await addMargin(bob, requiredMarginForShortOrder) + await placeOrderFromLimitOrderV2(shortOrder, bob) + }) + this.afterAll(async function () { + await cancelOrderFromLimitOrderV2(longOrder, alice) + await cancelOrderFromLimitOrderV2(shortOrder, bob) + await removeAllAvailableMargin(alice) + await removeAllAvailableMargin(bob) + }) + + it("returns error", async function () { + fillAmount = shortOrderBaseAssetQuantity.abs().mul(2) + output = await juror.validateOrdersAndDetermineFillPrice([encodeLimitOrderV2WithType(longOrder), encodeLimitOrderV2WithType(shortOrder)], fillAmount) + expect(output.err).to.equal("overfill") + expect(output.element).to.equal(1) + expect(output.res.fillPrice.toNumber()).to.equal(0) + }) + }) + context("when shortOrder's unfilled > fillAmount", async function () { + context.skip("when order is reduceOnly", async function () { + it("returns error if fillAmount > currentPosition of shortOrder's trader", async function () { + }) + }) + }) + }) + }) + }) + }) + }) + context("when both orders are valid", async function () { + let amm, minSizeRequirement, lowerBoundPrice, upperBoundPrice + this.beforeEach(async function () { + await disableValidatorMatching() + amm = await getAMMContract(market) + minSizeRequirement = await amm.minSizeRequirement() + let maxOracleSpreadRatio = await amm.maxOracleSpreadRatio() + let oraclePrice = await amm.getUnderlyingPrice() + upperBoundPrice = oraclePrice.mul(_1e6.add(maxOracleSpreadRatio)).div(_1e6) + lowerBoundPrice = oraclePrice.mul(_1e6.sub(maxOracleSpreadRatio)).div(_1e6) + }) + + this.afterEach(async function () { + await enableValidatorMatching() + }) + + context("when amm is different for long and short orders", async function () { + it("returns error", async function () { + // needs deploying another amm + }) + }) + context("when amm is same for long and short orders", async function () { + context("when longOrder's price is less than shortOrder's price", async function () { + let longOrder = getOrderV2(market, alice.address, longOrderBaseAssetQuantity, longOrderPrice, getRandomSalt()) + let newShortOrderPrice = longOrderPrice.add(1) + let shortOrder = getOrderV2(market, bob.address, shortOrderBaseAssetQuantity, newShortOrderPrice, getRandomSalt()) + this.beforeEach(async function () { + requiredMarginForLongOrder = await getRequiredMarginForLongOrder(longOrder) + await addMargin(alice, requiredMarginForLongOrder) + await placeOrderFromLimitOrderV2(longOrder, alice) + requiredMarginForShortOrder = await getRequiredMarginForShortOrder(shortOrder) + await addMargin(bob, requiredMarginForShortOrder) + await placeOrderFromLimitOrderV2(shortOrder, bob) + }) + this.afterEach(async function () { + await cancelOrderFromLimitOrderV2(longOrder, alice) + await cancelOrderFromLimitOrderV2(shortOrder, bob) + await removeAllAvailableMargin(alice) + await removeAllAvailableMargin(bob) + }) + + it("returns error ", async function () { + fillAmount = longOrderBaseAssetQuantity + output = await juror.validateOrdersAndDetermineFillPrice([encodeLimitOrderV2WithType(longOrder), encodeLimitOrderV2WithType(shortOrder)], fillAmount) + expect(output.err).to.equal("OB_orders_do_not_match") + expect(output.element).to.equal(2) + expect(output.res.fillPrice.toNumber()).to.equal(0) + }) + }) + context("when longOrder's price is greater than shortOrder's price", async function () { + context("when fillAmount is not a multiple of minSizeRequirement", async function () { + context("when fillAmount < minSizeRequirement", async function () { + let longOrder, shortOrder + this.beforeEach(async function () { + longOrder = getOrderV2(market, alice.address, longOrderBaseAssetQuantity, longOrderPrice, getRandomSalt()) + let requiredMarginForLongOrder = await getRequiredMarginForLongOrder(longOrder) + shortOrder = getOrderV2(market, bob.address, shortOrderBaseAssetQuantity, shortOrderPrice, getRandomSalt()) + let requiredMarginForShortOrder = await getRequiredMarginForShortOrder(shortOrder) + await addMargin(alice, requiredMarginForLongOrder) + await addMargin(bob, requiredMarginForShortOrder) + await placeOrderFromLimitOrderV2(longOrder, alice) + await placeOrderFromLimitOrderV2(shortOrder, bob) + }) + this.afterEach(async function () { + await cancelOrderFromLimitOrderV2(longOrder, alice) + await cancelOrderFromLimitOrderV2(shortOrder, bob) + await removeAllAvailableMargin(alice) + await removeAllAvailableMargin(bob) + }) + + it("returns error", async function () { + let fillAmount = minSizeRequirement.div(2) + output = await juror.validateOrdersAndDetermineFillPrice([encodeLimitOrderV2WithType(longOrder), encodeLimitOrderV2WithType(shortOrder)], fillAmount) + expect(output.err).to.equal("not multiple") + expect(output.element).to.equal(2) + expect(output.res.fillPrice.toNumber()).to.equal(0) + }) + }) + context("when fillAmount > minSizeRequirement", async function () { + let longOrder, shortOrder + this.beforeEach(async function () { + longOrder = getOrderV2(market, alice.address, longOrderBaseAssetQuantity, longOrderPrice, getRandomSalt()) + let requiredMarginForLongOrder = await getRequiredMarginForLongOrder(longOrder) + shortOrder = getOrderV2(market, bob.address, shortOrderBaseAssetQuantity, shortOrderPrice, getRandomSalt()) + let requiredMarginForShortOrder = await getRequiredMarginForShortOrder(shortOrder) + await addMargin(alice, requiredMarginForLongOrder) + await addMargin(bob, requiredMarginForShortOrder) + await placeOrderFromLimitOrderV2(longOrder, alice) + await placeOrderFromLimitOrderV2(shortOrder, bob) + }) + this.afterEach(async function () { + await cancelOrderFromLimitOrderV2(longOrder, alice) + await cancelOrderFromLimitOrderV2(shortOrder, bob) + await removeAllAvailableMargin(alice) + await removeAllAvailableMargin(bob) + }) + + it("returns error", async function () { + let fillAmount = minSizeRequirement.mul(3).div(2) + output = await juror.validateOrdersAndDetermineFillPrice([encodeLimitOrderV2WithType(longOrder), encodeLimitOrderV2WithType(shortOrder)], fillAmount) + expect(output.err).to.equal("not multiple") + expect(output.element).to.equal(2) + expect(output.res.fillPrice.toNumber()).to.equal(0) + }) + }) + }) + context("when fillAmount is a multiple of minSizeRequirement", async function () { + context("when longOrder price is less than lowerBoundPrice", async function () { + let longOrder, shortOrder + this.beforeEach(async function () { + let longOrderPrice = lowerBoundPrice.sub(1) + longOrder = getOrderV2(market, alice.address, longOrderBaseAssetQuantity, longOrderPrice, getRandomSalt()) + let requiredMarginForLongOrder = await getRequiredMarginForLongOrder(longOrder) + await addMargin(alice, requiredMarginForLongOrder) + let shortOrderPrice = longOrderPrice + shortOrder = getOrderV2(market, bob.address, shortOrderBaseAssetQuantity, shortOrderPrice, getRandomSalt()) + let requiredMarginForShortOrder = await getRequiredMarginForShortOrder(shortOrder) + await addMargin(bob, requiredMarginForShortOrder) + await placeOrderFromLimitOrderV2(longOrder, alice) + await placeOrderFromLimitOrderV2(shortOrder, bob) + }) + this.afterEach(async function () { + await cancelOrderFromLimitOrderV2(longOrder, alice) + await cancelOrderFromLimitOrderV2(shortOrder, bob) + await removeAllAvailableMargin(alice) + await removeAllAvailableMargin(bob) + }) + + it("returns error", async function () { + fillAmount = minSizeRequirement.mul(3) + output = await juror.validateOrdersAndDetermineFillPrice([encodeLimitOrderV2WithType(longOrder), encodeLimitOrderV2WithType(shortOrder)], fillAmount) + expect(output.err).to.equal("long price below lower bound") + expect(output.element).to.equal(0) + expect(output.res.fillPrice.toNumber()).to.equal(0) + }) + }) + context("when longOrder price is >= lowerBoundPrice", async function () { + context("when shortOrder price is greater than upperBoundPrice", async function () { + let longOrder, shortOrder + this.beforeEach(async function () { + longOrderPrice = upperBoundPrice.add(1) + longOrder = getOrderV2(market, alice.address, longOrderBaseAssetQuantity, longOrderPrice, getRandomSalt()) + let requiredMarginForLongOrder = await getRequiredMarginForLongOrder(longOrder) + await addMargin(alice, requiredMarginForLongOrder) + shortOrderPrice = upperBoundPrice.add(1) + shortOrder = getOrderV2(market, bob.address, shortOrderBaseAssetQuantity, shortOrderPrice, getRandomSalt()) + let requiredMarginForShortOrder = await getRequiredMarginForShortOrder(shortOrder) + await addMargin(bob, requiredMarginForShortOrder) + await placeOrderFromLimitOrderV2(longOrder, alice) + await placeOrderFromLimitOrderV2(shortOrder, bob) + }) + this.afterEach(async function () { + await cancelOrderFromLimitOrderV2(longOrder, alice) + await cancelOrderFromLimitOrderV2(shortOrder, bob) + await removeAllAvailableMargin(alice) + await removeAllAvailableMargin(bob) + }) + + it("returns error", async function () { + fillAmount = minSizeRequirement.mul(3) + output = await juror.validateOrdersAndDetermineFillPrice([encodeLimitOrderV2WithType(longOrder), encodeLimitOrderV2WithType(shortOrder)], fillAmount) + expect(output.err).to.equal("short price above upper bound") + expect(output.element).to.equal(1) + expect(output.res.fillPrice.toNumber()).to.equal(0) + }) + }) + context("when shortOrder price is <= upperBoundPrice", async function () { + context("When longOrder was placed in earlier block than shortOrder", async function () { + context("if longOrder price is greater than lowerBoundPrice but less than upperBoundPrice", async function () { + let longOrder, shortOrder + this.beforeEach(async function () { + let longOrderPrice = lowerBoundPrice.add(1) + longOrder = getOrderV2(market, alice.address, longOrderBaseAssetQuantity, longOrderPrice, getRandomSalt()) + let requiredMarginForLongOrder = await getRequiredMarginForLongOrder(longOrder) + await addMargin(alice, requiredMarginForLongOrder) + let shortOrderPrice = longOrderPrice + shortOrder = getOrderV2(market, bob.address, shortOrderBaseAssetQuantity, shortOrderPrice, getRandomSalt()) + let requiredMarginForShortOrder = await getRequiredMarginForShortOrder(shortOrder) + await addMargin(bob, requiredMarginForShortOrder) + await placeOrderFromLimitOrderV2(longOrder, alice) + await placeOrderFromLimitOrderV2(shortOrder, bob) + }) + this.afterEach(async function () { + await cancelOrderFromLimitOrderV2(longOrder, alice) + await cancelOrderFromLimitOrderV2(shortOrder, bob) + await removeAllAvailableMargin(alice) + await removeAllAvailableMargin(bob) + }) + it("returns longOrder's price as fillPrice", async function () { + let fillAmount = minSizeRequirement.mul(3) + response = await juror.validateOrdersAndDetermineFillPrice([encodeLimitOrderV2WithType(longOrder), encodeLimitOrderV2WithType(shortOrder)], fillAmount) + expect(response.err).to.equal("") + expect(response.element).to.equal(3) + expect(response.res.fillPrice.toString()).to.equal(longOrder.price.toString()) + expect(response.res.instructions.length).to.equal(2) + //longOrder + expect(response.res.instructions[0].ammIndex.toNumber()).to.equal(0) + expect(response.res.instructions[0].trader).to.equal(alice.address) + expect(response.res.instructions[0].mode).to.equal(1) + longOrderHash = await limitOrderBook.getOrderHash(longOrder) + expect(response.res.instructions[0].orderHash).to.equal(longOrderHash) + + //shortOrder + expect(response.res.instructions[1].ammIndex.toNumber()).to.equal(0) + expect(response.res.instructions[1].trader).to.equal(bob.address) + expect(response.res.instructions[1].mode).to.equal(0) + shortOrderHash = await limitOrderBook.getOrderHash(shortOrder) + expect(response.res.instructions[1].orderHash).to.equal(shortOrderHash) + + expect(response.res.orderTypes.length).to.equal(2) + expect(response.res.orderTypes[0]).to.equal(0) + expect(response.res.orderTypes[1]).to.equal(0) + + expect(response.res.encodedOrders[0]).to.equal(encodeLimitOrderV2(longOrder)) + expect(response.res.encodedOrders[1]).to.equal(encodeLimitOrderV2(shortOrder)) + }) + }) + context("if longOrder price is greater than upperBoundPrice", async function () { + let longOrder, shortOrder + this.beforeEach(async function () { + let longOrderPrice = upperBoundPrice.add(1) + longOrder = getOrderV2(market, alice.address, longOrderBaseAssetQuantity, longOrderPrice, getRandomSalt()) + let requiredMarginForLongOrder = await getRequiredMarginForLongOrder(longOrder) + await addMargin(alice, requiredMarginForLongOrder) + let shortOrderPrice = upperBoundPrice.sub(1) + shortOrder = getOrderV2(market, bob.address, shortOrderBaseAssetQuantity, shortOrderPrice, getRandomSalt()) + let requiredMarginForShortOrder = await getRequiredMarginForShortOrder(shortOrder) + await addMargin(bob, requiredMarginForShortOrder) + await placeOrderFromLimitOrderV2(longOrder, alice) + await placeOrderFromLimitOrderV2(shortOrder, bob) + }) + this.afterEach(async function () { + await cancelOrderFromLimitOrderV2(longOrder, alice) + await cancelOrderFromLimitOrderV2(shortOrder, bob) + await removeAllAvailableMargin(alice) + await removeAllAvailableMargin(bob) + }) + it("returns upperBound as fillPrice", async function () { + let fillAmount = minSizeRequirement.mul(3) + response = await juror.validateOrdersAndDetermineFillPrice([encodeLimitOrderV2WithType(longOrder), encodeLimitOrderV2WithType(shortOrder)], fillAmount) + expect(response.err).to.equal("") + expect(response.element).to.equal(3) + expect(response.res.fillPrice.toString()).to.equal(upperBoundPrice.toString()) + expect(response.res.instructions.length).to.equal(2) + //longOrder + expect(response.res.instructions[0].ammIndex.toNumber()).to.equal(0) + expect(response.res.instructions[0].trader).to.equal(alice.address) + expect(response.res.instructions[0].mode).to.equal(1) + longOrderHash = await limitOrderBook.getOrderHash(longOrder) + expect(response.res.instructions[0].orderHash).to.equal(longOrderHash) + + //shortOrder + expect(response.res.instructions[1].ammIndex.toNumber()).to.equal(0) + expect(response.res.instructions[1].trader).to.equal(bob.address) + expect(response.res.instructions[1].mode).to.equal(0) + shortOrderHash = await limitOrderBook.getOrderHash(shortOrder) + expect(response.res.instructions[1].orderHash).to.equal(shortOrderHash) + + expect(response.res.orderTypes.length).to.equal(2) + expect(response.res.orderTypes[0]).to.equal(0) + expect(response.res.orderTypes[1]).to.equal(0) + + expect(response.res.encodedOrders[0]).to.equal(encodeLimitOrderV2(longOrder)) + expect(response.res.encodedOrders[1]).to.equal(encodeLimitOrderV2(shortOrder)) + }) + }) + }) + context("When shortOrder was placed in same or earlier block than longOrder", async function () { + context("if shortOrder price is less than upperBoundPrice greater than lowerBoundPrice", async function () { + let longOrder, shortOrder + this.beforeEach(async function () { + let shortOrderPrice = upperBoundPrice.sub(1) + shortOrder = getOrderV2(market, bob.address, shortOrderBaseAssetQuantity, shortOrderPrice, getRandomSalt()) + let requiredMarginForShortOrder = await getRequiredMarginForShortOrder(shortOrder) + await addMargin(bob, requiredMarginForShortOrder) + let longOrderPrice = shortOrderPrice.add(2) + longOrder = getOrderV2(market, alice.address, longOrderBaseAssetQuantity, longOrderPrice, getRandomSalt()) + let requiredMarginForLongOrder = await getRequiredMarginForLongOrder(longOrder) + await addMargin(alice, requiredMarginForLongOrder) + await placeOrderFromLimitOrderV2(shortOrder, bob) + await placeOrderFromLimitOrderV2(longOrder, alice) + }) + this.afterEach(async function () { + await cancelOrderFromLimitOrderV2(longOrder, alice) + await cancelOrderFromLimitOrderV2(shortOrder, bob) + await removeAllAvailableMargin(alice) + await removeAllAvailableMargin(bob) + }) + + it("returns shortOrder's price as fillPrice", async function () { + fillAmount = minSizeRequirement.mul(3) + response = await juror.validateOrdersAndDetermineFillPrice([encodeLimitOrderV2WithType(longOrder), encodeLimitOrderV2WithType(shortOrder)], fillAmount) + expect(response.res.fillPrice.toString()).to.equal(shortOrder.price.toString()) + expect(response.res.instructions.length).to.equal(2) + //longOrder + expect(response.res.instructions[0].ammIndex.toNumber()).to.equal(0) + expect(response.res.instructions[0].trader).to.equal(alice.address) + expect(response.res.instructions[0].mode).to.equal(0) + longOrderHash = await limitOrderBook.getOrderHash(longOrder) + expect(response.res.instructions[0].orderHash).to.equal(longOrderHash) + + //shortOrder + expect(response.res.instructions[1].ammIndex.toNumber()).to.equal(0) + expect(response.res.instructions[1].trader).to.equal(bob.address) + expect(response.res.instructions[1].mode).to.equal(1) + shortOrderHash = await limitOrderBook.getOrderHash(shortOrder) + expect(response.res.instructions[1].orderHash).to.equal(shortOrderHash) + + expect(response.res.orderTypes.length).to.equal(2) + expect(response.res.orderTypes[0]).to.equal(0) + expect(response.res.orderTypes[1]).to.equal(0) + + expect(response.res.encodedOrders[0]).to.equal(encodeLimitOrderV2(longOrder)) + expect(response.res.encodedOrders[1]).to.equal(encodeLimitOrderV2(shortOrder)) + }) + }) + context("returns lowerBoundPrice price as fillPrice if shortOrder's price is less than lowerBoundPrice", async function () { + let longOrder, shortOrder + this.beforeEach(async function () { + let shortOrderPrice = lowerBoundPrice.sub(1) + shortOrder = getOrderV2(market, bob.address, shortOrderBaseAssetQuantity, shortOrderPrice, getRandomSalt()) + let requiredMarginForShortOrder = await getRequiredMarginForShortOrder(shortOrder) + await addMargin(bob, requiredMarginForShortOrder) + let longOrderPrice = shortOrderPrice.add(2) + longOrder = getOrderV2(market, alice.address, longOrderBaseAssetQuantity, longOrderPrice, getRandomSalt()) + let requiredMarginForLongOrder = await getRequiredMarginForLongOrder(longOrder) + await addMargin(alice, requiredMarginForLongOrder) + await placeOrderFromLimitOrderV2(shortOrder, bob) + await placeOrderFromLimitOrderV2(longOrder, alice) + }) + this.afterEach(async function () { + await cancelOrderFromLimitOrderV2(longOrder, alice) + await cancelOrderFromLimitOrderV2(shortOrder, bob) + await removeAllAvailableMargin(alice) + await removeAllAvailableMargin(bob) + }) + it("returns lowerBoundPrice price as fillPrice", async function () { + fillAmount = minSizeRequirement.mul(3) + response = await juror.validateOrdersAndDetermineFillPrice([encodeLimitOrderV2WithType(longOrder), encodeLimitOrderV2WithType(shortOrder)], fillAmount) + expect(response.res.fillPrice.toString()).to.equal(lowerBoundPrice.toString()) + expect(response.res.instructions.length).to.equal(2) + //longOrder + expect(response.res.instructions[0].ammIndex.toNumber()).to.equal(0) + expect(response.res.instructions[0].trader).to.equal(alice.address) + expect(response.res.instructions[0].mode).to.equal(0) + longOrderHash = await limitOrderBook.getOrderHash(longOrder) + expect(response.res.instructions[0].orderHash).to.equal(longOrderHash) + + //shortOrder + expect(response.res.instructions[1].ammIndex.toNumber()).to.equal(0) + expect(response.res.instructions[1].trader).to.equal(bob.address) + expect(response.res.instructions[1].mode).to.equal(1) + shortOrderHash = await limitOrderBook.getOrderHash(shortOrder) + expect(response.res.instructions[1].orderHash).to.equal(shortOrderHash) + + expect(response.res.orderTypes.length).to.equal(2) + expect(response.res.orderTypes[0]).to.equal(0) + expect(response.res.orderTypes[1]).to.equal(0) + + expect(response.res.encodedOrders[0]).to.equal(encodeLimitOrderV2(longOrder)) + expect(response.res.encodedOrders[1]).to.equal(encodeLimitOrderV2(shortOrder)) + }) + }) + }) + }) + }) + }) + }) + }) + }) + }) +}) diff --git a/tests/orderbook/package-lock.json b/tests/orderbook/package-lock.json new file mode 100644 index 0000000000..6d1d5b84eb --- /dev/null +++ b/tests/orderbook/package-lock.json @@ -0,0 +1,3487 @@ +{ + "name": "orderbook", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "orderbook", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "axios": "^1.3.4", + "chai": "^4.3.7", + "chai-http": "^4.3.0", + "ethers": "^5.5.2", + "mocha": "^10.2.0" + } + }, + "node_modules/@ethersproject/abi": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abi/-/abi-5.5.0.tgz", + "integrity": "sha512-loW7I4AohP5KycATvc0MgujU6JyCHPqHdeoo9z3Nr9xEiNioxa65ccdm1+fsoJhkuhdRtfcL8cfyGamz2AxZ5w==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/address": "^5.5.0", + "@ethersproject/bignumber": "^5.5.0", + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/constants": "^5.5.0", + "@ethersproject/hash": "^5.5.0", + "@ethersproject/keccak256": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "@ethersproject/properties": "^5.5.0", + "@ethersproject/strings": "^5.5.0" + } + }, + "node_modules/@ethersproject/abstract-provider": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-provider/-/abstract-provider-5.5.1.tgz", + "integrity": "sha512-m+MA/ful6eKbxpr99xUYeRvLkfnlqzrF8SZ46d/xFB1A7ZVknYc/sXJG0RcufF52Qn2jeFj1hhcoQ7IXjNKUqg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bignumber": "^5.5.0", + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "@ethersproject/networks": "^5.5.0", + "@ethersproject/properties": "^5.5.0", + "@ethersproject/transactions": "^5.5.0", + "@ethersproject/web": "^5.5.0" + } + }, + "node_modules/@ethersproject/abstract-signer": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.5.0.tgz", + "integrity": "sha512-lj//7r250MXVLKI7sVarXAbZXbv9P50lgmJQGr2/is82EwEb8r7HrxsmMqAjTsztMYy7ohrIhGMIml+Gx4D3mA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/abstract-provider": "^5.5.0", + "@ethersproject/bignumber": "^5.5.0", + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "@ethersproject/properties": "^5.5.0" + } + }, + "node_modules/@ethersproject/address": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/address/-/address-5.5.0.tgz", + "integrity": "sha512-l4Nj0eWlTUh6ro5IbPTgbpT4wRbdH5l8CQf7icF7sb/SI3Nhd9Y9HzhonTSTi6CefI0necIw7LJqQPopPLZyWw==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bignumber": "^5.5.0", + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/keccak256": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "@ethersproject/rlp": "^5.5.0" + } + }, + "node_modules/@ethersproject/base64": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/base64/-/base64-5.5.0.tgz", + "integrity": "sha512-tdayUKhU1ljrlHzEWbStXazDpsx4eg1dBXUSI6+mHlYklOXoXF6lZvw8tnD6oVaWfnMxAgRSKROg3cVKtCcppA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.5.0" + } + }, + "node_modules/@ethersproject/basex": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/basex/-/basex-5.5.0.tgz", + "integrity": "sha512-ZIodwhHpVJ0Y3hUCfUucmxKsWQA5TMnavp5j/UOuDdzZWzJlRmuOjcTMIGgHCYuZmHt36BfiSyQPSRskPxbfaQ==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/properties": "^5.5.0" + } + }, + "node_modules/@ethersproject/bignumber": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.5.0.tgz", + "integrity": "sha512-6Xytlwvy6Rn3U3gKEc1vP7nR92frHkv6wtVr95LFR3jREXiCPzdWxKQ1cx4JGQBXxcguAwjA8murlYN2TSiEbg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "bn.js": "^4.11.9" + } + }, + "node_modules/@ethersproject/bytes": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bytes/-/bytes-5.5.0.tgz", + "integrity": "sha512-ABvc7BHWhZU9PNM/tANm/Qx4ostPGadAuQzWTr3doklZOhDlmcBqclrQe/ZXUIj3K8wC28oYeuRa+A37tX9kog==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/logger": "^5.5.0" + } + }, + "node_modules/@ethersproject/constants": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/constants/-/constants-5.5.0.tgz", + "integrity": "sha512-2MsRRVChkvMWR+GyMGY4N1sAX9Mt3J9KykCsgUFd/1mwS0UH1qw+Bv9k1UJb3X3YJYFco9H20pjSlOIfCG5HYQ==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bignumber": "^5.5.0" + } + }, + "node_modules/@ethersproject/contracts": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/contracts/-/contracts-5.5.0.tgz", + "integrity": "sha512-2viY7NzyvJkh+Ug17v7g3/IJC8HqZBDcOjYARZLdzRxrfGlRgmYgl6xPRKVbEzy1dWKw/iv7chDcS83pg6cLxg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/abi": "^5.5.0", + "@ethersproject/abstract-provider": "^5.5.0", + "@ethersproject/abstract-signer": "^5.5.0", + "@ethersproject/address": "^5.5.0", + "@ethersproject/bignumber": "^5.5.0", + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/constants": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "@ethersproject/properties": "^5.5.0", + "@ethersproject/transactions": "^5.5.0" + } + }, + "node_modules/@ethersproject/hash": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/hash/-/hash-5.5.0.tgz", + "integrity": "sha512-dnGVpK1WtBjmnp3mUT0PlU2MpapnwWI0PibldQEq1408tQBAbZpPidkWoVVuNMOl/lISO3+4hXZWCL3YV7qzfg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/abstract-signer": "^5.5.0", + "@ethersproject/address": "^5.5.0", + "@ethersproject/bignumber": "^5.5.0", + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/keccak256": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "@ethersproject/properties": "^5.5.0", + "@ethersproject/strings": "^5.5.0" + } + }, + "node_modules/@ethersproject/hdnode": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/hdnode/-/hdnode-5.5.0.tgz", + "integrity": "sha512-mcSOo9zeUg1L0CoJH7zmxwUG5ggQHU1UrRf8jyTYy6HxdZV+r0PBoL1bxr+JHIPXRzS6u/UW4mEn43y0tmyF8Q==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/abstract-signer": "^5.5.0", + "@ethersproject/basex": "^5.5.0", + "@ethersproject/bignumber": "^5.5.0", + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "@ethersproject/pbkdf2": "^5.5.0", + "@ethersproject/properties": "^5.5.0", + "@ethersproject/sha2": "^5.5.0", + "@ethersproject/signing-key": "^5.5.0", + "@ethersproject/strings": "^5.5.0", + "@ethersproject/transactions": "^5.5.0", + "@ethersproject/wordlists": "^5.5.0" + } + }, + "node_modules/@ethersproject/json-wallets": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/json-wallets/-/json-wallets-5.5.0.tgz", + "integrity": "sha512-9lA21XQnCdcS72xlBn1jfQdj2A1VUxZzOzi9UkNdnokNKke/9Ya2xA9aIK1SC3PQyBDLt4C+dfps7ULpkvKikQ==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/abstract-signer": "^5.5.0", + "@ethersproject/address": "^5.5.0", + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/hdnode": "^5.5.0", + "@ethersproject/keccak256": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "@ethersproject/pbkdf2": "^5.5.0", + "@ethersproject/properties": "^5.5.0", + "@ethersproject/random": "^5.5.0", + "@ethersproject/strings": "^5.5.0", + "@ethersproject/transactions": "^5.5.0", + "aes-js": "3.0.0", + "scrypt-js": "3.0.1" + } + }, + "node_modules/@ethersproject/keccak256": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/keccak256/-/keccak256-5.5.0.tgz", + "integrity": "sha512-5VoFCTjo2rYbBe1l2f4mccaRFN/4VQEYFwwn04aJV2h7qf4ZvI2wFxUE1XOX+snbwCLRzIeikOqtAoPwMza9kg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.5.0", + "js-sha3": "0.8.0" + } + }, + "node_modules/@ethersproject/keccak256/node_modules/js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==" + }, + "node_modules/@ethersproject/logger": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.5.0.tgz", + "integrity": "sha512-rIY/6WPm7T8n3qS2vuHTUBPdXHl+rGxWxW5okDfo9J4Z0+gRRZT0msvUdIJkE4/HS29GUMziwGaaKO2bWONBrg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ] + }, + "node_modules/@ethersproject/networks": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.5.1.tgz", + "integrity": "sha512-tYRDM4zZtSUcKnD4UMuAlj7SeXH/k5WC4SP2u1Pn57++JdXHkRu2zwNkgNogZoxHzhm9Q6qqurDBVptHOsW49Q==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/logger": "^5.5.0" + } + }, + "node_modules/@ethersproject/pbkdf2": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/pbkdf2/-/pbkdf2-5.5.0.tgz", + "integrity": "sha512-SaDvQFvXPnz1QGpzr6/HToLifftSXGoXrbpZ6BvoZhmx4bNLHrxDe8MZisuecyOziP1aVEwzC2Hasj+86TgWVg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/sha2": "^5.5.0" + } + }, + "node_modules/@ethersproject/properties": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/properties/-/properties-5.5.0.tgz", + "integrity": "sha512-l3zRQg3JkD8EL3CPjNK5g7kMx4qSwiR60/uk5IVjd3oq1MZR5qUg40CNOoEJoX5wc3DyY5bt9EbMk86C7x0DNA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/logger": "^5.5.0" + } + }, + "node_modules/@ethersproject/providers": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@ethersproject/providers/-/providers-5.5.1.tgz", + "integrity": "sha512-2zdD5sltACDWhjUE12Kucg2PcgM6V2q9JMyVvObtVGnzJu+QSmibbP+BHQyLWZUBfLApx2942+7DC5D+n4wBQQ==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/abstract-provider": "^5.5.0", + "@ethersproject/abstract-signer": "^5.5.0", + "@ethersproject/address": "^5.5.0", + "@ethersproject/basex": "^5.5.0", + "@ethersproject/bignumber": "^5.5.0", + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/constants": "^5.5.0", + "@ethersproject/hash": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "@ethersproject/networks": "^5.5.0", + "@ethersproject/properties": "^5.5.0", + "@ethersproject/random": "^5.5.0", + "@ethersproject/rlp": "^5.5.0", + "@ethersproject/sha2": "^5.5.0", + "@ethersproject/strings": "^5.5.0", + "@ethersproject/transactions": "^5.5.0", + "@ethersproject/web": "^5.5.0", + "bech32": "1.1.4", + "ws": "7.4.6" + } + }, + "node_modules/@ethersproject/random": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/random/-/random-5.5.0.tgz", + "integrity": "sha512-egGYZwZ/YIFKMHcoBUo8t3a8Hb/TKYX8BCBoLjudVCZh892welR3jOxgOmb48xznc9bTcMm7Tpwc1gHC1PFNFQ==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/logger": "^5.5.0" + } + }, + "node_modules/@ethersproject/rlp": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/rlp/-/rlp-5.5.0.tgz", + "integrity": "sha512-hLv8XaQ8PTI9g2RHoQGf/WSxBfTB/NudRacbzdxmst5VHAqd1sMibWG7SENzT5Dj3yZ3kJYx+WiRYEcQTAkcYA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/logger": "^5.5.0" + } + }, + "node_modules/@ethersproject/sha2": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/sha2/-/sha2-5.5.0.tgz", + "integrity": "sha512-B5UBoglbCiHamRVPLA110J+2uqsifpZaTmid2/7W5rbtYVz6gus6/hSDieIU/6gaKIDcOj12WnOdiymEUHIAOA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "hash.js": "1.1.7" + } + }, + "node_modules/@ethersproject/signing-key": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/signing-key/-/signing-key-5.5.0.tgz", + "integrity": "sha512-5VmseH7qjtNmDdZBswavhotYbWB0bOwKIlOTSlX14rKn5c11QmJwGt4GHeo7NrL/Ycl7uo9AHvEqs5xZgFBTng==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "@ethersproject/properties": "^5.5.0", + "bn.js": "^4.11.9", + "elliptic": "6.5.4", + "hash.js": "1.1.7" + } + }, + "node_modules/@ethersproject/solidity": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/solidity/-/solidity-5.5.0.tgz", + "integrity": "sha512-9NgZs9LhGMj6aCtHXhtmFQ4AN4sth5HuFXVvAQtzmm0jpSCNOTGtrHZJAeYTh7MBjRR8brylWZxBZR9zDStXbw==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bignumber": "^5.5.0", + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/keccak256": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "@ethersproject/sha2": "^5.5.0", + "@ethersproject/strings": "^5.5.0" + } + }, + "node_modules/@ethersproject/strings": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/strings/-/strings-5.5.0.tgz", + "integrity": "sha512-9fy3TtF5LrX/wTrBaT8FGE6TDJyVjOvXynXJz5MT5azq+E6D92zuKNx7i29sWW2FjVOaWjAsiZ1ZWznuduTIIQ==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/constants": "^5.5.0", + "@ethersproject/logger": "^5.5.0" + } + }, + "node_modules/@ethersproject/transactions": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/transactions/-/transactions-5.5.0.tgz", + "integrity": "sha512-9RZYSKX26KfzEd/1eqvv8pLauCKzDTub0Ko4LfIgaERvRuwyaNV78mJs7cpIgZaDl6RJui4o49lHwwCM0526zA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/address": "^5.5.0", + "@ethersproject/bignumber": "^5.5.0", + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/constants": "^5.5.0", + "@ethersproject/keccak256": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "@ethersproject/properties": "^5.5.0", + "@ethersproject/rlp": "^5.5.0", + "@ethersproject/signing-key": "^5.5.0" + } + }, + "node_modules/@ethersproject/units": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/units/-/units-5.5.0.tgz", + "integrity": "sha512-7+DpjiZk4v6wrikj+TCyWWa9dXLNU73tSTa7n0TSJDxkYbV3Yf1eRh9ToMLlZtuctNYu9RDNNy2USq3AdqSbag==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bignumber": "^5.5.0", + "@ethersproject/constants": "^5.5.0", + "@ethersproject/logger": "^5.5.0" + } + }, + "node_modules/@ethersproject/wallet": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/wallet/-/wallet-5.5.0.tgz", + "integrity": "sha512-Mlu13hIctSYaZmUOo7r2PhNSd8eaMPVXe1wxrz4w4FCE4tDYBywDH+bAR1Xz2ADyXGwqYMwstzTrtUVIsKDO0Q==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/abstract-provider": "^5.5.0", + "@ethersproject/abstract-signer": "^5.5.0", + "@ethersproject/address": "^5.5.0", + "@ethersproject/bignumber": "^5.5.0", + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/hash": "^5.5.0", + "@ethersproject/hdnode": "^5.5.0", + "@ethersproject/json-wallets": "^5.5.0", + "@ethersproject/keccak256": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "@ethersproject/properties": "^5.5.0", + "@ethersproject/random": "^5.5.0", + "@ethersproject/signing-key": "^5.5.0", + "@ethersproject/transactions": "^5.5.0", + "@ethersproject/wordlists": "^5.5.0" + } + }, + "node_modules/@ethersproject/web": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@ethersproject/web/-/web-5.5.1.tgz", + "integrity": "sha512-olvLvc1CB12sREc1ROPSHTdFCdvMh0J5GSJYiQg2D0hdD4QmJDy8QYDb1CvoqD/bF1c++aeKv2sR5uduuG9dQg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/base64": "^5.5.0", + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "@ethersproject/properties": "^5.5.0", + "@ethersproject/strings": "^5.5.0" + } + }, + "node_modules/@ethersproject/wordlists": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/wordlists/-/wordlists-5.5.0.tgz", + "integrity": "sha512-bL0UTReWDiaQJJYOC9sh/XcRu/9i2jMrzf8VLRmPKx58ckSlOJiohODkECCO50dtLZHcGU6MLXQ4OOrgBwP77Q==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/hash": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "@ethersproject/properties": "^5.5.0", + "@ethersproject/strings": "^5.5.0" + } + }, + "node_modules/@types/chai": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.5.tgz", + "integrity": "sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==" + }, + "node_modules/@types/cookiejar": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz", + "integrity": "sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==" + }, + "node_modules/@types/node": { + "version": "20.5.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.8.tgz", + "integrity": "sha512-eajsR9aeljqNhK028VG0Wuw+OaY5LLxYmxeoXynIoE6jannr9/Ucd1LL0hSSoafk5LTYG+FfqsyGt81Q6Zkybw==" + }, + "node_modules/@types/superagent": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-4.1.13.tgz", + "integrity": "sha512-YIGelp3ZyMiH0/A09PMAORO0EBGlF5xIKfDpK74wdYvWUs2o96b5CItJcWPdH409b7SAXIIG6p8NdU/4U2Maww==", + "dependencies": { + "@types/cookiejar": "*", + "@types/node": "*" + } + }, + "node_modules/aes-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz", + "integrity": "sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==" + }, + "node_modules/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "engines": { + "node": "*" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz", + "integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/bech32": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", + "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==" + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==" + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chai": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", + "integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^4.1.2", + "get-func-name": "^2.0.0", + "loupe": "^2.3.1", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chai-http": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/chai-http/-/chai-http-4.4.0.tgz", + "integrity": "sha512-uswN3rZpawlRaa5NiDUHcDZ3v2dw5QgLyAwnQ2tnVNuP7CwIsOFuYJ0xR1WiR7ymD4roBnJIzOUep7w9jQMFJA==", + "dependencies": { + "@types/chai": "4", + "@types/superagent": "4.1.13", + "charset": "^1.0.1", + "cookiejar": "^2.1.4", + "is-ip": "^2.0.0", + "methods": "^1.1.2", + "qs": "^6.11.2", + "superagent": "^8.0.9" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/charset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/charset/-/charset-1.0.1.tgz", + "integrity": "sha512-6dVyOOYjpfFcL1Y4qChrAoQLRHvj2ziyhcm0QJlhOcAhykL/k1kTUPbeo+87MNRTRdk2OIIsIXbuF3x2wi5EXg==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==" + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/elliptic": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", + "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ethers": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.5.2.tgz", + "integrity": "sha512-EF5W+6Wwcu6BqVwpgmyR5U2+L4c1FQzlM/02dkZOugN3KF0cG9bzHZP+TDJglmPm2/IzCEJDT7KBxzayk7SAHw==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/abi": "5.5.0", + "@ethersproject/abstract-provider": "5.5.1", + "@ethersproject/abstract-signer": "5.5.0", + "@ethersproject/address": "5.5.0", + "@ethersproject/base64": "5.5.0", + "@ethersproject/basex": "5.5.0", + "@ethersproject/bignumber": "5.5.0", + "@ethersproject/bytes": "5.5.0", + "@ethersproject/constants": "5.5.0", + "@ethersproject/contracts": "5.5.0", + "@ethersproject/hash": "5.5.0", + "@ethersproject/hdnode": "5.5.0", + "@ethersproject/json-wallets": "5.5.0", + "@ethersproject/keccak256": "5.5.0", + "@ethersproject/logger": "5.5.0", + "@ethersproject/networks": "5.5.1", + "@ethersproject/pbkdf2": "5.5.0", + "@ethersproject/properties": "5.5.0", + "@ethersproject/providers": "5.5.1", + "@ethersproject/random": "5.5.0", + "@ethersproject/rlp": "5.5.0", + "@ethersproject/sha2": "5.5.0", + "@ethersproject/signing-key": "5.5.0", + "@ethersproject/solidity": "5.5.0", + "@ethersproject/strings": "5.5.0", + "@ethersproject/transactions": "5.5.0", + "@ethersproject/units": "5.5.0", + "@ethersproject/wallet": "5.5.0", + "@ethersproject/web": "5.5.1", + "@ethersproject/wordlists": "5.5.0" + } + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formidable": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.2.tgz", + "integrity": "sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==", + "dependencies": { + "dezalgo": "^1.0.4", + "hexoid": "^1.0.0", + "once": "^1.4.0", + "qs": "^6.11.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", + "engines": { + "node": "*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "bin": { + "he": "bin/he" + } + }, + "node_modules/hexoid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", + "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==", + "engines": { + "node": ">=8" + } + }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha512-58yWmlHpp7VYfcdTwMTvwMmqx/Elfxjd9RXTDyMsbL7lLWmhMylLEqiYVLKuLzOZqVgiWXD9MfR62Vv89VRxkw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-ip": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-2.0.0.tgz", + "integrity": "sha512-9MTn0dteHETtyUx8pxqMwg5hMBi3pvlyglJ+b79KOCca0po23337LbVV2Hl4xmMvfw++ljnO0/+5G6G+0Szh6g==", + "dependencies": { + "ip-regex": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/loupe": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", + "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==", + "dependencies": { + "get-func-name": "^2.0.0" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" + }, + "node_modules/minimatch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", + "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", + "dependencies": { + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.4", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "5.0.1", + "ms": "2.1.3", + "nanoid": "3.3.3", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "workerpool": "6.2.1", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mochajs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/nanoid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", + "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "engines": { + "node": "*" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/qs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/scrypt-js": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-3.0.1.tgz", + "integrity": "sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA==" + }, + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/superagent": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.1.2.tgz", + "integrity": "sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==", + "dependencies": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.4", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^2.1.2", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.0", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=6.4.0 <13 || >=14" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "engines": { + "node": ">=4" + } + }, + "node_modules/workerpool": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", + "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/ws": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + }, + "dependencies": { + "@ethersproject/abi": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abi/-/abi-5.5.0.tgz", + "integrity": "sha512-loW7I4AohP5KycATvc0MgujU6JyCHPqHdeoo9z3Nr9xEiNioxa65ccdm1+fsoJhkuhdRtfcL8cfyGamz2AxZ5w==", + "requires": { + "@ethersproject/address": "^5.5.0", + "@ethersproject/bignumber": "^5.5.0", + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/constants": "^5.5.0", + "@ethersproject/hash": "^5.5.0", + "@ethersproject/keccak256": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "@ethersproject/properties": "^5.5.0", + "@ethersproject/strings": "^5.5.0" + } + }, + "@ethersproject/abstract-provider": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-provider/-/abstract-provider-5.5.1.tgz", + "integrity": "sha512-m+MA/ful6eKbxpr99xUYeRvLkfnlqzrF8SZ46d/xFB1A7ZVknYc/sXJG0RcufF52Qn2jeFj1hhcoQ7IXjNKUqg==", + "requires": { + "@ethersproject/bignumber": "^5.5.0", + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "@ethersproject/networks": "^5.5.0", + "@ethersproject/properties": "^5.5.0", + "@ethersproject/transactions": "^5.5.0", + "@ethersproject/web": "^5.5.0" + } + }, + "@ethersproject/abstract-signer": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.5.0.tgz", + "integrity": "sha512-lj//7r250MXVLKI7sVarXAbZXbv9P50lgmJQGr2/is82EwEb8r7HrxsmMqAjTsztMYy7ohrIhGMIml+Gx4D3mA==", + "requires": { + "@ethersproject/abstract-provider": "^5.5.0", + "@ethersproject/bignumber": "^5.5.0", + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "@ethersproject/properties": "^5.5.0" + } + }, + "@ethersproject/address": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/address/-/address-5.5.0.tgz", + "integrity": "sha512-l4Nj0eWlTUh6ro5IbPTgbpT4wRbdH5l8CQf7icF7sb/SI3Nhd9Y9HzhonTSTi6CefI0necIw7LJqQPopPLZyWw==", + "requires": { + "@ethersproject/bignumber": "^5.5.0", + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/keccak256": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "@ethersproject/rlp": "^5.5.0" + } + }, + "@ethersproject/base64": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/base64/-/base64-5.5.0.tgz", + "integrity": "sha512-tdayUKhU1ljrlHzEWbStXazDpsx4eg1dBXUSI6+mHlYklOXoXF6lZvw8tnD6oVaWfnMxAgRSKROg3cVKtCcppA==", + "requires": { + "@ethersproject/bytes": "^5.5.0" + } + }, + "@ethersproject/basex": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/basex/-/basex-5.5.0.tgz", + "integrity": "sha512-ZIodwhHpVJ0Y3hUCfUucmxKsWQA5TMnavp5j/UOuDdzZWzJlRmuOjcTMIGgHCYuZmHt36BfiSyQPSRskPxbfaQ==", + "requires": { + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/properties": "^5.5.0" + } + }, + "@ethersproject/bignumber": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.5.0.tgz", + "integrity": "sha512-6Xytlwvy6Rn3U3gKEc1vP7nR92frHkv6wtVr95LFR3jREXiCPzdWxKQ1cx4JGQBXxcguAwjA8murlYN2TSiEbg==", + "requires": { + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "bn.js": "^4.11.9" + } + }, + "@ethersproject/bytes": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bytes/-/bytes-5.5.0.tgz", + "integrity": "sha512-ABvc7BHWhZU9PNM/tANm/Qx4ostPGadAuQzWTr3doklZOhDlmcBqclrQe/ZXUIj3K8wC28oYeuRa+A37tX9kog==", + "requires": { + "@ethersproject/logger": "^5.5.0" + } + }, + "@ethersproject/constants": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/constants/-/constants-5.5.0.tgz", + "integrity": "sha512-2MsRRVChkvMWR+GyMGY4N1sAX9Mt3J9KykCsgUFd/1mwS0UH1qw+Bv9k1UJb3X3YJYFco9H20pjSlOIfCG5HYQ==", + "requires": { + "@ethersproject/bignumber": "^5.5.0" + } + }, + "@ethersproject/contracts": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/contracts/-/contracts-5.5.0.tgz", + "integrity": "sha512-2viY7NzyvJkh+Ug17v7g3/IJC8HqZBDcOjYARZLdzRxrfGlRgmYgl6xPRKVbEzy1dWKw/iv7chDcS83pg6cLxg==", + "requires": { + "@ethersproject/abi": "^5.5.0", + "@ethersproject/abstract-provider": "^5.5.0", + "@ethersproject/abstract-signer": "^5.5.0", + "@ethersproject/address": "^5.5.0", + "@ethersproject/bignumber": "^5.5.0", + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/constants": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "@ethersproject/properties": "^5.5.0", + "@ethersproject/transactions": "^5.5.0" + } + }, + "@ethersproject/hash": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/hash/-/hash-5.5.0.tgz", + "integrity": "sha512-dnGVpK1WtBjmnp3mUT0PlU2MpapnwWI0PibldQEq1408tQBAbZpPidkWoVVuNMOl/lISO3+4hXZWCL3YV7qzfg==", + "requires": { + "@ethersproject/abstract-signer": "^5.5.0", + "@ethersproject/address": "^5.5.0", + "@ethersproject/bignumber": "^5.5.0", + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/keccak256": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "@ethersproject/properties": "^5.5.0", + "@ethersproject/strings": "^5.5.0" + } + }, + "@ethersproject/hdnode": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/hdnode/-/hdnode-5.5.0.tgz", + "integrity": "sha512-mcSOo9zeUg1L0CoJH7zmxwUG5ggQHU1UrRf8jyTYy6HxdZV+r0PBoL1bxr+JHIPXRzS6u/UW4mEn43y0tmyF8Q==", + "requires": { + "@ethersproject/abstract-signer": "^5.5.0", + "@ethersproject/basex": "^5.5.0", + "@ethersproject/bignumber": "^5.5.0", + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "@ethersproject/pbkdf2": "^5.5.0", + "@ethersproject/properties": "^5.5.0", + "@ethersproject/sha2": "^5.5.0", + "@ethersproject/signing-key": "^5.5.0", + "@ethersproject/strings": "^5.5.0", + "@ethersproject/transactions": "^5.5.0", + "@ethersproject/wordlists": "^5.5.0" + } + }, + "@ethersproject/json-wallets": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/json-wallets/-/json-wallets-5.5.0.tgz", + "integrity": "sha512-9lA21XQnCdcS72xlBn1jfQdj2A1VUxZzOzi9UkNdnokNKke/9Ya2xA9aIK1SC3PQyBDLt4C+dfps7ULpkvKikQ==", + "requires": { + "@ethersproject/abstract-signer": "^5.5.0", + "@ethersproject/address": "^5.5.0", + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/hdnode": "^5.5.0", + "@ethersproject/keccak256": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "@ethersproject/pbkdf2": "^5.5.0", + "@ethersproject/properties": "^5.5.0", + "@ethersproject/random": "^5.5.0", + "@ethersproject/strings": "^5.5.0", + "@ethersproject/transactions": "^5.5.0", + "aes-js": "3.0.0", + "scrypt-js": "3.0.1" + } + }, + "@ethersproject/keccak256": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/keccak256/-/keccak256-5.5.0.tgz", + "integrity": "sha512-5VoFCTjo2rYbBe1l2f4mccaRFN/4VQEYFwwn04aJV2h7qf4ZvI2wFxUE1XOX+snbwCLRzIeikOqtAoPwMza9kg==", + "requires": { + "@ethersproject/bytes": "^5.5.0", + "js-sha3": "0.8.0" + }, + "dependencies": { + "js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==" + } + } + }, + "@ethersproject/logger": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.5.0.tgz", + "integrity": "sha512-rIY/6WPm7T8n3qS2vuHTUBPdXHl+rGxWxW5okDfo9J4Z0+gRRZT0msvUdIJkE4/HS29GUMziwGaaKO2bWONBrg==" + }, + "@ethersproject/networks": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.5.1.tgz", + "integrity": "sha512-tYRDM4zZtSUcKnD4UMuAlj7SeXH/k5WC4SP2u1Pn57++JdXHkRu2zwNkgNogZoxHzhm9Q6qqurDBVptHOsW49Q==", + "requires": { + "@ethersproject/logger": "^5.5.0" + } + }, + "@ethersproject/pbkdf2": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/pbkdf2/-/pbkdf2-5.5.0.tgz", + "integrity": "sha512-SaDvQFvXPnz1QGpzr6/HToLifftSXGoXrbpZ6BvoZhmx4bNLHrxDe8MZisuecyOziP1aVEwzC2Hasj+86TgWVg==", + "requires": { + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/sha2": "^5.5.0" + } + }, + "@ethersproject/properties": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/properties/-/properties-5.5.0.tgz", + "integrity": "sha512-l3zRQg3JkD8EL3CPjNK5g7kMx4qSwiR60/uk5IVjd3oq1MZR5qUg40CNOoEJoX5wc3DyY5bt9EbMk86C7x0DNA==", + "requires": { + "@ethersproject/logger": "^5.5.0" + } + }, + "@ethersproject/providers": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@ethersproject/providers/-/providers-5.5.1.tgz", + "integrity": "sha512-2zdD5sltACDWhjUE12Kucg2PcgM6V2q9JMyVvObtVGnzJu+QSmibbP+BHQyLWZUBfLApx2942+7DC5D+n4wBQQ==", + "requires": { + "@ethersproject/abstract-provider": "^5.5.0", + "@ethersproject/abstract-signer": "^5.5.0", + "@ethersproject/address": "^5.5.0", + "@ethersproject/basex": "^5.5.0", + "@ethersproject/bignumber": "^5.5.0", + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/constants": "^5.5.0", + "@ethersproject/hash": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "@ethersproject/networks": "^5.5.0", + "@ethersproject/properties": "^5.5.0", + "@ethersproject/random": "^5.5.0", + "@ethersproject/rlp": "^5.5.0", + "@ethersproject/sha2": "^5.5.0", + "@ethersproject/strings": "^5.5.0", + "@ethersproject/transactions": "^5.5.0", + "@ethersproject/web": "^5.5.0", + "bech32": "1.1.4", + "ws": "7.4.6" + } + }, + "@ethersproject/random": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/random/-/random-5.5.0.tgz", + "integrity": "sha512-egGYZwZ/YIFKMHcoBUo8t3a8Hb/TKYX8BCBoLjudVCZh892welR3jOxgOmb48xznc9bTcMm7Tpwc1gHC1PFNFQ==", + "requires": { + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/logger": "^5.5.0" + } + }, + "@ethersproject/rlp": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/rlp/-/rlp-5.5.0.tgz", + "integrity": "sha512-hLv8XaQ8PTI9g2RHoQGf/WSxBfTB/NudRacbzdxmst5VHAqd1sMibWG7SENzT5Dj3yZ3kJYx+WiRYEcQTAkcYA==", + "requires": { + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/logger": "^5.5.0" + } + }, + "@ethersproject/sha2": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/sha2/-/sha2-5.5.0.tgz", + "integrity": "sha512-B5UBoglbCiHamRVPLA110J+2uqsifpZaTmid2/7W5rbtYVz6gus6/hSDieIU/6gaKIDcOj12WnOdiymEUHIAOA==", + "requires": { + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "hash.js": "1.1.7" + } + }, + "@ethersproject/signing-key": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/signing-key/-/signing-key-5.5.0.tgz", + "integrity": "sha512-5VmseH7qjtNmDdZBswavhotYbWB0bOwKIlOTSlX14rKn5c11QmJwGt4GHeo7NrL/Ycl7uo9AHvEqs5xZgFBTng==", + "requires": { + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "@ethersproject/properties": "^5.5.0", + "bn.js": "^4.11.9", + "elliptic": "6.5.4", + "hash.js": "1.1.7" + } + }, + "@ethersproject/solidity": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/solidity/-/solidity-5.5.0.tgz", + "integrity": "sha512-9NgZs9LhGMj6aCtHXhtmFQ4AN4sth5HuFXVvAQtzmm0jpSCNOTGtrHZJAeYTh7MBjRR8brylWZxBZR9zDStXbw==", + "requires": { + "@ethersproject/bignumber": "^5.5.0", + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/keccak256": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "@ethersproject/sha2": "^5.5.0", + "@ethersproject/strings": "^5.5.0" + } + }, + "@ethersproject/strings": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/strings/-/strings-5.5.0.tgz", + "integrity": "sha512-9fy3TtF5LrX/wTrBaT8FGE6TDJyVjOvXynXJz5MT5azq+E6D92zuKNx7i29sWW2FjVOaWjAsiZ1ZWznuduTIIQ==", + "requires": { + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/constants": "^5.5.0", + "@ethersproject/logger": "^5.5.0" + } + }, + "@ethersproject/transactions": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/transactions/-/transactions-5.5.0.tgz", + "integrity": "sha512-9RZYSKX26KfzEd/1eqvv8pLauCKzDTub0Ko4LfIgaERvRuwyaNV78mJs7cpIgZaDl6RJui4o49lHwwCM0526zA==", + "requires": { + "@ethersproject/address": "^5.5.0", + "@ethersproject/bignumber": "^5.5.0", + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/constants": "^5.5.0", + "@ethersproject/keccak256": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "@ethersproject/properties": "^5.5.0", + "@ethersproject/rlp": "^5.5.0", + "@ethersproject/signing-key": "^5.5.0" + } + }, + "@ethersproject/units": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/units/-/units-5.5.0.tgz", + "integrity": "sha512-7+DpjiZk4v6wrikj+TCyWWa9dXLNU73tSTa7n0TSJDxkYbV3Yf1eRh9ToMLlZtuctNYu9RDNNy2USq3AdqSbag==", + "requires": { + "@ethersproject/bignumber": "^5.5.0", + "@ethersproject/constants": "^5.5.0", + "@ethersproject/logger": "^5.5.0" + } + }, + "@ethersproject/wallet": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/wallet/-/wallet-5.5.0.tgz", + "integrity": "sha512-Mlu13hIctSYaZmUOo7r2PhNSd8eaMPVXe1wxrz4w4FCE4tDYBywDH+bAR1Xz2ADyXGwqYMwstzTrtUVIsKDO0Q==", + "requires": { + "@ethersproject/abstract-provider": "^5.5.0", + "@ethersproject/abstract-signer": "^5.5.0", + "@ethersproject/address": "^5.5.0", + "@ethersproject/bignumber": "^5.5.0", + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/hash": "^5.5.0", + "@ethersproject/hdnode": "^5.5.0", + "@ethersproject/json-wallets": "^5.5.0", + "@ethersproject/keccak256": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "@ethersproject/properties": "^5.5.0", + "@ethersproject/random": "^5.5.0", + "@ethersproject/signing-key": "^5.5.0", + "@ethersproject/transactions": "^5.5.0", + "@ethersproject/wordlists": "^5.5.0" + } + }, + "@ethersproject/web": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@ethersproject/web/-/web-5.5.1.tgz", + "integrity": "sha512-olvLvc1CB12sREc1ROPSHTdFCdvMh0J5GSJYiQg2D0hdD4QmJDy8QYDb1CvoqD/bF1c++aeKv2sR5uduuG9dQg==", + "requires": { + "@ethersproject/base64": "^5.5.0", + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "@ethersproject/properties": "^5.5.0", + "@ethersproject/strings": "^5.5.0" + } + }, + "@ethersproject/wordlists": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@ethersproject/wordlists/-/wordlists-5.5.0.tgz", + "integrity": "sha512-bL0UTReWDiaQJJYOC9sh/XcRu/9i2jMrzf8VLRmPKx58ckSlOJiohODkECCO50dtLZHcGU6MLXQ4OOrgBwP77Q==", + "requires": { + "@ethersproject/bytes": "^5.5.0", + "@ethersproject/hash": "^5.5.0", + "@ethersproject/logger": "^5.5.0", + "@ethersproject/properties": "^5.5.0", + "@ethersproject/strings": "^5.5.0" + } + }, + "@types/chai": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.5.tgz", + "integrity": "sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==" + }, + "@types/cookiejar": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz", + "integrity": "sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==" + }, + "@types/node": { + "version": "20.5.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.8.tgz", + "integrity": "sha512-eajsR9aeljqNhK028VG0Wuw+OaY5LLxYmxeoXynIoE6jannr9/Ucd1LL0hSSoafk5LTYG+FfqsyGt81Q6Zkybw==" + }, + "@types/superagent": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-4.1.13.tgz", + "integrity": "sha512-YIGelp3ZyMiH0/A09PMAORO0EBGlF5xIKfDpK74wdYvWUs2o96b5CItJcWPdH409b7SAXIIG6p8NdU/4U2Maww==", + "requires": { + "@types/cookiejar": "*", + "@types/node": "*" + } + }, + "aes-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz", + "integrity": "sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==" + }, + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==" + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" + }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "axios": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz", + "integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==", + "requires": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "bech32": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", + "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==" + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" + }, + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + }, + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "requires": { + "balanced-match": "^1.0.0" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "requires": { + "fill-range": "^7.0.1" + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==" + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==" + }, + "chai": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", + "integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==", + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^4.1.2", + "get-func-name": "^2.0.0", + "loupe": "^2.3.1", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + } + }, + "chai-http": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/chai-http/-/chai-http-4.4.0.tgz", + "integrity": "sha512-uswN3rZpawlRaa5NiDUHcDZ3v2dw5QgLyAwnQ2tnVNuP7CwIsOFuYJ0xR1WiR7ymD4roBnJIzOUep7w9jQMFJA==", + "requires": { + "@types/chai": "4", + "@types/superagent": "4.1.13", + "charset": "^1.0.1", + "cookiejar": "^2.1.4", + "is-ip": "^2.0.0", + "methods": "^1.1.2", + "qs": "^6.11.2", + "superagent": "^8.0.9" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "charset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/charset/-/charset-1.0.1.tgz", + "integrity": "sha512-6dVyOOYjpfFcL1Y4qChrAoQLRHvj2ziyhcm0QJlhOcAhykL/k1kTUPbeo+87MNRTRdk2OIIsIXbuF3x2wi5EXg==" + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==" + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==" + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==" + }, + "deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "requires": { + "type-detect": "^4.0.0" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, + "dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "requires": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==" + }, + "elliptic": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", + "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "requires": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" + }, + "ethers": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.5.2.tgz", + "integrity": "sha512-EF5W+6Wwcu6BqVwpgmyR5U2+L4c1FQzlM/02dkZOugN3KF0cG9bzHZP+TDJglmPm2/IzCEJDT7KBxzayk7SAHw==", + "requires": { + "@ethersproject/abi": "5.5.0", + "@ethersproject/abstract-provider": "5.5.1", + "@ethersproject/abstract-signer": "5.5.0", + "@ethersproject/address": "5.5.0", + "@ethersproject/base64": "5.5.0", + "@ethersproject/basex": "5.5.0", + "@ethersproject/bignumber": "5.5.0", + "@ethersproject/bytes": "5.5.0", + "@ethersproject/constants": "5.5.0", + "@ethersproject/contracts": "5.5.0", + "@ethersproject/hash": "5.5.0", + "@ethersproject/hdnode": "5.5.0", + "@ethersproject/json-wallets": "5.5.0", + "@ethersproject/keccak256": "5.5.0", + "@ethersproject/logger": "5.5.0", + "@ethersproject/networks": "5.5.1", + "@ethersproject/pbkdf2": "5.5.0", + "@ethersproject/properties": "5.5.0", + "@ethersproject/providers": "5.5.1", + "@ethersproject/random": "5.5.0", + "@ethersproject/rlp": "5.5.0", + "@ethersproject/sha2": "5.5.0", + "@ethersproject/signing-key": "5.5.0", + "@ethersproject/solidity": "5.5.0", + "@ethersproject/strings": "5.5.0", + "@ethersproject/transactions": "5.5.0", + "@ethersproject/units": "5.5.0", + "@ethersproject/wallet": "5.5.0", + "@ethersproject/web": "5.5.1", + "@ethersproject/wordlists": "5.5.0" + } + }, + "fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==" + }, + "follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "formidable": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.2.tgz", + "integrity": "sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==", + "requires": { + "dezalgo": "^1.0.4", + "hexoid": "^1.0.0", + "once": "^1.4.0", + "qs": "^6.11.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==" + }, + "get-intrinsic": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3" + } + }, + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "requires": { + "is-glob": "^4.0.1" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==" + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" + }, + "hexoid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", + "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==" + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha512-58yWmlHpp7VYfcdTwMTvwMmqx/Elfxjd9RXTDyMsbL7lLWmhMylLEqiYVLKuLzOZqVgiWXD9MfR62Vv89VRxkw==" + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-ip": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-2.0.0.tgz", + "integrity": "sha512-9MTn0dteHETtyUx8pxqMwg5hMBi3pvlyglJ+b79KOCca0po23337LbVV2Hl4xmMvfw++ljnO0/+5G6G+0Szh6g==", + "requires": { + "ip-regex": "^2.0.0" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + }, + "is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==" + }, + "is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==" + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "requires": { + "argparse": "^2.0.1" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "requires": { + "p-locate": "^5.0.0" + } + }, + "log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "requires": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + } + }, + "loupe": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", + "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==", + "requires": { + "get-func-name": "^2.0.0" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" + }, + "mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==" + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" + }, + "minimatch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "mocha": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", + "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", + "requires": { + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.4", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "5.0.1", + "ms": "2.1.3", + "nanoid": "3.3.3", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "workerpool": "6.2.1", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "nanoid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", + "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==" + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + }, + "object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "requires": { + "wrappy": "1" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "requires": { + "p-limit": "^3.0.2" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" + }, + "pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==" + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" + }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "qs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "requires": { + "side-channel": "^1.0.4" + } + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "requires": { + "picomatch": "^2.2.1" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "scrypt-js": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-3.0.1.tgz", + "integrity": "sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA==" + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "requires": { + "randombytes": "^2.1.0" + } + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" + }, + "superagent": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.1.2.tgz", + "integrity": "sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==", + "requires": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.4", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^2.1.2", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.0", + "semver": "^7.3.8" + } + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "requires": { + "has-flag": "^4.0.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "requires": { + "is-number": "^7.0.0" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" + }, + "workerpool": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", + "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==" + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "ws": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "requires": {} + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==" + }, + "yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "requires": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + } + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" + } + } +} diff --git a/tests/orderbook/package.json b/tests/orderbook/package.json new file mode 100644 index 0000000000..17324dcc14 --- /dev/null +++ b/tests/orderbook/package.json @@ -0,0 +1,17 @@ +{ + "name": "orderbook", + "version": "1.0.0", + "description": "", + "scripts": { + "test": "mocha ./**/*.js --timeout 120000" + }, + "author": "", + "license": "ISC", + "dependencies": { + "axios": "^1.3.4", + "chai": "^4.3.7", + "chai-http": "^4.3.0", + "ethers": "^5.5.2", + "mocha": "^10.2.0" + } +} diff --git a/tests/orderbook/tests/test.js b/tests/orderbook/tests/test.js new file mode 100644 index 0000000000..77be742473 --- /dev/null +++ b/tests/orderbook/tests/test.js @@ -0,0 +1,716 @@ + +const { ethers } = require('ethers'); +const { BigNumber } = require('ethers') +const axios = require('axios'); +const { expect } = require('chai'); +const { randomInt } = require('crypto'); + +const OrderBookContractAddress = "0x03000000000000000000000000000000000000b0" +const MarginAccountContractAddress = "0x03000000000000000000000000000000000000b1" +const MarginAccountHelperContractAddress = "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82" +const ClearingHouseContractAddress = "0x03000000000000000000000000000000000000b2" + +let provider, domain, orderType, orderBook, marginAccount, marginAccountHelper, clearingHouse +let alice, bob, charlie, aliceAddress, bobAddress, charlieAddress +let governance +let alicePartialMatchedLongOrder, bobHighPriceShortOrder + +const ZERO = BigNumber.from(0) +const _1e6 = BigNumber.from(10).pow(6) +const _1e8 = BigNumber.from(10).pow(8) +const _1e12 = BigNumber.from(10).pow(12) +const _1e18 = ethers.constants.WeiPerEther +const maxLeverage = 5 +const tradeFeeRatio = 0.0025 + +const homedir = require('os').homedir() +let conf = require(`${homedir}/.hubblenet.json`) +const url = `http://127.0.0.1:9650/ext/bc/${conf.chain_id}/rpc` + +provider = new ethers.providers.JsonRpcProvider(url); +// Set up signer +governance = new ethers.Wallet('0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80', provider) // governance +alice = new ethers.Wallet('0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d', provider); // 0x70997970c51812dc3a010c7d01b50e0d17dc79c8 +bob = new ethers.Wallet('0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a', provider); // 0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc +charlie = new ethers.Wallet('15614556be13730e9e8d6eacc1603143e7b96987429df8726384c2ec4502ef6e', provider); // 0x55ee05df718f1a5c1441e76190eb1a19ee2c9430 +aliceAddress = alice.address.toLowerCase() +bobAddress = bob.address.toLowerCase() +charlieAddress = charlie.address.toLowerCase() +console.log({ alice: aliceAddress, bob: bobAddress, charlie: charlieAddress }); + +// Set up contract interface +orderBook = new ethers.Contract(OrderBookContractAddress, require('../abi/OrderBook.json'), provider); +clearingHouse = new ethers.Contract(ClearingHouseContractAddress, require('../abi/ClearingHouse.json'), provider); +marginAccount = new ethers.Contract(MarginAccountContractAddress, require('../abi/MarginAccount.json'), provider); +marginAccountHelper = new ethers.Contract(MarginAccountHelperContractAddress, require('../abi/MarginAccountHelper.json'), provider); + + +async function getDomain() { + domain = { + name: 'Hubble', + version: '2.0', + chainId: (await provider.getNetwork()).chainId, + verifyingContract: orderBook.address + } + return domain +} + +orderType = { + Order: [ + // field ordering must be the same as LIMIT_ORDER_TYPEHASH + { name: "ammIndex", type: "uint256" }, + { name: "trader", type: "address" }, + { name: "baseAssetQuantity", type: "int256" }, + { name: "price", type: "uint256" }, + { name: "salt", type: "uint256" }, + { name: "reduceOnly", type: "bool" }, + ] +} + +describe.skip('Submit transaction and compare with EVM state', function () { + let aliceMargin = _1e6 * 150 + let bobMargin = _1e6 * 150 + let charlieMargin = 0 + + let aliceOrderSize = 0.1 + let aliceOrderPrice = 1800 + let aliceReserved = getReservedMargin(aliceOrderSize * aliceOrderPrice) + let aliceTradeFee = getTradeFee(aliceOrderSize * aliceOrderPrice) + let aliceOpenNotional = Math.abs(aliceOrderSize * aliceOrderPrice) * 1e6 + let aliceLiquidationThreshold = getLiquidationThreshold(aliceOrderSize) + + + let bobOrderSize = -0.1 + let bobOrderPrice = 1800 + let bobOpenNotional = Math.abs(bobOrderSize * bobOrderPrice) * 1e6 + let bobLiquidationThreshold = getLiquidationThreshold(bobOrderSize) + let bobTradeFee = getTradeFee(bobOrderSize * bobOrderPrice) + + it('Add margin', async function () { + tx = await addMargin(alice, aliceMargin) + await tx.wait(); + + tx = await addMargin(bob, bobMargin) + await tx.wait(); + + const expectedState = { + "order_map": {}, + "trader_map": { + [bobAddress]: { + "positions": {}, + "margin": { + "reserved": 0, + "deposited": { + "0": bobMargin + } + } + }, + [aliceAddress]: { + "positions": {}, + "margin": { + "reserved": 0, + "deposited": { + "0": aliceMargin + } + } + } + }, + "next_funding_time": await getNextFundingTime(), + "last_price": { + "0": 0 + } + } + const evmState = await getEVMState() + // console.log(JSON.stringify(evmState, null, 2)) + expect(evmState).to.deep.contain(expectedState) + }); + + it('Remove margin', async function () { + const aliceMarginRemoved = _1e6 * 1 + const tx = await marginAccount.connect(alice).removeMargin(0, aliceMarginRemoved) + aliceMargin = aliceMargin - aliceMarginRemoved + await tx.wait(); + + const expectedState = { + "order_map": {}, + "trader_map": { + [bobAddress]: { + "positions": {}, + "margin": { + "reserved": 0, + "deposited": { + "0": bobMargin + } + } + }, + [aliceAddress]: { + "positions": {}, + "margin": { + "reserved": 0, + "deposited": { + "0": aliceMargin + } + } + } + }, + "next_funding_time": await getNextFundingTime(), + "last_price": { + "0": 0 + } + } + const evmState = await getEVMState() + // console.log(JSON.stringify(evmState, null, 2)) + expect(evmState).to.deep.contain(expectedState) + }); + + it('Place order', async function () { + const { hash, order, signature, tx, txReceipt } = await placeOrder(alice, aliceOrderSize, aliceOrderPrice, 101) + + const expectedState = { + "order_map": { + [hash]: { + "market": 0, + "position_type": "long", + "user_address": aliceAddress, + "base_asset_quantity": _1e18 * aliceOrderSize, + "filled_base_asset_quantity": 0, + "salt": 101, + "price": _1e6 * aliceOrderPrice, + "lifecycle_list": [ + { + "BlockNumber": txReceipt.blockNumber, + "Status": 0 + } + ], + "signature": signature, + "block_number": txReceipt.blockNumber, + "reduce_only": false + } + }, + "trader_map": { + [bobAddress]: { + "positions": {}, + "margin": { + "reserved": 0, + "deposited": { + "0": bobMargin + } + } + }, + [aliceAddress]: { + "positions": {}, + "margin": { + "reserved": aliceReserved, + "deposited": { + "0": aliceMargin + } + } + } + }, + "next_funding_time": await getNextFundingTime(), + "last_price": { + "0": 0 + } + } + + const evmState = await getEVMState() + // console.log(JSON.stringify(evmState, null, 2)) + expect(evmState).to.deep.contain(expectedState) + }); + + it('Match order', async function () { + const {tx} = await placeOrder(bob, bobOrderSize, bobOrderPrice, 201) + + await sleep(2) + + const expectedState = { + "order_map": {}, + "trader_map": { + [bobAddress]: { + "positions": { + "0": { + "open_notional": bobOpenNotional, + "size": _1e18 * bobOrderSize, + "unrealised_funding": 0, + "last_premium_fraction": 0, + "liquidation_threshold": bobLiquidationThreshold + } + }, + "margin": { + "reserved": 0, + "deposited": { + "0": bobMargin - bobTradeFee + } + } + }, + [aliceAddress]: { + "positions": { + "0": { + "open_notional": aliceOpenNotional, + "size": _1e18 * aliceOrderSize, + "unrealised_funding": 0, + "last_premium_fraction": 0, + "liquidation_threshold": aliceLiquidationThreshold + } + }, + "margin": { + "reserved": 0, + "deposited": { + "0": aliceMargin - aliceTradeFee + } + } + } + }, + "next_funding_time": await getNextFundingTime(), + "last_price": { + "0": _1e6 * aliceOrderPrice + } + } + const evmState = await getEVMState() + // console.log(JSON.stringify(evmState, null, 2)) + expect(evmState).to.deep.contain(expectedState) + }); + + it('Order cancel', async function () { + const { hash, order } = await placeOrder(alice, 0.1, 1800, 401) + + tx = await orderBook.connect(alice).cancelOrder(hash) + await tx.wait() + + // same as last test scenario + const expectedState = { + "order_map": {}, + "trader_map": { + [bobAddress]: { + "positions": { + "0": { + "open_notional": bobOpenNotional, + "size": _1e18 * bobOrderSize, + "unrealised_funding": 0, + "last_premium_fraction": 0, + "liquidation_threshold": bobLiquidationThreshold + } + }, + "margin": { + "reserved": 0, + "deposited": { + "0": bobMargin - bobTradeFee + } + } + }, + [aliceAddress]: { + "positions": { + "0": { + "open_notional": aliceOpenNotional, + "size": _1e18 * aliceOrderSize, + "unrealised_funding": 0, + "last_premium_fraction": 0, + "liquidation_threshold": aliceLiquidationThreshold + } + }, + "margin": { + "reserved": 0, + "deposited": { + "0": aliceMargin - aliceTradeFee + } + } + } + }, + "next_funding_time": await getNextFundingTime(), + "last_price": { + "0": _1e6 * aliceOrderPrice + } + } + + const evmState = await getEVMState() + // console.log(JSON.stringify(evmState, null, 2)) + expect(evmState).to.deep.contain(expectedState) + }); + + it('Partially match order', async function () { + alicePartialMatchedLongOrder = await placeOrder(alice, 0.2, aliceOrderPrice, 301) + const { tx, hash: bobShortOrderHash } = await placeOrder(bob, -0.1, bobOrderPrice, 302) + + await tx.wait() + + aliceOrderSize = aliceOrderSize + 0.1 + bobOrderSize = bobOrderSize - 0.1 + bobOpenNotional = Math.abs(bobOrderSize * bobOrderPrice) * 1e6 + aliceOpenNotional = Math.abs(aliceOrderSize * aliceOrderPrice) * 1e6 + bobLiquidationThreshold = getLiquidationThreshold(bobOrderSize) + aliceLiquidationThreshold = getLiquidationThreshold(aliceOrderSize) + + aliceTradeFee = getTradeFee(0.2 * aliceOrderPrice) + bobTradeFee = getTradeFee(0.2 * bobOrderPrice) + aliceReserved = getReservedMargin(0.1 * aliceOrderPrice) // reserved only for 0.1 size + const expectedState = { + "order_map": { + [alicePartialMatchedLongOrder.hash]: { + "market": 0, + "position_type": "long", + "user_address": aliceAddress, + "base_asset_quantity": 200000000000000000, + "filled_base_asset_quantity": 100000000000000000, + "salt": 301, + "price": 1800000000, + "lifecycle_list": [ + { + "BlockNumber": alicePartialMatchedLongOrder.txReceipt.blockNumber, + "Status": 0 + } + ], + "signature": alicePartialMatchedLongOrder.signature, + "block_number": alicePartialMatchedLongOrder.txReceipt.blockNumber, + "reduce_only": false + } + }, + "trader_map": { + [bobAddress]: { + "positions": { + "0": { + "open_notional": bobOpenNotional, + "size": _1e18 * bobOrderSize, + "unrealised_funding": 0, + "last_premium_fraction": 0, + "liquidation_threshold": bobLiquidationThreshold + } + }, + "margin": { + "reserved": 0, + "deposited": { + "0": bobMargin - bobTradeFee + } + } + }, + [aliceAddress]: { + "positions": { + "0": { + "open_notional": aliceOpenNotional, + "size": _1e18 * aliceOrderSize, + "unrealised_funding": 0, + "last_premium_fraction": 0, + "liquidation_threshold": aliceLiquidationThreshold + } + }, + "margin": { + "reserved": aliceReserved, + "deposited": { + "0": aliceMargin - aliceTradeFee + } + } + } + }, + "next_funding_time": await getNextFundingTime(), + "last_price": { + "0": _1e6 * aliceOrderPrice + } + } + const evmState = await getEVMState() + // console.log(JSON.stringify(evmState, null, 2)) + expect(evmState).to.deep.contain(expectedState) + }); + + + // it.skip('Order match error', async function () { + // // place an order with reduceOnly which should fail + // // const { hash: charlieHash } = await placeOrder(charlie, 50, 12, 501) + // await orderBook.connect(alice).cancelOrder(alicePartialMatchedLongOrder.hash) + // bobReverseOrder = await placeOrder(bob, 6.95, 10, 501) + // aliceReverseOrder = await placeOrder(alice, -7, 10, 502) + // // bobHighPriceShortOrder = await placeOrder(bob, -2, 10, 502) // reduceOnly; this should fail while matching + + // await sleep(2) + // const expectedState = { + // "order_map": { + // [aliceReverseOrder.hash]: { + // "market": 0, + // "position_type": "short", + // "user_address": aliceAddress, + // "base_asset_quantity": -7000000000000000000, + // "filled_base_asset_quantity": -6950000000000000000, + // "salt": 502, + // "price": 10000000, + // "lifecycle_list": [ + // { + // "BlockNumber": aliceReverseOrder.txReceipt.blockNumber, + // "Status": 0 + // } + // ], + // "signature": aliceReverseOrder.signature, + // "block_number": aliceReverseOrder.txReceipt.blockNumber, + // "reduce_only": false + // } + // }, + // "trader_map": { + // [bobAddress]: { + // "positions": { + // "0": { + // "open_notional": 70000000, + // "size": -7000000000000000000, + // "unrealised_funding": 0, + // "last_premium_fraction": 0, + // "liquidation_threshold": -5000000000000000000 + // } + // }, + // "margin": { + // "reserved": 0, + // "deposited": { + // "0": 39965000 + // } + // } + // }, + // [aliceAddress]: { + // "positions": { + // "0": { + // "open_notional": 70000000, + // "size": 7000000000000000000, + // "unrealised_funding": 0, + // "last_premium_fraction": 0, + // "liquidation_threshold": 5000000000000000000 + // } + // }, + // "margin": { + // "reserved": 0, + // "deposited": { + // "0": 29965000 + // } + // } + // } + // }, + // "next_funding_time": await getNextFundingTime(), + // "last_price": { + // "0": 10000000 + // } + // } + // const evmState = await getEVMState() + // console.log(JSON.stringify(evmState, null, 2)) + // expect(evmState).to.deep.contain(expectedState) + // }); + + it('Liquidate trader', async function () { + await addMargin(charlie, _1e6.mul(100)) + await addMargin(alice, _1e6.mul(300)) + await addMargin(bob, _1e6.mul(200)) + + await orderBook.connect(alice).cancelOrder(alicePartialMatchedLongOrder.hash) + + aliceMargin = aliceMargin + (_1e6 * 300) + bobMargin = bobMargin + (_1e6 * 200) + charlieMargin = _1e6 * 100 + + // large position by charlie + let charlieOrderSize = 0.25 + let charliePrice = 1800 + await placeOrder(charlie, charlieOrderSize, charliePrice, 601) + await placeOrder(bob, -charlieOrderSize, charliePrice, 602) + + bobOrderSize -= charlieOrderSize + bobOpenNotional = bobOpenNotional + Math.abs(charlieOrderSize * charliePrice * _1e6) + let charlieOpenNotional = Math.abs(charlieOrderSize * charliePrice * _1e6) + + // reduce the price + let reducedPrice = 1400 + await setOraclePrice(0, reducedPrice * _1e6) + const { hash: aliceHash } = await placeOrder(alice, 0.01, reducedPrice, 603) + const { hash: bobHash2 } = await placeOrder(bob, -0.01, reducedPrice, 604) + + bobOpenNotional = bobOpenNotional + Math.abs(0.01 * reducedPrice * _1e6) + aliceOpenNotional = aliceOpenNotional + Math.abs(0.01 * reducedPrice * _1e6) + aliceOrderSize += 0.01 + bobOrderSize -= 0.01 + bobLiquidationThreshold = getLiquidationThreshold(bobOrderSize) + aliceLiquidationThreshold = getLiquidationThreshold(aliceOrderSize) + let charlieLiquidationThreshold = getLiquidationThreshold(charlieOrderSize) + aliceTradeFee = getTradeFee(aliceOpenNotional/_1e6) + bobTradeFee = getTradeFee(bobOpenNotional/_1e6) + let charlieTradeFee = getTradeFee(charlieOpenNotional/_1e6) + + // 1 long order so that liquidation can run + // const aliceNewPrice = 1800 + // let increasedPrice = 1800 + // await setOraclePrice(0, increasedPrice * _1e6) + const aliceLongOrderForLiquidation = await placeOrder(alice, charlieOrderSize, reducedPrice, 605) + aliceOrderSize += charlieOrderSize + aliceOpenNotional = aliceOpenNotional + Math.abs(charlieOrderSize * reducedPrice * _1e6) + aliceLiquidationThreshold = getLiquidationThreshold(aliceOrderSize) + aliceTradeFee = aliceTradeFee + getTradeFee(charlieOrderSize * reducedPrice) + + charlieMargin = (_1e6 * 100 + - charlieTradeFee // tradeFee for initial order + - (reducedPrice * charlieOrderSize * 0.05 * _1e6) // 5% liquidation penalty + - ((charliePrice - reducedPrice) * charlieOrderSize * _1e6)) // negative pnl for liquidated position + + + const expectedState = { + "order_map": {}, + "trader_map": { + [bobAddress]: { + "positions": { + "0": { + "open_notional": bobOpenNotional, + "size": _1e18 * bobOrderSize, + "unrealised_funding": 0, + "last_premium_fraction": 0, + "liquidation_threshold": bobLiquidationThreshold + } + }, + "margin": { + "reserved": 0, + "deposited": { + "0": bobMargin - bobTradeFee + } + } + }, + [aliceAddress]: { + "positions": { + "0": { + "open_notional": aliceOpenNotional, + "size": _1e18 * aliceOrderSize, + "unrealised_funding": 0, + "last_premium_fraction": 0, + "liquidation_threshold": aliceLiquidationThreshold + } + }, + "margin": { + "reserved": 0, + "deposited": { + "0": aliceMargin - aliceTradeFee + } + } + }, + [charlieAddress]: { + "positions": { + "0": { + "open_notional": 0, + "size": 0, + "unrealised_funding": 0, + "last_premium_fraction": 0, + "liquidation_threshold": 0 + } + }, + "margin": { + "reserved": 0, + "deposited": { + "0": charlieMargin + } + } + } + }, + "next_funding_time": await getNextFundingTime(), + "last_price": { + "0": _1e6 * reducedPrice + } + } + + await sleep(5) + const evmState = await getEVMState() + // console.log(JSON.stringify(evmState, null, 2)) + // console.log(JSON.stringify(expectedState, null, 2)) + expect(evmState).to.deep.contain(expectedState) + }); +}); + +async function placeOrder(market, trader, size, price, salt=Date.now(), reduceOnly=false) { + const order = { + ammIndex: market, + trader: trader.address, + baseAssetQuantity: ethers.utils.parseEther(size.toString()), + price: ethers.utils.parseUnits(price.toString(), 6), + salt: BigNumber.from(salt), + reduceOnly: reduceOnly, + } + const tx = await orderBook.connect(trader).placeOrder(order) + const txReceipt = await tx.wait() + return { tx, txReceipt } +} + +async function addMargin(trader, amount) { + const hgtAmount = _1e12.mul(amount) + console.log("adding margin") + const tx = await marginAccountHelper.connect(trader).addVUSDMarginWithReserve(amount, { value: hgtAmount }) + const txReceipt = await tx.wait() + return { tx, txReceipt } +} + +async function getNextFundingTime() { + const fundingEvents = await clearingHouse.queryFilter('FundingRateUpdated') + const latestFundingEvent = fundingEvents.pop() + return latestFundingEvent.args.nextFundingTime.toNumber() +} + +function getLiquidationThreshold(size) { + const absSize = Math.abs(size) + let liquidationThreshold = Math.max(absSize / 4, 0.01) + return size >= 0 ? _1e18 * liquidationThreshold : _1e18 * -liquidationThreshold; +} + +function getReservedMargin(notional) { + const leveraged = Math.abs(notional / maxLeverage) + let tradeFee = leveraged * tradeFeeRatio + let reserved = leveraged + tradeFee + return _1e6 * reserved +} + +function getTradeFee(notional) { + const leveraged = Math.abs(notional / maxLeverage) + let tradeFee = leveraged * tradeFeeRatio + return _1e6 * tradeFee +} + +async function setOraclePrice(market, price) { + const ammAddress = await clearingHouse.amms(market) + const amm = new ethers.Contract(ammAddress, require('../abi/AMM.json'), provider); + const underlying = await amm.underlyingAsset() + const oracleAddress = await marginAccount.oracle() + const oracle = new ethers.Contract(oracleAddress, require('../abi/Oracle.json'), provider); + + await oracle.connect(governance).setUnderlyingPrice(underlying, price) +} + +async function getOraclePrice(market) { + const ammAddress = await clearingHouse.amms(market) + const amm = new ethers.Contract(ammAddress, require('../abi/AMM.json'), provider); + return await amm.getUnderlyingPrice() +} + +async function getLastPrice(market) { + const ammAddress = await clearingHouse.amms(market) + const amm = new ethers.Contract(ammAddress, require('../abi/AMM.json'), provider); + return await amm.getLastPrice() +} + +async function getEVMState() { + const response = await axios.post(url, { + jsonrpc: '2.0', + id: 1, + method: 'orderbook_getDetailedOrderBookData', + params: [] + }, { + headers: { + 'Content-Type': 'application/json' + } + }); + + return response.data.result +} + +function sleep(s) { + console.log(`Requested a sleep of ${s} seconds...`) + return new Promise(resolve => setTimeout(resolve, s * 1000)); +} + + +module.exports = { + OrderBookContractAddress, + ClearingHouseContractAddress, + MarginAccountContractAddress, + MarginAccountHelperContractAddress, + _1e6, + _1e12, + _1e18, + placeOrder, + addMargin, + sleep, + getOraclePrice, + getLastPrice +} diff --git a/tests/orderbook/utils.js b/tests/orderbook/utils.js new file mode 100644 index 0000000000..e8ec62e60f --- /dev/null +++ b/tests/orderbook/utils.js @@ -0,0 +1,439 @@ +const { ethers, BigNumber } = require('ethers'); + +const _1e6 = BigNumber.from(10).pow(6) +const _1e12 = BigNumber.from(10).pow(12) +const _1e18 = BigNumber.from(10).pow(18) +const homedir = require('os').homedir() +let conf = require(`${homedir}/.hubblenet.json`) +const url = `http://127.0.0.1:9650/ext/bc/${conf.chain_id}/rpc` +provider = new ethers.providers.JsonRpcProvider(url); + +// Set up signer +governance = new ethers.Wallet('0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80', provider) // governance +alice = new ethers.Wallet('0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d', provider); // 0x70997970c51812dc3a010c7d01b50e0d17dc79c8 +bob = new ethers.Wallet('0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a', provider); // 0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc +charlie = new ethers.Wallet('15614556be13730e9e8d6eacc1603143e7b96987429df8726384c2ec4502ef6e', provider); // 0x55ee05df718f1a5c1441e76190eb1a19ee2c9430 + +const OrderBookContractAddress = "0x03000000000000000000000000000000000000b0" +const MarginAccountContractAddress = "0x03000000000000000000000000000000000000b1" +const ClearingHouseContractAddress = "0x03000000000000000000000000000000000000b2" +const JurorPrecompileAddress = "0x03000000000000000000000000000000000000a0" +const TicksPrecompileAddress = "0x03000000000000000000000000000000000000a1" +const LimitOrderBookContractAddress = "0x03000000000000000000000000000000000000b3" +const IOCContractAddress = "0x03000000000000000000000000000000000000b4" + +orderBook = new ethers.Contract(OrderBookContractAddress, require('./abi/OrderBook.json'), provider); +limitOrderBook = new ethers.Contract(LimitOrderBookContractAddress, require('./abi/LimitOrderBook.json'), provider); +clearingHouse = new ethers.Contract(ClearingHouseContractAddress, require('./abi/ClearingHouse.json'), provider); +marginAccount = new ethers.Contract(MarginAccountContractAddress, require('./abi/MarginAccount.json'), provider); +ioc = new ethers.Contract(IOCContractAddress, require('./abi/IOC.json'), provider); +juror = new ethers.Contract(JurorPrecompileAddress, require('./abi/Juror.json'), provider); +ticks = new ethers.Contract(TicksPrecompileAddress, require('./abi/Ticks.json'), provider); + +orderType = { + Order: [ + // field ordering must be the same as LIMIT_ORDER_TYPEHASH + { name: "trader", type: "address" }, + { name: "baseAssetQuantity", type: "int256" }, + { name: "price", type: "uint256" }, + { name: "salt", type: "uint256" }, + ] +} + +function getOrder(market, traderAddress, baseAssetQuantity, price, salt, reduceOnly=false) { + return { + ammIndex: market, + trader: traderAddress, + baseAssetQuantity: baseAssetQuantity, + price: price, + salt: BigNumber.from(salt), + reduceOnly: reduceOnly, + } +} + +function getOrderV2(ammIndex, trader, baseAssetQuantity, price, salt, reduceOnly=false, postOnly=false) { + return { + ammIndex, + trader, + baseAssetQuantity, + price, + salt: BigNumber.from(salt), + reduceOnly, + postOnly + } +} + +function getIOCOrder(expireAt, ammIndex, trader, baseAssetQuantity, price, salt, reduceOnly=false) { + return { + orderType: 1, + expireAt: expireAt, + ammIndex: ammIndex, + trader: trader, + baseAssetQuantity: baseAssetQuantity, + price: price, + salt: salt, + reduceOnly: false + } +} + +//Convert to wei units to support 18 decimals +function multiplySize(size) { + // return _1e18.mul(size) + return ethers.utils.parseEther(size.toString()) +} + +function multiplyPrice(price) { + return _1e6.mul(price) + // return ethers.utils.parseUnits(price.toString(), 6) +} + +async function getDomain() { + domain = { + name: "Hubble", + version: "2.0", + chainId: (await provider.getNetwork()).chainId, + verifyingContract: orderBook.address + } + return domain +} + +async function placeOrder(market, trader, size, price, salt=Date.now(), reduceOnly=false) { + order = getOrder(market, trader.address, size, price, salt, reduceOnly) + return placeOrderFromLimitOrder(order, trader) +} + +async function placeOrderFromLimitOrder(order, trader) { + const tx = await limitOrderBook.connect(trader).placeOrders([order]) + const txReceipt = await tx.wait() + return { tx, txReceipt } +} + +async function placeOrderFromLimitOrderV2(order, trader) { + // console.log({ placeOrderEstimateGas: (await limitOrderBook.connect(trader).estimateGas.placeOrders([order])).toNumber() }) + // return limitOrderBook.connect(trader).placeOrders([order]) + const tx = await limitOrderBook.connect(trader).placeOrders([order]) + const txReceipt = await tx.wait() + return { tx, txReceipt } +} + +async function placeV2Orders(orders, trader) { + console.log({ placeOrdersEstimateGas: (await limitOrderBook.connect(trader).estimateGas.placeOrders(orders)).toNumber() }) + const tx = await limitOrderBook.connect(trader).placeOrders(orders) + const txReceipt = await tx.wait() + return { tx, txReceipt } +} + +async function placeIOCOrder(order, trader) { + const tx = await ioc.connect(trader).placeOrders([order]) + const txReceipt = await tx.wait() + return { tx, txReceipt } +} + +async function cancelOrderFromLimitOrder(order, trader) { + const tx = await limitOrderBook.connect(trader).cancelOrder(order) + const txReceipt = await tx.wait() + return { tx, txReceipt } +} + +async function cancelOrderFromLimitOrderV2(order, trader) { + // console.log({ estimateGas: (await limitOrderBook.connect(trader).estimateGas.cancelOrders([order])).toNumber() }) + // return limitOrderBook.connect(trader).cancelOrders([order]) + const tx = await limitOrderBook.connect(trader).cancelOrders([order]) + const txReceipt = await tx.wait() + return { tx, txReceipt } +} + +async function cancelV2Orders(orders, trader) { + console.log({ cancelV2OrdersEstimateGas: (await limitOrderBook.connect(trader).estimateGas.cancelOrders(orders)).toNumber() }) + const tx = await limitOrderBook.connect(trader).cancelOrders(orders) + const txReceipt = await tx.wait() + return { tx, txReceipt } +} + +function sleep(s) { + return new Promise(resolve => setTimeout(resolve, s * 1000)); +} + +async function addMargin(trader, amount, txOpts={}) { + const hgtAmount = _1e12.mul(amount) + marginAccountHelper = await getMarginAccountHelper() + const tx = await marginAccountHelper.connect(trader).addVUSDMarginWithReserve(amount, trader.address, Object.assign(txOpts, { value: hgtAmount })) + const result = await marginAccount.marginAccountHelper() + const txReceipt = await tx.wait() + return { tx, txReceipt } +} + +async function removeMargin(trader, amount) { + const hgtAmount = _1e12.mul(amount) + marginAccountHelper = await getMarginAccountHelper() + const tx = await marginAccountHelper.connect(trader).removeMarginInUSD(hgtAmount) + const txReceipt = await tx.wait() + return { tx, txReceipt } +} + +async function removeAllAvailableMargin(trader) { + margin = await marginAccount.getAvailableMargin(trader.address) + marginAccountHelper = await getMarginAccountHelper() + if (margin.toNumber() > 0) { + // const tx = await marginAccountHelper.connect(trader).removeMarginInUSD(5e11) + const tx = await marginAccountHelper.connect(trader).removeMarginInUSD(margin.toNumber()) + await tx.wait() + } + return +} + +async function getMarginAccountHelper() { + marginAccountHelperAddress = await marginAccount.marginAccountHelper() + return new ethers.Contract(marginAccountHelperAddress, require('./abi/MarginAccountHelper.json'), provider) +} + +function encodeLimitOrder(order) { + const encodedOrder = ethers.utils.defaultAbiCoder.encode( + [ + 'uint256', + 'address', + 'int256', + 'uint256', + 'uint256', + 'bool', + ], + [ + order.ammIndex, + order.trader, + order.baseAssetQuantity, + order.price, + order.salt, + order.reduceOnly, + ] + ) + return encodedOrder +} + +function encodeLimitOrderWithType(order) { + encodedOrder = encodeLimitOrder(order) + const typedEncodedOrder = ethers.utils.defaultAbiCoder.encode(['uint8', 'bytes'], [0, encodedOrder]) + return typedEncodedOrder +} + +function encodeLimitOrderV2(order) { + const encodedOrder = ethers.utils.defaultAbiCoder.encode( + [ + 'uint256', + 'address', + 'int256', + 'uint256', + 'uint256', + 'bool', + 'bool', + ], + [ + order.ammIndex, + order.trader, + order.baseAssetQuantity, + order.price, + order.salt, + order.reduceOnly, + order.postOnly, + ] + ) + return encodedOrder +} + +function encodeLimitOrderV2WithType(order) { + encodedOrder = encodeLimitOrderV2(order) + const typedEncodedOrder = ethers.utils.defaultAbiCoder.encode(['uint8', 'bytes'], [0, encodedOrder]) + return typedEncodedOrder +} + +// async function cleanUpPositionsAndRemoveMargin(market, trader1, trader2) { +// position1 = await amm.positions(trader1.address) +// position2 = await amm.positions(trader2.address) +// if (position1.size.toString() != "0" && position2.size.toString() != "0") { +// if (position1.size.toString() != positionSize2.size.toString()) { +// console.log("Position sizes are not equal") +// return +// } +// price = BigNumber.from(position1.notionalPosition.toString()).div(BigNumber.from(position1.size.toString())) +// console.log("placing opposite orders to close positions") +// await placeOrder(market, trader1, positionSize1, price) +// await placeOrder(market, trader2, positionSize2, price) +// await sleep(10) +// } + +// console.log("removing margin for both traders") +// await removeAllAvailableMargin(trader1) +// await removeAllAvailableMargin(trader2) +// } + +function getRandomSalt() { + // return date and add random number to generate unique salts even when called concurrently(hopefully) + // Return a random number between 0 and 1000: + randomNumber = Date.now() + Math.floor(Math.random()*1000) + return BigNumber.from(randomNumber) +} + +async function waitForOrdersToMatch() { + await sleep(5) +} + +async function enableValidatorMatching() { + const tx = await orderBook.connect(governance).setValidatorStatus(ethers.utils.getAddress('0x4Cf2eD3665F6bFA95cE6A11CFDb7A2EF5FC1C7E4'), true) + await tx.wait() +} + +async function disableValidatorMatching() { + const tx = await orderBook.connect(governance).setValidatorStatus(ethers.utils.getAddress('0x4Cf2eD3665F6bFA95cE6A11CFDb7A2EF5FC1C7E4'), false) + await tx.wait() +} + +async function getAMMContract(market) { + const ammAddress = await clearingHouse.amms(market) + amm = new ethers.Contract(ammAddress, require("./abi/AMM.json"), provider); + return amm +} + +async function getMinSizeRequirement(market) { + const amm = await getAMMContract(market) + return await amm.minSizeRequirement() +} + +async function getMakerFee() { + return await clearingHouse.makerFee() +} + +async function getTakerFee() { + return await clearingHouse.takerFee() +} + +async function getOrderBookEvents(fromBlock=0) { + block = await provider.getBlock("latest") + events = await orderBook.queryFilter("*",fromBlock,block.number) + return events +} + +async function getLimitOrderBookEvents(fromBlock=0) { + block = await provider.getBlock("latest") + events = await limitOrderBook.queryFilter("*",fromBlock,block.number) + return events +} + +function bnToFloat(num, decimals = 6) { + return parseFloat(ethers.utils.formatUnits(num.toString(), decimals)) +} + +async function getRequiredMarginForLongOrder(longOrder) { + price = longOrder.price + baseAssetQuantity = longOrder.baseAssetQuantity + + minAllowableMargin = await clearingHouse.minAllowableMargin() + takerFee = await clearingHouse.takerFee() + + quoteAsset = baseAssetQuantity.mul(price).div(_1e18).abs() + requiredMargin = quoteAsset.mul(minAllowableMargin).div(_1e6) + requiredTakerFee = quoteAsset.mul(takerFee).div(_1e6) + totalRequiredMargin = requiredMargin.add(requiredTakerFee) + return totalRequiredMargin +} + +async function getRequiredMarginForShortOrder(shortOrder) { + price = shortOrder.price + baseAssetQuantity = shortOrder.baseAssetQuantity + + minAllowableMargin = await clearingHouse.minAllowableMargin() + takerFee = await clearingHouse.takerFee() + amm = await getAMMContract(shortOrder.ammIndex) + oraclePrice = await amm.getUnderlyingPrice() + maxOracleSpreadRatio = await amm.maxOracleSpreadRatio() + upperBound = oraclePrice.mul(maxOracleSpreadRatio.add(_1e6)).div(_1e6) + if (price < upperBound) { + price = upperBound + } + // for shortOrder we use upperBound to reservePrice as it is the worst price + quoteAsset = baseAssetQuantity.mul(price).div(_1e18).abs() + requiredMargin = quoteAsset.mul(minAllowableMargin).div(_1e6) + requiredTakerFee = quoteAsset.mul(takerFee).div(_1e6) + return requiredMargin.add(requiredTakerFee) +} + +async function getEventsFromOrderBookTx(transactionHash) { + tx = await provider.getTransaction(transactionHash) + events = await getOrderBookEvents(tx.blockNumber) + var orderBookLogsWithEvent = [] + for(i = 0; i < events.length; i++) { + if(events[i].transactionHash == transactionHash) { + orderBookLogsWithEvent.push(events[i]) + break + } + } + return orderBookLogsWithEvent +} + +async function getEventsFromLimitOrderBookTx(transactionHash) { + tx = await provider.getTransaction(transactionHash) + events = await getLimitOrderBookEvents(tx.blockNumber) + var limitOrderBookLogsWithEvent = [] + for(i = 0; i < events.length; i++) { + if(events[i].transactionHash == transactionHash) { + limitOrderBookLogsWithEvent.push(events[i]) + break + } + } + return limitOrderBookLogsWithEvent +} + +module.exports = { + _1e6, + _1e12, + _1e18, + addMargin, + alice, + bnToFloat, + bob, + cancelOrderFromLimitOrder, + cancelOrderFromLimitOrderV2, + cancelV2Orders, + charlie, + clearingHouse, + disableValidatorMatching, + enableValidatorMatching, + encodeLimitOrder, + encodeLimitOrderWithType, + encodeLimitOrderV2, + encodeLimitOrderV2WithType, + getAMMContract, + getDomain, + getEventsFromOrderBookTx, + getEventsFromLimitOrderBookTx, + getIOCOrder, + getOrder, + getOrderV2, + getMakerFee, + getMinSizeRequirement, + getOrderBookEvents, + getLimitOrderBookEvents, + getRandomSalt, + getRequiredMarginForLongOrder, + getRequiredMarginForShortOrder, + getTakerFee, + governance, + ioc, + juror, + limitOrderBook, + marginAccount, + multiplySize, + multiplyPrice, + orderBook, + orderType, + provider, + placeOrder, + placeV2Orders, + placeOrderFromLimitOrder, + placeOrderFromLimitOrderV2, + placeIOCOrder, + removeAllAvailableMargin, + removeMargin, + sleep, + ticks, + url, + waitForOrdersToMatch, +} diff --git a/utils/bigint.go b/utils/bigint.go new file mode 100644 index 0000000000..5bf956d12c --- /dev/null +++ b/utils/bigint.go @@ -0,0 +1,59 @@ +package utils + +import ( + "fmt" + "math" + "math/big" +) + +func BigIntMax(x, y *big.Int) *big.Int { + if x.Cmp(y) == 1 { + return big.NewInt(0).Set(x) + } else { + return big.NewInt(0).Set(y) + } +} + +func BigIntMin(x, y *big.Int) *big.Int { + if x.Cmp(y) == -1 { + return big.NewInt(0).Set(x) + } else { + return big.NewInt(0).Set(y) + } +} + +// BigIntMinAbs calculates minimum of absolute values +func BigIntMinAbs(x, y *big.Int) *big.Int { + xAbs := big.NewInt(0).Abs(x) + yAbs := big.NewInt(0).Abs(y) + if xAbs.Cmp(yAbs) == -1 { + return big.NewInt(0).Set(xAbs) + } else { + return big.NewInt(0).Set(yAbs) + } +} + +func BigIntToDecimal(x *big.Int, scale int, decimals int) string { + // Create big.Float from x + f := new(big.Float).SetInt(x) + + // Create big.Float for scale and set its value + s := new(big.Float) + s.SetInt(big.NewInt(int64(1))) + for i := 0; i < scale; i++ { + s.Mul(s, big.NewFloat(10)) + } + + // Divide x by scale + f.Quo(f, s) + + // Setting precision and converting big.Float to string + str := fmt.Sprintf("%.*f", decimals, f) + + return str +} + +func BigIntToFloat(number *big.Int, scale int8) float64 { + float, _ := new(big.Float).Quo(new(big.Float).SetInt(number), big.NewFloat(math.Pow10(int(scale)))).Float64() + return float +} diff --git a/utils/file.go b/utils/file.go new file mode 100644 index 0000000000..b818ae7c37 --- /dev/null +++ b/utils/file.go @@ -0,0 +1,20 @@ +package utils + +import ( + "os" +) + +func AppendToFile(file string, data []byte) error { + f, err := os.OpenFile(file, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return err + } + defer f.Close() + + // Add a newline to the beginning of the data + data = append([]byte("\n"), data...) + if _, err := f.Write(data); err != nil { + return err + } + return nil +} diff --git a/utils/string.go b/utils/string.go new file mode 100644 index 0000000000..ef00402eff --- /dev/null +++ b/utils/string.go @@ -0,0 +1,25 @@ +package utils + +import ( + "strings" + "unicode" +) + +func ContainsString(list []string, item string) bool { + for _, i := range list { + if i == item { + return true + } + } + return false +} + +func RemoveSpacesAndSpecialChars(str string) string { + var builder strings.Builder + for _, r := range str { + if unicode.IsLetter(r) || unicode.IsNumber(r) { + builder.WriteRune(r) + } + } + return builder.String() +}