├── .editorconfig ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── deploy-gh-pages.yml │ └── lint-and-test.yml ├── .gitignore ├── .gitlab-ci.yml ├── .npmignore ├── .prettierignore ├── .vscode └── tasks.json ├── LICENSE ├── README.md ├── eslint.config.js ├── jest.config.js ├── package.json ├── src ├── __tests__ │ ├── main.test.ts │ └── token.test.ts ├── abis │ ├── croc.ts │ ├── erc20.read.ts │ ├── erc20.ts │ ├── external │ │ ├── L1GasPriceOracle.ts │ │ └── TempestVaultAbi.ts │ ├── impact.ts │ ├── index.ts │ └── query.ts ├── constants.ts ├── constants │ └── mainnetTokens.ts ├── context.ts ├── croc.ts ├── encoding │ ├── flags.ts │ ├── init.ts │ ├── knockout.ts │ ├── liquidity.ts │ └── longform.ts ├── examples │ ├── demo.ts │ ├── poolPrice.ts │ └── queryMinimal.ts ├── index.ts ├── knockout.ts ├── pool.ts ├── position.ts ├── recipes │ └── reposition.ts ├── slots.ts ├── swap.ts ├── tokens.ts ├── utils │ ├── gas.ts │ ├── index.ts │ ├── liquidity.ts │ ├── math.ts │ ├── price.ts │ └── token.ts └── vaults │ └── tempest.ts ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | max_line_length = 0 14 | trim_trailing_whitespace = true 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Expected Behavior 2 | 3 | 4 | ## Actual Behavior 5 | 6 | 7 | ## Steps to Reproduce the Problem 8 | 9 | 1. 10 | 1. 11 | 1. 12 | 13 | ## Specifications 14 | 15 | - Version: 16 | - Platform: 17 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Fixes # 2 | 3 | > It's a good idea to open an issue first for discussion. 4 | 5 | - [ ] Tests pass 6 | - [ ] Appropriate changes to README are included in PR 7 | -------------------------------------------------------------------------------- /.github/workflows/deploy-gh-pages.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Builds the docs and deploys to GitHub pages 3 | # 4 | # https://github.com/actions/setup-node 5 | # Using https://github.com/marketplace/actions/deploy-to-github-pages 6 | name: Deploy to Github pages 7 | 8 | on: 9 | push: 10 | #tags: 11 | # - v* 12 | branches: 13 | - master 14 | 15 | jobs: 16 | deploy_pages: 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | - uses: actions/setup-node@v2 22 | with: 23 | node-version: '12' 24 | cache: 'yarn' 25 | - run: yarn install 26 | - run: yarn docs 27 | 28 | - run: touch docs/.nojekyll 29 | - name: Deploy docs 🚀 30 | uses: JamesIves/github-pages-deploy-action@releases/v3 31 | with: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | BRANCH: gh-pages # The branch the action should deploy to. 34 | FOLDER: docs # The folder the action should deploy. 35 | -------------------------------------------------------------------------------- /.github/workflows/lint-and-test.yml: -------------------------------------------------------------------------------- 1 | name: Lint and test 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | build_and_test: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | nodejs: [18, 20, 21] 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | # https://github.com/actions/setup-node 17 | - uses: actions/setup-node@v2-beta 18 | with: 19 | node-version: ${{ matrix.nodejs }} 20 | 21 | - run: yarn install 22 | - run: yarn add -D esbuild 23 | - run: yarn test 24 | - run: yarn lint 25 | - run: yarn build-all 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /build/ 3 | /lib/ 4 | /dist/ 5 | /docs/ 6 | .idea/* 7 | 8 | .DS_Store 9 | coverage 10 | *.log 11 | 12 | tmp* 13 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: node:12 2 | 3 | cache: 4 | paths: 5 | - node_modules/ 6 | 7 | stages: 8 | - test 9 | - deploy 10 | 11 | lint-and-test: 12 | stage: test 13 | script: 14 | - yarn install 15 | - yarn add -D esbuild 16 | - yarn test 17 | - yarn lint 18 | - yarn build-all 19 | 20 | # Deploy docs to Gitlab pages 21 | pages: 22 | stage: deploy 23 | only: 24 | - master 25 | script: 26 | - yarn install 27 | - yarn docs 28 | - mv docs public 29 | artifacts: 30 | paths: 31 | - public 32 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /.github/ 2 | /.vscode/ 3 | /node_modules/ 4 | /build/ 5 | /tmp/ 6 | .idea/* 7 | /docs/ 8 | 9 | coverage 10 | *.log 11 | .gitlab-ci.yml 12 | 13 | package-lock.json 14 | /*.tgz 15 | /tmp* 16 | /mnt/ 17 | /package/ 18 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | ** -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "build", 7 | "group": { 8 | "kind": "build", 9 | "isDefault": true 10 | }, 11 | "problemMatcher": [], 12 | "label": "npm: build", 13 | "detail": "tsc -p tsconfig.json" 14 | }, 15 | { 16 | "type": "npm", 17 | "script": "test", 18 | "group": { 19 | "kind": "test", 20 | "isDefault": true 21 | }, 22 | "problemMatcher": [], 23 | "label": "npm: test", 24 | "detail": "jest" 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Crocodile Labs, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🛠🐊🛠 An SDK for building applications on top of CrocSwap 2 | 3 | ## Getting Started 4 | 5 | ### https://www.npmjs.com/package/@crocswap-libs/sdk 6 | 7 | ```bash 8 | # Clone the repository (you can also click "Use this template") 9 | git clone https://github.com/CrocSwap/sdk.git your_project_name 10 | cd your_project_name 11 | 12 | # Edit `package.json` and `tsconfig.json` to your liking 13 | ... 14 | 15 | # Install dependencies 16 | yarn install 17 | 18 | # Now you can run various yarn commands: 19 | yarn lint 20 | yarn test 21 | yarn build-all 22 | yarn build-local (builds and copies /dist to node_modules/@crocswap-libs/sdk) 23 | (note for above command - you may need to update this command in package.json with the correct path to your local interface directory.) 24 | yarn ts-node 25 | ... 26 | 27 | # featured capabilities 28 | 29 | parse a transaction receipt from ethers.js and web3.js 30 | 31 | # run the first example app (note: run 'yarn build-all' first) 32 | # yarn ts-node ./examples/01-retrieve-spot-price.ts 33 | 34 | # project dependencies 35 | ethers v6 36 | ``` 37 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | 'no-unexpected-multiline': 'off', 4 | '@typescript-eslint/no-inferrable-types': 'off' 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ['/src'], 3 | testMatch: [ 4 | "**/__tests__/**/*.+(ts|tsx|js)", 5 | "**/?(*.)+(spec|test).+(ts|tsx|js)" 6 | ], 7 | transform: { 8 | "^.+\\.(ts|tsx)$": "ts-jest" 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@crocswap-libs/sdk", 3 | "version": "2.0.14", 4 | "description": "🛠🐊🛠 An SDK for building applications on top of CrocSwap", 5 | "author": "Ben Wolski ", 6 | "repository": "https://github.com/CrocSwap/sdk.git", 7 | "license": "MIT", 8 | "keywords": [ 9 | "web3", 10 | "DEX", 11 | "sdk" 12 | ], 13 | "main": "./dist/index.js", 14 | "types": "./dist/index.d.ts", 15 | "files": [ 16 | "dist/**/*" 17 | ], 18 | "scripts": { 19 | "lint": "eslint .", 20 | "test": "vitest", 21 | "clean": "rm -rf dist build package", 22 | "ts-node": "ts-node", 23 | "docs": "typedoc --entryPoints src/index.ts", 24 | "build": "tsc -p tsconfig.json", 25 | "build-all": "yarn clean && yarn build", 26 | "copy-local": "cp -r dist ../ambient-ts-app/node_modules/@crocswap-libs/sdk/", 27 | "build-local": "yarn build-all && yarn copy-local" 28 | }, 29 | "devDependencies": { 30 | "@typescript-eslint/eslint-plugin": "^8.18.2", 31 | "@typescript-eslint/parser": "^8.18.2", 32 | "esbuild": "^0.24.2", 33 | "eslint": "^9.17.0", 34 | "ts-node": "^10.9.2", 35 | "typedoc": "^0.27.5", 36 | "typescript": "^5.7.2", 37 | "vitest": "^2.1.8" 38 | }, 39 | "dependencies": { 40 | "ethers": "^6.13.5" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/__tests__/main.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { 3 | baseConcFactor, 4 | calcRangeTilt, 5 | concDepositSkew, 6 | fromDisplayPrice, 7 | pinTickLower, 8 | pinTickUpper, 9 | quoteConcFactor, 10 | toDisplayPrice 11 | } from "../utils"; 12 | import { fromDisplayQty, toDisplayQty } from "../utils/token"; 13 | 14 | describe("Utility Functions Tests", () => { 15 | it("1 is 1?", () => { 16 | expect(1).toBe(1); 17 | }); 18 | 19 | it("scaleQty integer as string", () => { 20 | const scaledQty = fromDisplayQty("1", 18).toString(); 21 | expect(scaledQty).toBe("1000000000000000000"); 22 | }); 23 | 24 | it("scaledQty float as string", () => { 25 | const scaledQty = fromDisplayQty(".1", 18).toString(); 26 | expect(scaledQty).toBe("100000000000000000"); 27 | }); 28 | 29 | // it("throws error on scaledQty longer than decimals", () => { 30 | // expect(() => { 31 | // fromDisplayQty("1.1234567", 6); 32 | // }).toThrowError(); 33 | // }); 34 | 35 | it("unscaleQty integer as string", () => { 36 | const unscaledQty = toDisplayQty("100", 2).toString(); 37 | expect(unscaledQty).toBe("1.0"); 38 | }); 39 | 40 | it("throws error on unscaleQty float as string", () => { 41 | expect(() => { 42 | toDisplayQty("100.1", 2).toString(); 43 | }).toThrowError(); 44 | }); 45 | 46 | it("to display price", () => { 47 | expect(toDisplayPrice(1500, 18, 18, false)).toBeCloseTo(1500, 0.0001); 48 | expect(toDisplayPrice(2000, 18, 18, true)).toBeCloseTo(0.0005, 0.0001); 49 | expect(toDisplayPrice(20, 6, 10, false)).toBeCloseTo(200000, 0.0001); 50 | expect(toDisplayPrice(20, 6, 10, true)).toBeCloseTo(0.000005, 0.0001); 51 | }); 52 | 53 | it("from display price", () => { 54 | expect(fromDisplayPrice(toDisplayPrice(1500, 18, 18, false), 18, 18, false)).toBeCloseTo(1500, 0.0001); 55 | expect(fromDisplayPrice(toDisplayPrice(2000, 18, 18, true), 18, 18, true)).toBeCloseTo(2000, 0.0001); 56 | expect(fromDisplayPrice(toDisplayPrice(20, 10, 6, false), 10, 6, false)).toBeCloseTo(20, 0.0001); 57 | expect(fromDisplayPrice(toDisplayPrice(20, 10, 6, true), 10, 6, true)).toBeCloseTo(20, 0.0001); 58 | }); 59 | 60 | it("pin tick upper", () => { 61 | expect(pinTickUpper(5943, 50)).toBe(86950); 62 | expect(pinTickUpper(0.042, 50)).toBe(-31700); 63 | }); 64 | 65 | it("pin tick lower", () => { 66 | expect(pinTickLower(5943, 50)).toBe(86900); 67 | expect(pinTickLower(0.042, 50)).toBe(-31750); 68 | }); 69 | 70 | it("range collateral tilt", () => { 71 | expect(calcRangeTilt(0.9, -5000, -3000)).toBe(Infinity); 72 | expect(calcRangeTilt(0.9, 3000, 5000)).toBe(0); 73 | expect(calcRangeTilt(0.9, -5000, 5000)).toBe(0.9); 74 | }); 75 | 76 | it("base conc factor", () => { 77 | expect(baseConcFactor(25.0, 9.0, 100.0)).toBe(2.5); 78 | expect(baseConcFactor(1.0, 9.0, 100.0)).toBe(Infinity); 79 | expect(baseConcFactor(400.0, 9.0, 100.0)).toBe(1 / 0.35); 80 | }); 81 | 82 | it("quote conc factor", () => { 83 | expect(quoteConcFactor(25.0, 9.0, 64.0)).toBe(1 / 0.375); 84 | expect(quoteConcFactor(1.0, 16.0, 100.0)).toBe(1 / 0.15); 85 | expect(quoteConcFactor(400.0, 9.0, 100.0)).toBe(Infinity); 86 | }); 87 | 88 | it("conc deposit skew", () => { 89 | expect(concDepositSkew(25.0, 9.0, 100.0)).toBe(0.8); 90 | expect(concDepositSkew(1.0, 16.0, 100.0)).toBe(0); 91 | expect(concDepositSkew(400.0, 9.0, 100.0)).toBe(Infinity); 92 | }); 93 | 94 | // it("liquidity quote tokens", () => { 95 | // expect(liquidityForQuoteQty(0.01 ** 2, BigInt(10000))).toBe(100); 96 | // expect(liquidityForQuoteQty(0.01075 ** 2, BigInt(9998))).toBe(107); 97 | // }); 98 | 99 | // it("liquidity base tokens", () => { 100 | // expect(liquidityForBaseQty(0.01 ** 2, BigInt(50))).toBe(5000); 101 | // expect(liquidityForBaseQty(109 ** 2, BigInt(9999))).toBe(91); 102 | // }); 103 | 104 | // it("truncate right bits", () => { 105 | // expect(truncateRightBits(BigInt(48024845023), 10)).toBe(48024844288); 106 | // }); 107 | }); 108 | -------------------------------------------------------------------------------- /src/__tests__/token.test.ts: -------------------------------------------------------------------------------- 1 | import { ZeroAddress } from "ethers"; 2 | import { describe, expect, it } from "vitest"; 3 | import { 4 | getBaseTokenAddress, 5 | getQuoteTokenAddress, 6 | sortBaseQuoteTokens, 7 | } from "../utils/token"; 8 | 9 | describe("Token Address Utility Functions", () => { 10 | it("getQuoteTokenAddress returns correct address when ETH compared with Dai on Kovan", () => { 11 | const daiKovanAddress = "0x4F96Fe3b7A6Cf9725f59d353F723c1bDb64CA6Aa"; 12 | const quoteAddress = getQuoteTokenAddress(ZeroAddress, daiKovanAddress); 13 | expect(quoteAddress).toBe(daiKovanAddress); 14 | }); 15 | 16 | it("getBaseTokenAddress returns correct address when ETH compared with Dai on Kovan", () => { 17 | const daiKovanAddress = "0x4F96Fe3b7A6Cf9725f59d353F723c1bDb64CA6Aa"; 18 | const baseAddress = getBaseTokenAddress(ZeroAddress, daiKovanAddress); 19 | expect(baseAddress).toBe(ZeroAddress); 20 | }); 21 | 22 | it("sortBaseQuoteTokens returns correct address array when ETH compared with Dai on Kovan when already correctly sorted", () => { 23 | const daiKovanAddress = "0x4F96Fe3b7A6Cf9725f59d353F723c1bDb64CA6Aa"; 24 | const addressArray = sortBaseQuoteTokens(ZeroAddress, daiKovanAddress); 25 | expect(addressArray).toStrictEqual([ZeroAddress, daiKovanAddress]); 26 | }); 27 | 28 | it("sortBaseQuoteTokens returns correct address array when ETH compared with Dai on Kovan when NOT already correctly sorted", () => { 29 | const daiKovanAddress = "0x4F96Fe3b7A6Cf9725f59d353F723c1bDb64CA6Aa"; 30 | const addressArray = sortBaseQuoteTokens(daiKovanAddress, ZeroAddress); 31 | expect(addressArray).toStrictEqual([ZeroAddress, daiKovanAddress]); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/abis/croc.ts: -------------------------------------------------------------------------------- 1 | export const CROC_ABI = [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "address", 6 | "name": "authority", 7 | "type": "address" 8 | }, 9 | { 10 | "internalType": "address", 11 | "name": "coldPath", 12 | "type": "address" 13 | } 14 | ], 15 | "stateMutability": "nonpayable", 16 | "type": "constructor" 17 | }, 18 | { 19 | "anonymous": false, 20 | "inputs": [ 21 | { 22 | "indexed": true, 23 | "internalType": "bytes32", 24 | "name": "pool", 25 | "type": "bytes32" 26 | }, 27 | { 28 | "indexed": true, 29 | "internalType": "int24", 30 | "name": "tick", 31 | "type": "int24" 32 | }, 33 | { 34 | "indexed": false, 35 | "internalType": "bool", 36 | "name": "isBid", 37 | "type": "bool" 38 | }, 39 | { 40 | "indexed": false, 41 | "internalType": "uint32", 42 | "name": "pivotTime", 43 | "type": "uint32" 44 | }, 45 | { 46 | "indexed": false, 47 | "internalType": "uint64", 48 | "name": "feeMileage", 49 | "type": "uint64" 50 | } 51 | ], 52 | "name": "CrocKnockoutCross", 53 | "type": "event" 54 | }, 55 | { 56 | "inputs": [ 57 | { 58 | "internalType": "uint16", 59 | "name": "callpath", 60 | "type": "uint16" 61 | }, 62 | { 63 | "internalType": "bytes", 64 | "name": "cmd", 65 | "type": "bytes" 66 | }, 67 | { 68 | "internalType": "bool", 69 | "name": "sudo", 70 | "type": "bool" 71 | } 72 | ], 73 | "name": "protocolCmd", 74 | "outputs": [ 75 | { 76 | "internalType": "bytes", 77 | "name": "", 78 | "type": "bytes" 79 | } 80 | ], 81 | "stateMutability": "payable", 82 | "type": "function" 83 | }, 84 | { 85 | "inputs": [ 86 | { 87 | "internalType": "uint256", 88 | "name": "slot", 89 | "type": "uint256" 90 | } 91 | ], 92 | "name": "readSlot", 93 | "outputs": [ 94 | { 95 | "internalType": "uint256", 96 | "name": "data", 97 | "type": "uint256" 98 | } 99 | ], 100 | "stateMutability": "view", 101 | "type": "function" 102 | }, 103 | { 104 | "inputs": [ 105 | { 106 | "internalType": "address", 107 | "name": "base", 108 | "type": "address" 109 | }, 110 | { 111 | "internalType": "address", 112 | "name": "quote", 113 | "type": "address" 114 | }, 115 | { 116 | "internalType": "uint256", 117 | "name": "poolIdx", 118 | "type": "uint256" 119 | }, 120 | { 121 | "internalType": "bool", 122 | "name": "isBuy", 123 | "type": "bool" 124 | }, 125 | { 126 | "internalType": "bool", 127 | "name": "inBaseQty", 128 | "type": "bool" 129 | }, 130 | { 131 | "internalType": "uint128", 132 | "name": "qty", 133 | "type": "uint128" 134 | }, 135 | { 136 | "internalType": "uint16", 137 | "name": "tip", 138 | "type": "uint16" 139 | }, 140 | { 141 | "internalType": "uint128", 142 | "name": "limitPrice", 143 | "type": "uint128" 144 | }, 145 | { 146 | "internalType": "uint128", 147 | "name": "minOut", 148 | "type": "uint128" 149 | }, 150 | { 151 | "internalType": "uint8", 152 | "name": "reserveFlags", 153 | "type": "uint8" 154 | } 155 | ], 156 | "name": "swap", 157 | "outputs": [ 158 | { 159 | "internalType": "int128", 160 | "name": "", 161 | "type": "int128" 162 | } 163 | ], 164 | "stateMutability": "payable", 165 | "type": "function" 166 | }, 167 | { 168 | "inputs": [ 169 | { 170 | "internalType": "uint16", 171 | "name": "callpath", 172 | "type": "uint16" 173 | }, 174 | { 175 | "internalType": "bytes", 176 | "name": "cmd", 177 | "type": "bytes" 178 | } 179 | ], 180 | "name": "userCmd", 181 | "outputs": [ 182 | { 183 | "internalType": "bytes", 184 | "name": "", 185 | "type": "bytes" 186 | } 187 | ], 188 | "stateMutability": "payable", 189 | "type": "function" 190 | }, 191 | { 192 | "inputs": [ 193 | { 194 | "internalType": "uint16", 195 | "name": "proxyIdx", 196 | "type": "uint16" 197 | }, 198 | { 199 | "internalType": "bytes", 200 | "name": "cmd", 201 | "type": "bytes" 202 | }, 203 | { 204 | "internalType": "bytes", 205 | "name": "conds", 206 | "type": "bytes" 207 | }, 208 | { 209 | "internalType": "bytes", 210 | "name": "relayerTip", 211 | "type": "bytes" 212 | }, 213 | { 214 | "internalType": "bytes", 215 | "name": "signature", 216 | "type": "bytes" 217 | } 218 | ], 219 | "name": "userCmdRelayer", 220 | "outputs": [ 221 | { 222 | "internalType": "bytes", 223 | "name": "output", 224 | "type": "bytes" 225 | } 226 | ], 227 | "stateMutability": "payable", 228 | "type": "function" 229 | }, 230 | { 231 | "inputs": [ 232 | { 233 | "internalType": "uint16", 234 | "name": "proxyIdx", 235 | "type": "uint16" 236 | }, 237 | { 238 | "internalType": "bytes", 239 | "name": "input", 240 | "type": "bytes" 241 | }, 242 | { 243 | "internalType": "address", 244 | "name": "client", 245 | "type": "address" 246 | }, 247 | { 248 | "internalType": "uint256", 249 | "name": "salt", 250 | "type": "uint256" 251 | } 252 | ], 253 | "name": "userCmdRouter", 254 | "outputs": [ 255 | { 256 | "internalType": "bytes", 257 | "name": "", 258 | "type": "bytes" 259 | } 260 | ], 261 | "stateMutability": "payable", 262 | "type": "function" 263 | } 264 | ]; 265 | -------------------------------------------------------------------------------- /src/abis/erc20.read.ts: -------------------------------------------------------------------------------- 1 | export const ERC20_READ_ABI = [ 2 | { 3 | "constant": true, 4 | "inputs": [], 5 | "name": "name", 6 | "outputs": [ 7 | { 8 | "name": "", 9 | "type": "string" 10 | } 11 | ], 12 | "payable": false, 13 | "stateMutability": "view", 14 | "type": "function" 15 | }, 16 | { 17 | "constant": true, 18 | "inputs": [], 19 | "name": "totalSupply", 20 | "outputs": [ 21 | { 22 | "name": "", 23 | "type": "uint256" 24 | } 25 | ], 26 | "payable": false, 27 | "stateMutability": "view", 28 | "type": "function" 29 | }, 30 | { 31 | "constant": true, 32 | "inputs": [], 33 | "name": "decimals", 34 | "outputs": [ 35 | { 36 | "name": "", 37 | "type": "uint8" 38 | } 39 | ], 40 | "payable": false, 41 | "stateMutability": "view", 42 | "type": "function" 43 | }, 44 | { 45 | "constant": true, 46 | "inputs": [ 47 | { 48 | "name": "_owner", 49 | "type": "address" 50 | } 51 | ], 52 | "name": "balanceOf", 53 | "outputs": [ 54 | { 55 | "name": "balance", 56 | "type": "uint256" 57 | } 58 | ], 59 | "payable": false, 60 | "stateMutability": "view", 61 | "type": "function" 62 | }, 63 | { 64 | "constant": true, 65 | "inputs": [], 66 | "name": "symbol", 67 | "outputs": [ 68 | { 69 | "name": "", 70 | "type": "string" 71 | } 72 | ], 73 | "payable": false, 74 | "stateMutability": "view", 75 | "type": "function" 76 | }, 77 | { 78 | "constant": true, 79 | "inputs": [ 80 | { 81 | "name": "_owner", 82 | "type": "address" 83 | }, 84 | { 85 | "name": "_spender", 86 | "type": "address" 87 | } 88 | ], 89 | "name": "allowance", 90 | "outputs": [ 91 | { 92 | "name": "", 93 | "type": "uint256" 94 | } 95 | ], 96 | "payable": false, 97 | "stateMutability": "view", 98 | "type": "function" 99 | }, 100 | { 101 | "anonymous": false, 102 | "inputs": [ 103 | { 104 | "indexed": true, 105 | "name": "owner", 106 | "type": "address" 107 | }, 108 | { 109 | "indexed": true, 110 | "name": "spender", 111 | "type": "address" 112 | }, 113 | { 114 | "indexed": false, 115 | "name": "value", 116 | "type": "uint256" 117 | } 118 | ], 119 | "name": "Approval", 120 | "type": "event" 121 | }, 122 | { 123 | "anonymous": false, 124 | "inputs": [ 125 | { 126 | "indexed": true, 127 | "name": "from", 128 | "type": "address" 129 | }, 130 | { 131 | "indexed": true, 132 | "name": "to", 133 | "type": "address" 134 | }, 135 | { 136 | "indexed": false, 137 | "name": "value", 138 | "type": "uint256" 139 | } 140 | ], 141 | "name": "Transfer", 142 | "type": "event" 143 | } 144 | ] -------------------------------------------------------------------------------- /src/abis/erc20.ts: -------------------------------------------------------------------------------- 1 | export const ERC20_ABI = [ 2 | { 3 | "constant": true, 4 | "inputs": [], 5 | "name": "name", 6 | "outputs": [ 7 | { 8 | "name": "", 9 | "type": "string" 10 | } 11 | ], 12 | "payable": false, 13 | "stateMutability": "view", 14 | "type": "function" 15 | }, 16 | { 17 | "constant": false, 18 | "inputs": [ 19 | { 20 | "name": "_spender", 21 | "type": "address" 22 | }, 23 | { 24 | "name": "_value", 25 | "type": "uint256" 26 | } 27 | ], 28 | "name": "approve", 29 | "outputs": [ 30 | { 31 | "name": "", 32 | "type": "bool" 33 | } 34 | ], 35 | "payable": false, 36 | "stateMutability": "nonpayable", 37 | "type": "function" 38 | }, 39 | { 40 | "constant": true, 41 | "inputs": [], 42 | "name": "totalSupply", 43 | "outputs": [ 44 | { 45 | "name": "", 46 | "type": "uint256" 47 | } 48 | ], 49 | "payable": false, 50 | "stateMutability": "view", 51 | "type": "function" 52 | }, 53 | { 54 | "constant": false, 55 | "inputs": [ 56 | { 57 | "name": "_from", 58 | "type": "address" 59 | }, 60 | { 61 | "name": "_to", 62 | "type": "address" 63 | }, 64 | { 65 | "name": "_value", 66 | "type": "uint256" 67 | } 68 | ], 69 | "name": "transferFrom", 70 | "outputs": [ 71 | { 72 | "name": "", 73 | "type": "bool" 74 | } 75 | ], 76 | "payable": false, 77 | "stateMutability": "nonpayable", 78 | "type": "function" 79 | }, 80 | { 81 | "constant": true, 82 | "inputs": [], 83 | "name": "decimals", 84 | "outputs": [ 85 | { 86 | "name": "", 87 | "type": "uint8" 88 | } 89 | ], 90 | "payable": false, 91 | "stateMutability": "view", 92 | "type": "function" 93 | }, 94 | { 95 | "constant": true, 96 | "inputs": [ 97 | { 98 | "name": "_owner", 99 | "type": "address" 100 | } 101 | ], 102 | "name": "balanceOf", 103 | "outputs": [ 104 | { 105 | "name": "balance", 106 | "type": "uint256" 107 | } 108 | ], 109 | "payable": false, 110 | "stateMutability": "view", 111 | "type": "function" 112 | }, 113 | { 114 | "constant": true, 115 | "inputs": [], 116 | "name": "symbol", 117 | "outputs": [ 118 | { 119 | "name": "", 120 | "type": "string" 121 | } 122 | ], 123 | "payable": false, 124 | "stateMutability": "view", 125 | "type": "function" 126 | }, 127 | { 128 | "constant": false, 129 | "inputs": [ 130 | { 131 | "name": "_to", 132 | "type": "address" 133 | }, 134 | { 135 | "name": "_value", 136 | "type": "uint256" 137 | } 138 | ], 139 | "name": "transfer", 140 | "outputs": [ 141 | { 142 | "name": "", 143 | "type": "bool" 144 | } 145 | ], 146 | "payable": false, 147 | "stateMutability": "nonpayable", 148 | "type": "function" 149 | }, 150 | { 151 | "constant": true, 152 | "inputs": [ 153 | { 154 | "name": "_owner", 155 | "type": "address" 156 | }, 157 | { 158 | "name": "_spender", 159 | "type": "address" 160 | } 161 | ], 162 | "name": "allowance", 163 | "outputs": [ 164 | { 165 | "name": "", 166 | "type": "uint256" 167 | } 168 | ], 169 | "payable": false, 170 | "stateMutability": "view", 171 | "type": "function" 172 | }, 173 | { 174 | "payable": true, 175 | "stateMutability": "payable", 176 | "type": "fallback" 177 | }, 178 | { 179 | "anonymous": false, 180 | "inputs": [ 181 | { 182 | "indexed": true, 183 | "name": "owner", 184 | "type": "address" 185 | }, 186 | { 187 | "indexed": true, 188 | "name": "spender", 189 | "type": "address" 190 | }, 191 | { 192 | "indexed": false, 193 | "name": "value", 194 | "type": "uint256" 195 | } 196 | ], 197 | "name": "Approval", 198 | "type": "event" 199 | }, 200 | { 201 | "anonymous": false, 202 | "inputs": [ 203 | { 204 | "indexed": true, 205 | "name": "from", 206 | "type": "address" 207 | }, 208 | { 209 | "indexed": true, 210 | "name": "to", 211 | "type": "address" 212 | }, 213 | { 214 | "indexed": false, 215 | "name": "value", 216 | "type": "uint256" 217 | } 218 | ], 219 | "name": "Transfer", 220 | "type": "event" 221 | } 222 | ] -------------------------------------------------------------------------------- /src/abis/external/L1GasPriceOracle.ts: -------------------------------------------------------------------------------- 1 | export const L1_GAS_PRICE_ORACLE_ABI = [{"inputs":[{"internalType":"address","name":"_owner","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"l1BaseFee","type":"uint256"}],"name":"L1BaseFeeUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"overhead","type":"uint256"}],"name":"OverheadUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"_oldOwner","type":"address"},{"indexed":true,"internalType":"address","name":"_newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"scalar","type":"uint256"}],"name":"ScalarUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"_oldWhitelist","type":"address"},{"indexed":false,"internalType":"address","name":"_newWhitelist","type":"address"}],"name":"UpdateWhitelist","type":"event"},{"inputs":[{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"getL1Fee","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"getL1GasUsed","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"l1BaseFee","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"overhead","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"scalar","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_l1BaseFee","type":"uint256"}],"name":"setL1BaseFee","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_overhead","type":"uint256"}],"name":"setOverhead","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_scalar","type":"uint256"}],"name":"setScalar","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_newWhitelist","type":"address"}],"name":"updateWhitelist","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"whitelist","outputs":[{"internalType":"contract IWhitelist","name":"","type":"address"}],"stateMutability":"view","type":"function"}] as const; -------------------------------------------------------------------------------- /src/abis/impact.ts: -------------------------------------------------------------------------------- 1 | 2 | export const IMPACT_ABI = [ 3 | { 4 | "inputs": [ 5 | { 6 | "internalType": "address", 7 | "name": "dex", 8 | "type": "address" 9 | } 10 | ], 11 | "stateMutability": "nonpayable", 12 | "type": "constructor" 13 | }, 14 | { 15 | "inputs": [ 16 | { 17 | "internalType": "address", 18 | "name": "base", 19 | "type": "address" 20 | }, 21 | { 22 | "internalType": "address", 23 | "name": "quote", 24 | "type": "address" 25 | }, 26 | { 27 | "internalType": "uint256", 28 | "name": "poolIdx", 29 | "type": "uint256" 30 | }, 31 | { 32 | "internalType": "bool", 33 | "name": "isBuy", 34 | "type": "bool" 35 | }, 36 | { 37 | "internalType": "bool", 38 | "name": "inBaseQty", 39 | "type": "bool" 40 | }, 41 | { 42 | "internalType": "uint128", 43 | "name": "qty", 44 | "type": "uint128" 45 | }, 46 | { 47 | "internalType": "uint16", 48 | "name": "poolTip", 49 | "type": "uint16" 50 | }, 51 | { 52 | "internalType": "uint128", 53 | "name": "limitPrice", 54 | "type": "uint128" 55 | } 56 | ], 57 | "name": "calcImpact", 58 | "outputs": [ 59 | { 60 | "internalType": "int128", 61 | "name": "baseFlow", 62 | "type": "int128" 63 | }, 64 | { 65 | "internalType": "int128", 66 | "name": "quoteFlow", 67 | "type": "int128" 68 | }, 69 | { 70 | "internalType": "uint128", 71 | "name": "finalPrice", 72 | "type": "uint128" 73 | } 74 | ], 75 | "stateMutability": "view", 76 | "type": "function" 77 | }, 78 | { 79 | "inputs": [], 80 | "name": "dex_", 81 | "outputs": [ 82 | { 83 | "internalType": "address", 84 | "name": "", 85 | "type": "address" 86 | } 87 | ], 88 | "stateMutability": "view", 89 | "type": "function" 90 | } 91 | ] 92 | -------------------------------------------------------------------------------- /src/abis/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./query"; 2 | export * from "./impact"; 3 | export * from "./erc20"; 4 | export * from "./croc"; 5 | -------------------------------------------------------------------------------- /src/abis/query.ts: -------------------------------------------------------------------------------- 1 | export const QUERY_ABI = [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "address", 6 | "name": "dex", 7 | "type": "address" 8 | } 9 | ], 10 | "stateMutability": "nonpayable", 11 | "type": "constructor" 12 | }, 13 | { 14 | "inputs": [], 15 | "name": "dex_", 16 | "outputs": [ 17 | { 18 | "internalType": "address", 19 | "name": "", 20 | "type": "address" 21 | } 22 | ], 23 | "stateMutability": "view", 24 | "type": "function" 25 | }, 26 | { 27 | "inputs": [ 28 | { 29 | "internalType": "address", 30 | "name": "owner", 31 | "type": "address" 32 | }, 33 | { 34 | "internalType": "address", 35 | "name": "base", 36 | "type": "address" 37 | }, 38 | { 39 | "internalType": "address", 40 | "name": "quote", 41 | "type": "address" 42 | }, 43 | { 44 | "internalType": "uint256", 45 | "name": "poolIdx", 46 | "type": "uint256" 47 | } 48 | ], 49 | "name": "queryAmbientPosition", 50 | "outputs": [ 51 | { 52 | "internalType": "uint128", 53 | "name": "seeds", 54 | "type": "uint128" 55 | }, 56 | { 57 | "internalType": "uint32", 58 | "name": "timestamp", 59 | "type": "uint32" 60 | } 61 | ], 62 | "stateMutability": "view", 63 | "type": "function" 64 | }, 65 | { 66 | "inputs": [ 67 | { 68 | "internalType": "address", 69 | "name": "owner", 70 | "type": "address" 71 | }, 72 | { 73 | "internalType": "address", 74 | "name": "base", 75 | "type": "address" 76 | }, 77 | { 78 | "internalType": "address", 79 | "name": "quote", 80 | "type": "address" 81 | }, 82 | { 83 | "internalType": "uint256", 84 | "name": "poolIdx", 85 | "type": "uint256" 86 | } 87 | ], 88 | "name": "queryAmbientTokens", 89 | "outputs": [ 90 | { 91 | "internalType": "uint128", 92 | "name": "liq", 93 | "type": "uint128" 94 | }, 95 | { 96 | "internalType": "uint128", 97 | "name": "baseQty", 98 | "type": "uint128" 99 | }, 100 | { 101 | "internalType": "uint128", 102 | "name": "quoteQty", 103 | "type": "uint128" 104 | } 105 | ], 106 | "stateMutability": "view", 107 | "type": "function" 108 | }, 109 | { 110 | "inputs": [ 111 | { 112 | "internalType": "address", 113 | "name": "owner", 114 | "type": "address" 115 | }, 116 | { 117 | "internalType": "address", 118 | "name": "base", 119 | "type": "address" 120 | }, 121 | { 122 | "internalType": "address", 123 | "name": "quote", 124 | "type": "address" 125 | }, 126 | { 127 | "internalType": "uint256", 128 | "name": "poolIdx", 129 | "type": "uint256" 130 | }, 131 | { 132 | "internalType": "int24", 133 | "name": "lowerTick", 134 | "type": "int24" 135 | }, 136 | { 137 | "internalType": "int24", 138 | "name": "upperTick", 139 | "type": "int24" 140 | } 141 | ], 142 | "name": "queryConcRewards", 143 | "outputs": [ 144 | { 145 | "internalType": "uint128", 146 | "name": "liqRewards", 147 | "type": "uint128" 148 | }, 149 | { 150 | "internalType": "uint128", 151 | "name": "baseRewards", 152 | "type": "uint128" 153 | }, 154 | { 155 | "internalType": "uint128", 156 | "name": "quoteRewards", 157 | "type": "uint128" 158 | } 159 | ], 160 | "stateMutability": "view", 161 | "type": "function" 162 | }, 163 | { 164 | "inputs": [ 165 | { 166 | "internalType": "address", 167 | "name": "base", 168 | "type": "address" 169 | }, 170 | { 171 | "internalType": "address", 172 | "name": "quote", 173 | "type": "address" 174 | }, 175 | { 176 | "internalType": "uint256", 177 | "name": "poolIdx", 178 | "type": "uint256" 179 | } 180 | ], 181 | "name": "queryCurve", 182 | "outputs": [ 183 | { 184 | "components": [ 185 | { 186 | "internalType": "uint128", 187 | "name": "priceRoot_", 188 | "type": "uint128" 189 | }, 190 | { 191 | "internalType": "uint128", 192 | "name": "ambientSeeds_", 193 | "type": "uint128" 194 | }, 195 | { 196 | "internalType": "uint128", 197 | "name": "concLiq_", 198 | "type": "uint128" 199 | }, 200 | { 201 | "internalType": "uint64", 202 | "name": "seedDeflator_", 203 | "type": "uint64" 204 | }, 205 | { 206 | "internalType": "uint64", 207 | "name": "concGrowth_", 208 | "type": "uint64" 209 | } 210 | ], 211 | "internalType": "struct CurveMath.CurveState", 212 | "name": "curve", 213 | "type": "tuple" 214 | } 215 | ], 216 | "stateMutability": "view", 217 | "type": "function" 218 | }, 219 | { 220 | "inputs": [ 221 | { 222 | "internalType": "address", 223 | "name": "base", 224 | "type": "address" 225 | }, 226 | { 227 | "internalType": "address", 228 | "name": "quote", 229 | "type": "address" 230 | }, 231 | { 232 | "internalType": "uint256", 233 | "name": "poolIdx", 234 | "type": "uint256" 235 | } 236 | ], 237 | "name": "queryCurveTick", 238 | "outputs": [ 239 | { 240 | "internalType": "int24", 241 | "name": "", 242 | "type": "int24" 243 | } 244 | ], 245 | "stateMutability": "view", 246 | "type": "function" 247 | }, 248 | { 249 | "inputs": [ 250 | { 251 | "internalType": "address", 252 | "name": "base", 253 | "type": "address" 254 | }, 255 | { 256 | "internalType": "address", 257 | "name": "quote", 258 | "type": "address" 259 | }, 260 | { 261 | "internalType": "uint256", 262 | "name": "poolIdx", 263 | "type": "uint256" 264 | }, 265 | { 266 | "internalType": "bool", 267 | "name": "isBid", 268 | "type": "bool" 269 | }, 270 | { 271 | "internalType": "int24", 272 | "name": "tick", 273 | "type": "int24" 274 | } 275 | ], 276 | "name": "queryKnockoutMerkle", 277 | "outputs": [ 278 | { 279 | "internalType": "uint160", 280 | "name": "root", 281 | "type": "uint160" 282 | }, 283 | { 284 | "internalType": "uint32", 285 | "name": "pivot", 286 | "type": "uint32" 287 | }, 288 | { 289 | "internalType": "uint64", 290 | "name": "fee", 291 | "type": "uint64" 292 | } 293 | ], 294 | "stateMutability": "view", 295 | "type": "function" 296 | }, 297 | { 298 | "inputs": [ 299 | { 300 | "internalType": "address", 301 | "name": "base", 302 | "type": "address" 303 | }, 304 | { 305 | "internalType": "address", 306 | "name": "quote", 307 | "type": "address" 308 | }, 309 | { 310 | "internalType": "uint256", 311 | "name": "poolIdx", 312 | "type": "uint256" 313 | }, 314 | { 315 | "internalType": "bool", 316 | "name": "isBid", 317 | "type": "bool" 318 | }, 319 | { 320 | "internalType": "int24", 321 | "name": "tick", 322 | "type": "int24" 323 | } 324 | ], 325 | "name": "queryKnockoutPivot", 326 | "outputs": [ 327 | { 328 | "internalType": "uint96", 329 | "name": "lots", 330 | "type": "uint96" 331 | }, 332 | { 333 | "internalType": "uint32", 334 | "name": "pivot", 335 | "type": "uint32" 336 | }, 337 | { 338 | "internalType": "uint16", 339 | "name": "range", 340 | "type": "uint16" 341 | } 342 | ], 343 | "stateMutability": "view", 344 | "type": "function" 345 | }, 346 | { 347 | "inputs": [ 348 | { 349 | "internalType": "address", 350 | "name": "owner", 351 | "type": "address" 352 | }, 353 | { 354 | "internalType": "address", 355 | "name": "base", 356 | "type": "address" 357 | }, 358 | { 359 | "internalType": "address", 360 | "name": "quote", 361 | "type": "address" 362 | }, 363 | { 364 | "internalType": "uint256", 365 | "name": "poolIdx", 366 | "type": "uint256" 367 | }, 368 | { 369 | "internalType": "uint32", 370 | "name": "pivot", 371 | "type": "uint32" 372 | }, 373 | { 374 | "internalType": "bool", 375 | "name": "isBid", 376 | "type": "bool" 377 | }, 378 | { 379 | "internalType": "int24", 380 | "name": "lowerTick", 381 | "type": "int24" 382 | }, 383 | { 384 | "internalType": "int24", 385 | "name": "upperTick", 386 | "type": "int24" 387 | } 388 | ], 389 | "name": "queryKnockoutPos", 390 | "outputs": [ 391 | { 392 | "internalType": "uint96", 393 | "name": "lots", 394 | "type": "uint96" 395 | }, 396 | { 397 | "internalType": "uint64", 398 | "name": "mileage", 399 | "type": "uint64" 400 | }, 401 | { 402 | "internalType": "uint32", 403 | "name": "timestamp", 404 | "type": "uint32" 405 | } 406 | ], 407 | "stateMutability": "view", 408 | "type": "function" 409 | }, 410 | { 411 | "inputs": [ 412 | { 413 | "internalType": "address", 414 | "name": "owner", 415 | "type": "address" 416 | }, 417 | { 418 | "internalType": "address", 419 | "name": "base", 420 | "type": "address" 421 | }, 422 | { 423 | "internalType": "address", 424 | "name": "quote", 425 | "type": "address" 426 | }, 427 | { 428 | "internalType": "uint256", 429 | "name": "poolIdx", 430 | "type": "uint256" 431 | }, 432 | { 433 | "internalType": "uint32", 434 | "name": "pivot", 435 | "type": "uint32" 436 | }, 437 | { 438 | "internalType": "bool", 439 | "name": "isBid", 440 | "type": "bool" 441 | }, 442 | { 443 | "internalType": "int24", 444 | "name": "lowerTick", 445 | "type": "int24" 446 | }, 447 | { 448 | "internalType": "int24", 449 | "name": "upperTick", 450 | "type": "int24" 451 | } 452 | ], 453 | "name": "queryKnockoutTokens", 454 | "outputs": [ 455 | { 456 | "internalType": "uint128", 457 | "name": "liq", 458 | "type": "uint128" 459 | }, 460 | { 461 | "internalType": "uint128", 462 | "name": "baseQty", 463 | "type": "uint128" 464 | }, 465 | { 466 | "internalType": "uint128", 467 | "name": "quoteQty", 468 | "type": "uint128" 469 | }, 470 | { 471 | "internalType": "bool", 472 | "name": "knockedOut", 473 | "type": "bool" 474 | } 475 | ], 476 | "stateMutability": "view", 477 | "type": "function" 478 | }, 479 | { 480 | "inputs": [ 481 | { 482 | "internalType": "address", 483 | "name": "base", 484 | "type": "address" 485 | }, 486 | { 487 | "internalType": "address", 488 | "name": "quote", 489 | "type": "address" 490 | }, 491 | { 492 | "internalType": "uint256", 493 | "name": "poolIdx", 494 | "type": "uint256" 495 | }, 496 | { 497 | "internalType": "int24", 498 | "name": "tick", 499 | "type": "int24" 500 | } 501 | ], 502 | "name": "queryLevel", 503 | "outputs": [ 504 | { 505 | "internalType": "uint96", 506 | "name": "bidLots", 507 | "type": "uint96" 508 | }, 509 | { 510 | "internalType": "uint96", 511 | "name": "askLots", 512 | "type": "uint96" 513 | }, 514 | { 515 | "internalType": "uint64", 516 | "name": "odometer", 517 | "type": "uint64" 518 | } 519 | ], 520 | "stateMutability": "view", 521 | "type": "function" 522 | }, 523 | { 524 | "inputs": [ 525 | { 526 | "internalType": "address", 527 | "name": "base", 528 | "type": "address" 529 | }, 530 | { 531 | "internalType": "address", 532 | "name": "quote", 533 | "type": "address" 534 | }, 535 | { 536 | "internalType": "uint256", 537 | "name": "poolIdx", 538 | "type": "uint256" 539 | } 540 | ], 541 | "name": "queryLiquidity", 542 | "outputs": [ 543 | { 544 | "internalType": "uint128", 545 | "name": "", 546 | "type": "uint128" 547 | } 548 | ], 549 | "stateMutability": "view", 550 | "type": "function" 551 | }, 552 | { 553 | "inputs": [ 554 | { 555 | "internalType": "address", 556 | "name": "base", 557 | "type": "address" 558 | }, 559 | { 560 | "internalType": "address", 561 | "name": "quote", 562 | "type": "address" 563 | }, 564 | { 565 | "internalType": "uint256", 566 | "name": "poolIdx", 567 | "type": "uint256" 568 | } 569 | ], 570 | "name": "queryPrice", 571 | "outputs": [ 572 | { 573 | "internalType": "uint128", 574 | "name": "", 575 | "type": "uint128" 576 | } 577 | ], 578 | "stateMutability": "view", 579 | "type": "function" 580 | }, 581 | { 582 | "inputs": [ 583 | { 584 | "internalType": "address", 585 | "name": "token", 586 | "type": "address" 587 | } 588 | ], 589 | "name": "queryProtocolAccum", 590 | "outputs": [ 591 | { 592 | "internalType": "uint128", 593 | "name": "", 594 | "type": "uint128" 595 | } 596 | ], 597 | "stateMutability": "view", 598 | "type": "function" 599 | }, 600 | { 601 | "inputs": [ 602 | { 603 | "internalType": "address", 604 | "name": "owner", 605 | "type": "address" 606 | }, 607 | { 608 | "internalType": "address", 609 | "name": "base", 610 | "type": "address" 611 | }, 612 | { 613 | "internalType": "address", 614 | "name": "quote", 615 | "type": "address" 616 | }, 617 | { 618 | "internalType": "uint256", 619 | "name": "poolIdx", 620 | "type": "uint256" 621 | }, 622 | { 623 | "internalType": "int24", 624 | "name": "lowerTick", 625 | "type": "int24" 626 | }, 627 | { 628 | "internalType": "int24", 629 | "name": "upperTick", 630 | "type": "int24" 631 | } 632 | ], 633 | "name": "queryRangePosition", 634 | "outputs": [ 635 | { 636 | "internalType": "uint128", 637 | "name": "liq", 638 | "type": "uint128" 639 | }, 640 | { 641 | "internalType": "uint64", 642 | "name": "fee", 643 | "type": "uint64" 644 | }, 645 | { 646 | "internalType": "uint32", 647 | "name": "timestamp", 648 | "type": "uint32" 649 | }, 650 | { 651 | "internalType": "bool", 652 | "name": "atomic", 653 | "type": "bool" 654 | } 655 | ], 656 | "stateMutability": "view", 657 | "type": "function" 658 | }, 659 | { 660 | "inputs": [ 661 | { 662 | "internalType": "address", 663 | "name": "owner", 664 | "type": "address" 665 | }, 666 | { 667 | "internalType": "address", 668 | "name": "base", 669 | "type": "address" 670 | }, 671 | { 672 | "internalType": "address", 673 | "name": "quote", 674 | "type": "address" 675 | }, 676 | { 677 | "internalType": "uint256", 678 | "name": "poolIdx", 679 | "type": "uint256" 680 | }, 681 | { 682 | "internalType": "int24", 683 | "name": "lowerTick", 684 | "type": "int24" 685 | }, 686 | { 687 | "internalType": "int24", 688 | "name": "upperTick", 689 | "type": "int24" 690 | } 691 | ], 692 | "name": "queryRangeTokens", 693 | "outputs": [ 694 | { 695 | "internalType": "uint128", 696 | "name": "liq", 697 | "type": "uint128" 698 | }, 699 | { 700 | "internalType": "uint128", 701 | "name": "baseQty", 702 | "type": "uint128" 703 | }, 704 | { 705 | "internalType": "uint128", 706 | "name": "quoteQty", 707 | "type": "uint128" 708 | } 709 | ], 710 | "stateMutability": "view", 711 | "type": "function" 712 | }, 713 | { 714 | "inputs": [ 715 | { 716 | "internalType": "address", 717 | "name": "owner", 718 | "type": "address" 719 | }, 720 | { 721 | "internalType": "address", 722 | "name": "token", 723 | "type": "address" 724 | } 725 | ], 726 | "name": "querySurplus", 727 | "outputs": [ 728 | { 729 | "internalType": "uint128", 730 | "name": "surplus", 731 | "type": "uint128" 732 | } 733 | ], 734 | "stateMutability": "view", 735 | "type": "function" 736 | }, 737 | { 738 | "inputs": [ 739 | { 740 | "internalType": "address", 741 | "name": "owner", 742 | "type": "address" 743 | }, 744 | { 745 | "internalType": "address", 746 | "name": "tracker", 747 | "type": "address" 748 | }, 749 | { 750 | "internalType": "uint256", 751 | "name": "salt", 752 | "type": "uint256" 753 | } 754 | ], 755 | "name": "queryVirtual", 756 | "outputs": [ 757 | { 758 | "internalType": "uint128", 759 | "name": "surplus", 760 | "type": "uint128" 761 | } 762 | ], 763 | "stateMutability": "view", 764 | "type": "function" 765 | } 766 | ] 767 | 768 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | type ChainAddress = string; 2 | type ChainId = string; 3 | 4 | export const MIN_TICK = -665454; 5 | export const MAX_TICK = 831818; 6 | export const MAX_SQRT_PRICE: bigint = BigInt( 7 | "21267430153580247136652501917186561138") - BigInt(1); 8 | export const MIN_SQRT_PRICE: bigint = BigInt("65538") - BigInt(1); 9 | export const MAX_LIQ = BigInt(2) ** BigInt(128) - BigInt(1); 10 | 11 | 12 | export interface ChainSpec { 13 | nodeUrl: string; 14 | poolIndex: number; 15 | addrs: { 16 | dex: ChainAddress; 17 | query: ChainAddress; 18 | impact: ChainAddress; 19 | router?: ChainAddress 20 | routerBypass?: ChainAddress 21 | } 22 | isTestNet: boolean; 23 | chainId: ChainId; 24 | gridSize: number; 25 | proxyPaths: { 26 | cold: number, 27 | liq: number, 28 | long: number, 29 | dfltColdSwap?: boolean 30 | } 31 | blockExplorer?: string; 32 | displayName: string; 33 | } 34 | 35 | const MAINNET_CHAIN: ChainSpec = { 36 | nodeUrl: "https://ethereum-rpc.publicnode.com" , 37 | addrs: { 38 | dex: "0xAaAaAAAaA24eEeb8d57D431224f73832bC34f688", 39 | query: "0xc2e1f740E11294C64adE66f69a1271C5B32004c8", 40 | impact: "0x3e3EDd3eD7621891E574E5d7f47b1f30A994c0D0", 41 | router: "0x533E164ded63f4c55E83E1f409BDf2BaC5278035", 42 | routerBypass: "0xa3e58B0cB05447398358B6C59E4B2465342EFEd2" 43 | }, 44 | poolIndex: 420, 45 | isTestNet: false, 46 | chainId: "0x1", 47 | gridSize: 16, 48 | proxyPaths: { 49 | cold: 3, 50 | long: 4, 51 | liq: 2, 52 | dfltColdSwap: true 53 | }, 54 | blockExplorer: "https://etherscan.io/", 55 | displayName: "Ethereum", 56 | }; 57 | 58 | const SEPOLIA_CHAIN: ChainSpec = { 59 | nodeUrl: "https://ethereum-sepolia-rpc.publicnode.com", 60 | addrs: { 61 | dex: "0xFb8A46E7963E6397DBB4B2E1c0B3f0464fb5BDFF", 62 | query: "0xDB182F4687708D0F5798c77b4d02ad3425f4B672", 63 | impact: "0x80aEB76D091ecbEd3c609c0B794fC1A09B9cB8F4", 64 | router: "0x168dB7Ad649D9f7918028F709C5e2F245af284A4", 65 | routerBypass: "0xBC3d1Bb2d8A59eb25DA1E527bF0cA62B44346EE1" 66 | }, 67 | poolIndex: 36000, 68 | isTestNet: true, 69 | chainId: "0xaa36a7", 70 | gridSize: 16, 71 | proxyPaths: { 72 | cold: 3, 73 | long: 4, 74 | liq: 2 75 | }, 76 | blockExplorer: "https://sepolia.etherscan.io/", 77 | displayName: "Sepolia", 78 | }; 79 | 80 | const BLAST_CHAIN: ChainSpec = { 81 | nodeUrl: "https://rpc.ankr.com/blast", 82 | addrs: { 83 | dex: "0xaAaaaAAAFfe404EE9433EEf0094b6382D81fb958", 84 | query: "0xA3BD3bE19012De72190c885FB270beb93e36a8A7", 85 | impact: "0x6A699AB45ADce02891E6115b81Dfb46CAa5efDb9", 86 | router: "0xaab17419F062bB28CdBE82f9FC05E7C47C3F6194", 87 | routerBypass: "0xd83eF4d0e968A96329aC297bBf049CDdaC7E0362" 88 | }, 89 | poolIndex: 420, 90 | isTestNet: false, 91 | chainId: "0x13e31", 92 | gridSize: 4, 93 | proxyPaths: { 94 | cold: 3, 95 | long: 130, 96 | liq: 128, 97 | dfltColdSwap: true 98 | }, 99 | blockExplorer: "https://blastscan.io/", 100 | displayName: "Blast", 101 | }; 102 | 103 | const ZIRCUIT_CHAIN: ChainSpec = { 104 | nodeUrl: "https://rpc.ankr.com/blast", 105 | addrs: { 106 | dex: "0xaAaaaAAAFfe404EE9433EEf0094b6382D81fb958", 107 | query: "0xA3BD3bE19012De72190c885FB270beb93e36a8A7", 108 | impact: "0x6A699AB45ADce02891E6115b81Dfb46CAa5efDb9", 109 | router: "0xaab17419F062bB28CdBE82f9FC05E7C47C3F6194", 110 | routerBypass: "0xd83eF4d0e968A96329aC297bBf049CDdaC7E0362" 111 | }, 112 | poolIndex: 420, 113 | isTestNet: false, 114 | chainId: "0x13e31", 115 | gridSize: 4, 116 | proxyPaths: { 117 | cold: 3, 118 | long: 130, 119 | liq: 128, 120 | dfltColdSwap: true 121 | }, 122 | blockExplorer: "https://blastscan.io/", 123 | displayName: "Zircuit", 124 | }; 125 | 126 | const BLAST_SEPOLIA_CHAIN: ChainSpec = { 127 | nodeUrl: "https://sepolia.blast.io", 128 | addrs: { 129 | dex: "0xf65976C7f25b6320c7CD81b1db10cEe97F2bb7AC", 130 | query: "0x7757BAEC9c492691eAE235c6f01FB99AaA622975", 131 | impact: "0x5D42d6046927DEE12b9b4a235be0ceCd55D0E0fb", 132 | router: "0xdCB3b5ec9170beF68E9fff21F0EDD622F72f1899", 133 | routerBypass: "0x3A6E9cff691a473D4D0742E1dFc8Ea263a99F6d0" 134 | }, 135 | poolIndex: 36000, 136 | isTestNet: true, 137 | chainId: "0xa0c71fd", 138 | gridSize: 1, 139 | proxyPaths: { 140 | cold: 3, 141 | long: 130, 142 | liq: 128, 143 | dfltColdSwap: true 144 | }, 145 | blockExplorer: "https://testnet.blastscan.io/", 146 | displayName: "Blast Sepolia", 147 | }; 148 | 149 | const SCROLL_SEPOLIA_CHAIN: ChainSpec = { 150 | nodeUrl: "https://sepolia-rpc.scroll.io", 151 | addrs: { 152 | dex: "0xaaAAAaa6612bd88cD409cb0D70C99556C87A0E8c", 153 | query: "0x43eC1302FE3587862e15B2D52AD9653575FD79e9", 154 | impact: "0x9B28970D51A231741416D8D3e5281d9c51a50892", 155 | router: "0x323172539B1B0D9eDDFFBd0318C4d6Ab45292843", 156 | routerBypass: "0xb2aE163293C82DCF36b0cE704591eDC2f9E2608D" 157 | }, 158 | poolIndex: 36000, 159 | isTestNet: true, 160 | chainId: "0x8274f", 161 | gridSize: 1, 162 | proxyPaths: { 163 | cold: 3, 164 | long: 130, 165 | liq: 128 166 | }, 167 | blockExplorer: "https://sepolia.scrollscan.dev/", 168 | displayName: "Scroll Sepolia", 169 | }; 170 | 171 | const SWELL_SEPOLIA_CHAIN: ChainSpec = { 172 | nodeUrl: "https://swell-testnet.alt.technology", 173 | addrs: { 174 | dex: "0x4c722A53Cf9EB5373c655E1dD2dA95AcC10152D1", 175 | query: "0x1C74Dd2DF010657510715244DA10ba19D1F3D2B7", 176 | impact: "0x70a6a0C905af5737aD73Ceba4e6158e995031d4B", 177 | }, 178 | poolIndex: 36000, 179 | isTestNet: true, 180 | chainId: "0x784", 181 | gridSize: 1, 182 | proxyPaths: { 183 | cold: 3, 184 | long: 130, 185 | liq: 128 186 | }, 187 | blockExplorer: "https://swell-testnet-explorer.alt.technology/", 188 | displayName: "Swell Sepolia", 189 | }; 190 | 191 | const SWELL_CHAIN: ChainSpec = { 192 | nodeUrl: "https://swell-mainnet.alt.technology", 193 | addrs: { 194 | dex: "0xaAAaAaaa82812F0a1f274016514ba2cA933bF24D", 195 | query: "0xaab17419F062bB28CdBE82f9FC05E7C47C3F6194", 196 | impact: "0xd83eF4d0e968A96329aC297bBf049CDdaC7E0362", 197 | }, 198 | poolIndex: 420, 199 | isTestNet: false, 200 | chainId: "0x783", 201 | gridSize: 4, 202 | proxyPaths: { 203 | cold: 3, 204 | long: 130, 205 | liq: 128 206 | }, 207 | blockExplorer: "https://swellchainscan.io/", 208 | displayName: "Swell", 209 | }; 210 | 211 | const PLUME_LEGACY_CHAIN: ChainSpec = { 212 | nodeUrl: "https://rpc.plumenetwork.xyz", 213 | addrs: { 214 | dex: "0xAaAaAAAA81a99d2a05eE428eC7a1d8A3C2237D85", 215 | query: "0xA3BD3bE19012De72190c885FB270beb93e36a8A7", 216 | impact: "0x6A699AB45ADce02891E6115b81Dfb46CAa5efDb9", 217 | }, 218 | poolIndex: 420, 219 | isTestNet: false, 220 | chainId: "0x18231", 221 | gridSize: 4, 222 | proxyPaths: { 223 | cold: 3, 224 | long: 130, 225 | liq: 128 226 | }, 227 | blockExplorer: "https://explorer.plumenetwork.xyz/", 228 | displayName: "Plume", 229 | }; 230 | 231 | const PLUME_CHAIN: ChainSpec = { 232 | nodeUrl: "https://rpc.plume.org", 233 | addrs: { 234 | dex: "0xAaAaAAAA81a99d2a05eE428eC7a1d8A3C2237D85", 235 | query: "0x62223e90605845Cf5CC6DAE6E0de4CDA130d6DDf", 236 | impact: "0xc2c301759B5e0C385a38e678014868A33E2F3ae3", 237 | }, 238 | poolIndex: 420, 239 | isTestNet: false, 240 | chainId: "0x18232", 241 | gridSize: 4, 242 | proxyPaths: { 243 | cold: 3, 244 | long: 130, 245 | liq: 128 246 | }, 247 | blockExplorer: "https://explorer.plume.org/", 248 | displayName: "Plume", 249 | }; 250 | 251 | const BASE_SEPOLIA_CHAIN: ChainSpec = { 252 | nodeUrl: "https://sepolia.base.org", 253 | addrs: { 254 | dex: "0xD553d97EfD5faAB29Dc92CC87d5259ff59278176", 255 | query: "0x70a6a0C905af5737aD73Ceba4e6158e995031d4B", 256 | impact: "0x3108E20b0Da8b267DaA13f538964940C6eBaCCB2", 257 | }, 258 | poolIndex: 36000, 259 | isTestNet: true, 260 | chainId: "0x14a34", 261 | gridSize: 1, 262 | proxyPaths: { 263 | cold: 3, 264 | long: 130, 265 | liq: 128 266 | }, 267 | blockExplorer: "https://sepolia.basescan.org/", 268 | displayName: "Base Sepolia", 269 | }; 270 | 271 | 272 | const SCROLL_CHAIN: ChainSpec = { 273 | nodeUrl: "https://rpc.scroll.io", 274 | addrs: { 275 | dex: "0xaaaaAAAACB71BF2C8CaE522EA5fa455571A74106", 276 | query: "0x62223e90605845Cf5CC6DAE6E0de4CDA130d6DDf", 277 | impact: "0xc2c301759B5e0C385a38e678014868A33E2F3ae3", 278 | router: "0xfB5f26851E03449A0403Ca945eBB4201415fd1fc", 279 | routerBypass: "0xED5535C6237f72BD9b4fDEAa3b6D8d9998b4C4e4", 280 | }, 281 | poolIndex: 420, 282 | isTestNet: false, 283 | chainId: "0x82750", 284 | gridSize: 4, 285 | proxyPaths: { 286 | cold: 3, 287 | long: 130, 288 | liq: 128, 289 | dfltColdSwap: true 290 | }, 291 | blockExplorer: "https://scrollscan.com/", 292 | displayName: "Scroll", 293 | }; 294 | 295 | const MONAD_TESTNET_CHAIN: ChainSpec = { 296 | nodeUrl: "https://testnet-rpc.monad.xyz/", 297 | addrs: { 298 | dex: "0x88B96aF200c8a9c35442C8AC6cd3D22695AaE4F0", 299 | query: "0x1C74Dd2DF010657510715244DA10ba19D1F3D2B7", 300 | impact: "0x70a6a0C905af5737aD73Ceba4e6158e995031d4B", 301 | router: "0x3108E20b0Da8b267DaA13f538964940C6eBaCCB2", 302 | routerBypass: "0x8415bFC3b1ff76B804Ab8a6810a1810f9df32483", 303 | }, 304 | poolIndex: 36000, 305 | isTestNet: true, 306 | chainId: "0x279f", 307 | gridSize: 1, 308 | proxyPaths: { 309 | cold: 3, 310 | long: 130, 311 | liq: 128, 312 | dfltColdSwap: true 313 | }, 314 | blockExplorer: "https://testnet.monadexplorer.com/", 315 | displayName: "Monad Testnet", 316 | }; 317 | 318 | export const CHAIN_SPECS: { [chainId: string]: ChainSpec } = { 319 | "0x1": MAINNET_CHAIN, 320 | "0xaa36a7": SEPOLIA_CHAIN, 321 | "0x8274f": SCROLL_SEPOLIA_CHAIN, 322 | "0x82750": SCROLL_CHAIN, 323 | "0xa0c71fd": BLAST_SEPOLIA_CHAIN, 324 | "0x13e31": BLAST_CHAIN, 325 | "0x784": SWELL_SEPOLIA_CHAIN, 326 | "0x783": SWELL_CHAIN, 327 | "0x14a34": BASE_SEPOLIA_CHAIN, 328 | "0x18231": PLUME_LEGACY_CHAIN, 329 | "0x18232": PLUME_CHAIN, 330 | "0x279f": MONAD_TESTNET_CHAIN, 331 | "sepolia": SEPOLIA_CHAIN, 332 | "ethereum": MAINNET_CHAIN, 333 | "mainnet": MAINNET_CHAIN, 334 | "scrolltest": SCROLL_SEPOLIA_CHAIN, 335 | "scroll": SCROLL_CHAIN, 336 | "scrollsepolia": SCROLL_SEPOLIA_CHAIN, 337 | "blast": BLAST_CHAIN, 338 | "blastSepolia": BLAST_SEPOLIA_CHAIN, 339 | "swellSepolia": SWELL_SEPOLIA_CHAIN, 340 | "baseSepolia": BASE_SEPOLIA_CHAIN, 341 | "swell": SWELL_CHAIN, 342 | "plume": PLUME_CHAIN, 343 | "plumeLegacy": PLUME_LEGACY_CHAIN, 344 | "zircuit": ZIRCUIT_CHAIN, 345 | "monadTestnet": MONAD_TESTNET_CHAIN 346 | }; 347 | -------------------------------------------------------------------------------- /src/constants/mainnetTokens.ts: -------------------------------------------------------------------------------- 1 | import { ZeroAddress } from "ethers"; 2 | 3 | export const ETH = ZeroAddress 4 | 5 | export const USDC = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" 6 | export const DAI = "0x6b175474e89094c44da98b954eedeac495271d0f" 7 | -------------------------------------------------------------------------------- /src/context.ts: -------------------------------------------------------------------------------- 1 | import { Contract, ethers, JsonRpcProvider, Provider, Signer, ZeroAddress } from "ethers"; 2 | import { CROC_ABI, ERC20_ABI, QUERY_ABI } from "./abis"; 3 | import { ERC20_READ_ABI } from "./abis/erc20.read"; 4 | import { IMPACT_ABI } from "./abis/impact"; 5 | import { CHAIN_SPECS, ChainSpec } from "./constants"; 6 | 7 | export interface CrocContext { 8 | provider: Provider; 9 | actor: Provider | Signer; 10 | dex: Contract; 11 | router?: Contract; 12 | routerBypass?: Contract; 13 | query: Contract; 14 | slipQuery: Contract; 15 | erc20Read: Contract; 16 | erc20Write: Contract; 17 | chain: ChainSpec; 18 | senderAddr?: string 19 | } 20 | 21 | export type ChainIdentifier = number | string; 22 | export type ConnectArg = Provider | Signer | ChainIdentifier; 23 | 24 | export async function connectCroc( 25 | providerOrChainId: ConnectArg, 26 | signer?: Signer 27 | ): Promise { 28 | const [provider, maybeSigner] = await buildProvider(providerOrChainId, signer); 29 | return setupProvider(provider, maybeSigner); 30 | } 31 | 32 | async function buildProvider( 33 | arg: ConnectArg, 34 | signer?: Signer 35 | ): Promise<[Provider, Signer | undefined]> { 36 | if (typeof arg === "number" || typeof arg == "string") { 37 | const context = lookupChain(arg); 38 | return buildProvider(new JsonRpcProvider(context.nodeUrl), signer); 39 | } else if ("getNetwork" in arg) { 40 | return [arg, signer]; 41 | } else { 42 | const chainId = Number((await arg.provider?.getNetwork())?.chainId); 43 | return buildProvider(chainId, signer); 44 | } 45 | } 46 | 47 | async function setupProvider( 48 | provider: Provider, 49 | signer?: Signer 50 | ): Promise { 51 | const actor = await determineActor(provider, signer); 52 | const chainId = await getChain(provider); 53 | let cntx = inflateContracts(chainId, provider, actor); 54 | return await attachSenderAddr(cntx, actor) 55 | } 56 | 57 | async function attachSenderAddr (cntx: CrocContext, 58 | actor: Provider | Signer): Promise { 59 | if ('getAddress' in actor) { 60 | try { 61 | cntx.senderAddr = await actor.getAddress() 62 | } catch (e) { 63 | console.warn("Failed to get signer address:", e) 64 | } 65 | } 66 | return cntx 67 | } 68 | 69 | async function determineActor( 70 | provider: Provider, 71 | signer?: Signer 72 | ): Promise { 73 | if (signer) { 74 | try { 75 | return signer.connect(provider) 76 | } catch { 77 | return signer 78 | } 79 | } else if ("getSigner" in provider) { 80 | try { 81 | let signer = await ((provider as ethers.JsonRpcProvider).getSigner()); 82 | return signer 83 | } catch { 84 | return provider 85 | } 86 | } else { 87 | return provider; 88 | } 89 | } 90 | 91 | async function getChain(provider: Provider): Promise { 92 | if ("chainId" in provider) { 93 | return (provider as any).chainId as number; 94 | } else if ("getNetwork" in provider) { 95 | return provider.getNetwork().then((n) => Number(n.chainId)); 96 | } else { 97 | throw new Error("Invalid provider"); 98 | } 99 | } 100 | 101 | function inflateContracts( 102 | chainId: number, 103 | provider: Provider, 104 | actor: Provider | Signer, 105 | addr?: string 106 | ): CrocContext { 107 | const context = lookupChain(chainId); 108 | return { 109 | provider: provider, 110 | actor: actor, 111 | dex: new Contract(context.addrs.dex, CROC_ABI, actor), 112 | router: context.addrs.router ? new Contract(context.addrs.router || ZeroAddress, CROC_ABI, actor) : undefined, 113 | routerBypass: context.addrs.routerBypass ? new Contract(context.addrs.routerBypass || ZeroAddress, CROC_ABI, actor) : undefined, 114 | query: new Contract(context.addrs.query, QUERY_ABI, provider), 115 | slipQuery: new Contract(context.addrs.impact, IMPACT_ABI, provider), 116 | erc20Write: new Contract(ZeroAddress, ERC20_ABI, actor), 117 | erc20Read: new Contract(ZeroAddress, ERC20_READ_ABI, provider), 118 | chain: context, 119 | senderAddr: addr 120 | }; 121 | } 122 | 123 | export function lookupChain(chainId: number | string): ChainSpec { 124 | if (typeof chainId === "number") { 125 | return lookupChain("0x" + chainId.toString(16)); 126 | } else { 127 | const context = CHAIN_SPECS[chainId.toLowerCase()]; 128 | if (!context) { 129 | throw new Error("Unsupported chain ID: " + chainId); 130 | } 131 | return context; 132 | } 133 | } 134 | 135 | export async function ensureChain(cntx: CrocContext) { 136 | const walletNetwork = await cntx.actor.provider?.getNetwork() 137 | if (!walletNetwork) { 138 | throw new Error('No network selected in the wallet') 139 | } 140 | const contextNetwork = cntx.chain 141 | if (walletNetwork.chainId !== BigInt(contextNetwork.chainId)) { 142 | throw new Error(`Wrong chain selected in the wallet: expected ${contextNetwork.displayName} (${contextNetwork.chainId}) but got ${walletNetwork.name} (0x${Number(walletNetwork.chainId).toString(16)})`) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/croc.ts: -------------------------------------------------------------------------------- 1 | import { Signer, TransactionResponse, ZeroAddress } from 'ethers'; 2 | import { ConnectArg, CrocContext, connectCroc } from './context'; 3 | import { CrocKnockoutHandle } from './knockout'; 4 | import { CrocPoolView } from './pool'; 5 | import { CrocPositionView } from './position'; 6 | import { CrocSlotReader } from './slots'; 7 | import { CrocSwapPlan, CrocSwapPlanOpts } from './swap'; 8 | import { CrocTokenView, TokenQty } from './tokens'; 9 | import { TempestStrategy, TempestVault } from './vaults/tempest'; 10 | 11 | /* This is the main entry point for the Croc SDK. It provides a high-level interface 12 | * for interacting with CrocSwap smart contracts in an ergonomic way. */ 13 | export class CrocEnv { 14 | constructor (conn: ConnectArg, signer?: Signer) { 15 | this.context = connectCroc(conn, signer) 16 | this.tokens = new TokenRepo(this.context) 17 | } 18 | 19 | /* Generates a prefix object for a swap with a fixed buy quantity. 20 | * Example of generating a swap plan for buying 100 USDC by swapping DAI: 21 | * crocEnv.buy(USDC, 100).with(DAI) 22 | * 23 | * @param token The address of the token to buy. 24 | * @param qty The fixed quantity of the token to buy. */ 25 | buy (token: string, qty: TokenQty): BuyPrefix { 26 | return new BuyPrefix(token, qty, this.tokens, this.context) 27 | } 28 | 29 | /* Generates a prefix object for a swap with a fixed buy quantity of native ETH. 30 | * Example of generating a swap plan for buying 100 USDC by swapping DAI: 31 | * crocEnv.buyEth(100).with(DAI) 32 | * 33 | * @param qty The fixed quantity of native ETH to buy. */ 34 | buyEth (qty: TokenQty): BuyPrefix { 35 | return new BuyPrefix(ZeroAddress, qty, this.tokens, this.context) 36 | } 37 | 38 | /* Generates a prefix object for a swap with a fixed sell quantity. 39 | * Example of generating a swap plan for selling 100 USDC to swap into DAI: 40 | * crocEnv.sell(USDC, 100).for(DAI) 41 | * 42 | * @param token The address of the token to sell. 43 | * @param qty The fixed quantity of the token to sell. */ 44 | sell (token: string, qty: TokenQty): SellPrefix { 45 | return new SellPrefix(token, qty, this.tokens, this.context) 46 | } 47 | 48 | /* Generates a prefix object for a swap with a fixed sell quantity of native ETH. 49 | * Example of generating a swap plan for selling 100 native ETH to swap into DAI: 50 | * crocEnv.sellEth(100).for(DAI) 51 | * 52 | * @param qty The fixed quantity of native ETH to sell. */ 53 | sellEth (qty: TokenQty): SellPrefix { 54 | return new SellPrefix(ZeroAddress, qty, this.tokens, this.context) 55 | } 56 | 57 | /* Returns a view of the canonical pool for the underlying token pair. For example the 58 | * below would return pool view for WBTC/USDC with WBTC as the quote side token: 59 | * crocEnv.pool(WBTC, USDC) 60 | * 61 | * @param tokenQuote The address of the token to use as the quote token in the 62 | * view. Note the quote or base side only matters for display price 63 | * purposes. 64 | * @param tokenBase The address of the token to use as the base token in the view. */ 65 | pool (tokenQuote: string, tokenBase: string): CrocPoolView { 66 | const viewA = this.tokens.materialize(tokenQuote) 67 | const viewB = this.tokens.materialize(tokenBase) 68 | return new CrocPoolView(viewA, viewB, this.context) 69 | } 70 | 71 | /* Returns a view of the canonical pool for the token pair against native ETH. For example 72 | * the below woudl return a pool view for MKR/ETH with MKR priced in ETH for display purposes 73 | * crocEnv.poolEth(MKR) */ 74 | poolEth (token: string): CrocPoolView { 75 | return this.pool(token, ZeroAddress) 76 | } 77 | 78 | /* Returns a view of the canonical pool for the token pair against native ETH, but ETH is 79 | * priced in terms of the token. Usually the convention when ETH is paired against stablecoins 80 | * or paired against Bitcoin. For example the below would return a pool view for ETH/USDC 81 | * crocEnv.poolEthQuote(USDC) */ 82 | poolEthQuote (token: string): CrocPoolView { 83 | return this.pool(ZeroAddress, token) 84 | } 85 | 86 | /* Returns a position view for a single user on the canonical pool for a single pair. */ 87 | positions (tokenQuote: string, tokenBase: string, owner: string): CrocPositionView { 88 | return new CrocPositionView(this.tokens.materialize(tokenQuote), 89 | this.tokens.materialize(tokenBase), owner, this.context) 90 | } 91 | 92 | /* Returns a tokenView for a single token 93 | * @param token The address of the specifc token. */ 94 | token (token: string): CrocTokenView { 95 | return this.tokens.materialize(token) 96 | } 97 | 98 | /* Returns a tokenView for native ETH. */ 99 | tokenEth(): CrocTokenView { 100 | return this.tokens.materialize(ZeroAddress) 101 | } 102 | 103 | async approveBypassRouter(): Promise { 104 | return this.tokenEth().approveBypassRouter() 105 | } 106 | 107 | tempestVault (vaultAddr: string, token1Addr: string, strategy: TempestStrategy): TempestVault { 108 | let vaultView = this.tokens.materialize(vaultAddr) 109 | let token1View = this.tokens.materialize(token1Addr) 110 | return new TempestVault(vaultView, token1View, strategy, this.context) 111 | } 112 | 113 | slotReader(): CrocSlotReader { 114 | return new CrocSlotReader(this.context) 115 | } 116 | 117 | readonly context: Promise 118 | tokens: TokenRepo 119 | } 120 | 121 | class BuyPrefix { 122 | constructor (token: string, qty: TokenQty, repo: TokenRepo, context: Promise) { 123 | this.token = token 124 | this.qty = qty 125 | this.context = context 126 | this.repo = repo 127 | } 128 | 129 | with (token: string, args?: CrocSwapPlanOpts): CrocSwapPlan { 130 | return new CrocSwapPlan(this.repo.materialize(token), 131 | this.repo.materialize(this.token), this.qty, true, this.context, args) 132 | } 133 | 134 | withEth (args?: CrocSwapPlanOpts): CrocSwapPlan { 135 | return this.with(ZeroAddress, args) 136 | } 137 | 138 | atLimit (token: string, tick: number): CrocKnockoutHandle { 139 | return new CrocKnockoutHandle(this.repo.materialize(token), 140 | this.repo.materialize(this.token), this.qty, false, tick, this.context) 141 | } 142 | 143 | readonly token: string 144 | readonly qty: TokenQty 145 | readonly context: Promise 146 | repo: TokenRepo 147 | } 148 | 149 | class SellPrefix { 150 | constructor (token: string, qty: TokenQty, repo: TokenRepo, context: Promise) { 151 | this.token = token 152 | this.qty = qty 153 | this.context = context 154 | this.repo = repo 155 | } 156 | 157 | for (token: string, args?: CrocSwapPlanOpts): CrocSwapPlan { 158 | return new CrocSwapPlan(this.repo.materialize(this.token), 159 | this.repo.materialize(token), this.qty, false, this.context, args) 160 | 161 | } 162 | forEth (args?: CrocSwapPlanOpts): CrocSwapPlan { 163 | return this.for(ZeroAddress, args) 164 | } 165 | 166 | atLimit (token: string, tick: number): CrocKnockoutHandle { 167 | return new CrocKnockoutHandle(this.repo.materialize(this.token), 168 | this.repo.materialize(token), this.qty, true, tick, this.context) 169 | } 170 | 171 | readonly token: string 172 | readonly qty: TokenQty 173 | readonly context: Promise 174 | repo: TokenRepo 175 | 176 | } 177 | 178 | 179 | /* Use this to cache the construction of CrocTokenView objects across CrocEnv lifetime. 180 | * Because token view construction makes on-chain calls to get token metadata, doing this 181 | * drastically reduces the number of RPC calls. */ 182 | class TokenRepo { 183 | constructor (context: Promise) { 184 | this.tokenViews = new Map() 185 | this.context = context 186 | } 187 | 188 | /* Either generates or loads a previously cached token view object. 189 | * @param tokenAddr The Ethereum address of the token contract. */ 190 | materialize (tokenAddr: string): CrocTokenView { 191 | tokenAddr = tokenAddr.toLowerCase() 192 | let tokenView = this.tokenViews.get(tokenAddr) 193 | if (!tokenView) { 194 | tokenView = new CrocTokenView(this.context, tokenAddr) 195 | this.tokenViews.set(tokenAddr, tokenView) 196 | } 197 | return tokenView 198 | } 199 | 200 | tokenViews: Map 201 | context: Promise 202 | } 203 | -------------------------------------------------------------------------------- /src/encoding/flags.ts: -------------------------------------------------------------------------------- 1 | 2 | export type CrocSurplusFlags = boolean | [boolean, boolean] 3 | 4 | export function encodeSurplusArg (flags: CrocSurplusFlags, 5 | isPairInverted: boolean = false): number { 6 | return typeof(flags) === "boolean" ? 7 | encodeSurplusToggle(flags) : encodeSurplusPair(flags, isPairInverted) 8 | } 9 | 10 | function encodeSurplusToggle (flag: boolean): number { 11 | return flag ? 0x3 : 0x0; 12 | } 13 | 14 | function encodeSurplusPair (flags: [boolean, boolean], 15 | isPairInverted: boolean = false): number { 16 | const [leftFlag, rightFlag] = flags 17 | const [baseFlag, quoteFlag] = isPairInverted ? 18 | [rightFlag, leftFlag] : [leftFlag, rightFlag] 19 | return (baseFlag ? 0x1 : 0x0) + (quoteFlag ? 0x2 : 0x0) 20 | } 21 | 22 | export function decodeSurplusFlag (flag: number): [boolean, boolean] { 23 | return [((flag & 0x1) > 0), ((flag & 0x2) > 0)] 24 | } -------------------------------------------------------------------------------- /src/encoding/init.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "ethers"; 2 | import { encodeCrocPrice } from "../utils/price"; 3 | 4 | type Address = string; 5 | type PoolType = number; 6 | 7 | export class PoolInitEncoder { 8 | 9 | constructor (baseToken: Address, quoteToken: Address, poolIdx: PoolType) { 10 | this.baseToken = baseToken 11 | this.quoteToken = quoteToken 12 | this.poolIdx = poolIdx 13 | this.abiCoder = new ethers.AbiCoder(); 14 | } 15 | 16 | encodeInitialize (initPrice: number): string { 17 | const crocPrice = encodeCrocPrice(initPrice) 18 | const POOL_INIT_TYPES = ["uint8", "address", "address", "uint256", "uint128"] 19 | return this.abiCoder.encode(POOL_INIT_TYPES, 20 | [71, this.baseToken, this.quoteToken, this.poolIdx, crocPrice]) 21 | } 22 | 23 | private baseToken: Address 24 | private quoteToken: Address 25 | private poolIdx: PoolType 26 | private abiCoder: ethers.AbiCoder; 27 | } 28 | -------------------------------------------------------------------------------- /src/encoding/knockout.ts: -------------------------------------------------------------------------------- 1 | import { BigNumberish, ethers } from "ethers"; 2 | 3 | export class KnockoutEncoder { 4 | constructor(base: string, quote: string, poolIdx: number) { 5 | this.base = base; 6 | this.quote = quote; 7 | this.poolIdx = poolIdx; 8 | this.abiCoder = new ethers.AbiCoder(); 9 | } 10 | 11 | private base: string; 12 | private quote: string; 13 | private poolIdx: number; 14 | private abiCoder: ethers.AbiCoder; 15 | 16 | encodeKnockoutMint (qty: bigint, lowerTick:number, upperTick: number, 17 | isBid: boolean, useSurplusFlags: number): string { 18 | const MINT_SUBCMD = 91 19 | const suppArgs = this.abiCoder.encode(["uint128", "bool"], [qty, false]) 20 | return this.encodeCommonArgs(MINT_SUBCMD, lowerTick, upperTick, isBid, useSurplusFlags, suppArgs) 21 | } 22 | 23 | encodeKnockoutBurnQty (qty: bigint, lowerTick:number, upperTick: number, 24 | isBid: boolean, useSurplusFlags: number): string { 25 | const BURN_SUBCMD = 92 26 | const suppArgs = this.abiCoder.encode(["uint128", "bool", "bool"], [qty, false, false]) 27 | return this.encodeCommonArgs(BURN_SUBCMD, lowerTick, upperTick, isBid, useSurplusFlags, suppArgs) 28 | } 29 | 30 | encodeKnockoutBurnLiq (liq: bigint, lowerTick:number, upperTick: number, 31 | isBid: boolean, useSurplusFlags: number): string { 32 | const BURN_SUBCMD = 92 33 | const suppArgs = this.abiCoder.encode(["uint128", "bool", "bool"], [liq, true, true]) 34 | return this.encodeCommonArgs(BURN_SUBCMD, lowerTick, upperTick, isBid, useSurplusFlags, suppArgs) 35 | } 36 | 37 | encodeKnockoutRecover (pivotTime: number, lowerTick:number, upperTick: number, 38 | isBid: boolean, useSurplusFlags: number): string { 39 | const BURN_SUBCMD = 94 40 | const suppArgs = this.abiCoder.encode(["uint32"], [pivotTime]) 41 | return this.encodeCommonArgs(BURN_SUBCMD, lowerTick, upperTick, isBid, useSurplusFlags, suppArgs) 42 | } 43 | 44 | private encodeCommonArgs (subcmd: number, lowerTick:number, upperTick: number, 45 | isBid: boolean, useSurplusFlags: number, suppArgs: string): string { 46 | return this.abiCoder.encode(KNOCKOUT_ARG_TYPES, 47 | [subcmd, this.base, this.quote, this.poolIdx, 48 | lowerTick, upperTick, isBid, 49 | useSurplusFlags, suppArgs]) 50 | } 51 | } 52 | 53 | /* The decoded state of the tick from a CrocKnockoutCross event log. */ 54 | export interface KnockoutCrossState { 55 | pivotTime: number, 56 | feeMileage: BigNumberish, 57 | commitEntropy: bigint 58 | } 59 | 60 | export interface KnockoutClaimProof { 61 | root: bigint, 62 | steps: bigint[] 63 | } 64 | 65 | /* Packs a list of knockout cross events into a 256-bit array that can be passed directly 66 | * as a Merkle proof to the Croc knockout claim function. 67 | * 68 | * 69 | * 70 | * @remarks These values should be taken directly from the CrocKnockoutCross 71 | * event log. For an example see 72 | * https://etherscan.io/tx/0x022b1f3792b98a54c761c0a79268dbcb6e5f1a2a9f7494bab743f722957e7219#eventlog 73 | * 74 | * @param crosses The list of knockout cross events *only* at the given tick since the knockout 75 | * order was minted. Input can be in any order. 76 | * @returns The 256-bit array for the knockout proof. 77 | */ 78 | export function packKnockoutLinks (crosses: KnockoutCrossState[], merkleRoot: bigint): KnockoutClaimProof { 79 | let steps: bigint[] = [] 80 | for (let i = 0; i < crosses.length - 1; i++) { 81 | const link = packKnockoutLink(crosses[i].pivotTime, crosses[i].feeMileage, crosses[i+1].commitEntropy) 82 | steps.push(link) 83 | } 84 | return { root: merkleRoot, steps: steps } 85 | } 86 | 87 | /* Creates a single entry for an entry in a knockout proof. 88 | * 89 | * @param pivotTime The time of the pivot (from the event log). 90 | * @param mileage The mileage at the knockout cross (from the event log). 91 | * @param commitEntropy The random commit entropy (from the event log). 92 | * @returns The 256-bit array entry for the knockout proof. 93 | */ 94 | function packKnockoutLink (pivotTime: BigNumberish, 95 | mileage: BigNumberish, commitEntropy: bigint): bigint { 96 | // Converted BigInt code 97 | const packed = (BigInt(pivotTime) << BigInt(64)) + BigInt(mileage); 98 | return (commitEntropy << BigInt(96)) + BigInt(packed); 99 | } 100 | 101 | const KNOCKOUT_ARG_TYPES = [ 102 | "uint8", // Type call 103 | "address", // Base 104 | "address", // Quote 105 | "uint24", // Pool Index 106 | "int24", // Lower Tick 107 | "int24", // Upper Tick 108 | "bool", // isBid 109 | "uint8", // reserve flags 110 | "bytes", // subcmd args 111 | ]; 112 | -------------------------------------------------------------------------------- /src/encoding/liquidity.ts: -------------------------------------------------------------------------------- 1 | import { ethers, ZeroAddress } from "ethers"; 2 | import { MAX_LIQ } from "../constants"; 3 | import { encodeCrocPrice } from "../utils/price"; 4 | 5 | type Address = string; 6 | type PoolType = number; 7 | 8 | export class WarmPathEncoder { 9 | constructor(base: Address, quote: Address, poolIdx: PoolType) { 10 | this.base = base; 11 | this.quote = quote; 12 | this.poolIdx = poolIdx; 13 | this.abiCoder = new ethers.AbiCoder(); 14 | } 15 | 16 | private base: Address; 17 | private quote: Address; 18 | private poolIdx: PoolType; 19 | private abiCoder: ethers.AbiCoder; 20 | 21 | encodeMintConc( 22 | lowerTick: number, 23 | upperTick: number, 24 | qty: bigint, 25 | qtyIsBase: boolean, 26 | limitLow: number, 27 | limitHigh: number, 28 | useSurplus: number 29 | ) { 30 | return this.encodeWarmPath( 31 | qtyIsBase ? MINT_CONC_BASE : MINT_CONC_QUOTE, 32 | lowerTick, 33 | upperTick, 34 | qty, 35 | limitLow, 36 | limitHigh, 37 | useSurplus 38 | ); 39 | } 40 | 41 | encodeBurnConc( 42 | lowerTick: number, 43 | upperTick: number, 44 | liq: bigint, 45 | limitLow: number, 46 | limitHigh: number, 47 | useSurplus: number 48 | ) { 49 | return this.encodeWarmPath( 50 | BURN_CONCENTRATED, 51 | lowerTick, 52 | upperTick, 53 | liq, 54 | limitLow, 55 | limitHigh, 56 | useSurplus 57 | ); 58 | } 59 | 60 | encodeHarvestConc( 61 | lowerTick: number, 62 | upperTick: number, 63 | limitLow: number, 64 | limitHigh: number, 65 | useSurplus: number 66 | ) { 67 | return this.encodeWarmPath( 68 | HARVEST_CONCENTRATED, 69 | lowerTick, 70 | upperTick, 71 | BigInt(0), 72 | limitLow, 73 | limitHigh, 74 | useSurplus 75 | ); 76 | } 77 | 78 | encodeMintAmbient( 79 | qty: bigint, 80 | qtyIsBase: boolean, 81 | limitLow: number, 82 | limitHigh: number, 83 | useSurplus: number 84 | ) { 85 | return this.encodeWarmPath( 86 | qtyIsBase ? MINT_AMBIENT_BASE : MINT_AMBIENT_QUOTE, 87 | 0, 88 | 0, 89 | qty, 90 | limitLow, 91 | limitHigh, 92 | useSurplus 93 | ); 94 | } 95 | 96 | encodeBurnAmbient( 97 | liq: bigint, 98 | limitLow: number, 99 | limitHigh: number, 100 | useSurplus: number 101 | ) { 102 | return this.encodeWarmPath( 103 | BURN_AMBIENT, 104 | 0, 105 | 0, 106 | liq, 107 | limitLow, 108 | limitHigh, 109 | useSurplus 110 | ); 111 | } 112 | 113 | encodeBurnAmbientAll( 114 | limitLow: number, 115 | limitHigh: number, 116 | useSurplus: number 117 | ) { 118 | return this.encodeWarmPath( 119 | BURN_AMBIENT, 120 | 0, 121 | 0, 122 | MAX_LIQ, 123 | limitLow, 124 | limitHigh, 125 | useSurplus 126 | ); 127 | } 128 | 129 | private encodeWarmPath( 130 | callCode: number, 131 | lowerTick: number, 132 | upperTick: number, 133 | qty: bigint, 134 | limitLow: number, 135 | limitHigh: number, 136 | useSurplus: number 137 | ): string { 138 | return this.abiCoder.encode(WARM_ARG_TYPES, [ 139 | callCode, 140 | this.base, 141 | this.quote, 142 | this.poolIdx, 143 | lowerTick, 144 | upperTick, 145 | qty, 146 | encodeCrocPrice(limitLow), 147 | encodeCrocPrice(limitHigh), 148 | useSurplus, 149 | ZeroAddress, 150 | ]); 151 | } 152 | } 153 | 154 | const MINT_CONCENTRATED: number = 1; 155 | const MINT_CONC_BASE: number = 11; 156 | const MINT_CONC_QUOTE: number = 12 157 | const BURN_CONCENTRATED: number = 2; 158 | const MINT_AMBIENT: number = 3; 159 | const MINT_AMBIENT_BASE: number = 31; 160 | const MINT_AMBIENT_QUOTE: number = 32; 161 | const BURN_AMBIENT: number = 4; 162 | const HARVEST_CONCENTRATED: number = 5 163 | 164 | const WARM_ARG_TYPES = [ 165 | "uint8", // Type call 166 | "address", // Base 167 | "address", // Quote 168 | "uint24", // Pool Index 169 | "int24", // Lower Tick 170 | "int24", // Upper Tick 171 | "uint128", // Liquidity 172 | "uint128", // Lower limit 173 | "uint128", // Upper limit 174 | "uint8", // reserve flags 175 | "address", // deposit vault 176 | ]; 177 | 178 | export function isTradeWarmCall(txData: string): boolean { 179 | const USER_CMD_METHOD = "0xa15112f9"; 180 | const LIQ_PATH = 2 181 | const encoder = new ethers.AbiCoder(); 182 | 183 | if (txData.slice(0, 10) === USER_CMD_METHOD) { 184 | const result = encoder.decode( 185 | ["uint16", "bytes"], 186 | "0x".concat(txData.slice(10)) 187 | ); 188 | return result[0] == LIQ_PATH; 189 | } 190 | return false; 191 | } 192 | 193 | interface WarmPathArgs { 194 | isMint: boolean; 195 | isAmbient: boolean; 196 | base: string; 197 | quote: string; 198 | poolIdx: number; 199 | lowerTick: number; 200 | upperTick: number; 201 | qty: bigint; 202 | } 203 | 204 | export function decodeWarmPathCall(txData: string): WarmPathArgs { 205 | const argData = "0x".concat(txData.slice(10 + 192)); 206 | const encoder = new ethers.AbiCoder(); 207 | const result = encoder.decode(WARM_ARG_TYPES, argData); 208 | return { 209 | isMint: [MINT_AMBIENT, MINT_CONCENTRATED].includes(result[0]), 210 | isAmbient: [MINT_AMBIENT, BURN_AMBIENT].includes(result[0]), 211 | base: result[1], 212 | quote: result[2], 213 | poolIdx: result[3], 214 | lowerTick: result[4], 215 | upperTick: result[5], 216 | qty: result[6], 217 | }; 218 | } 219 | -------------------------------------------------------------------------------- /src/encoding/longform.ts: -------------------------------------------------------------------------------- 1 | import { BytesLike, ethers } from 'ethers'; 2 | 3 | export class OrderDirective { 4 | 5 | constructor (openToken: string) { 6 | this.open = simpleSettle(openToken) 7 | this.hops = [] 8 | } 9 | 10 | encodeBytes(): BytesLike { 11 | let schema = encodeWord(LONG_FORM_SCHEMA_TYPE) 12 | let open = encodeSettlement(this.open) 13 | let hops = listEncoding(this.hops, encodeHop) 14 | return ethers.concat([schema, open, hops]) 15 | } 16 | 17 | appendHop (nextToken: string): HopDirective { 18 | const hop = { settlement: simpleSettle(nextToken), 19 | pools: [], 20 | improve: { isEnabled: false, useBaseSide: false } } 21 | this.hops.push(hop) 22 | return hop 23 | } 24 | 25 | appendPool (poolIdx: number): PoolDirective { 26 | const pool = { 27 | poolIdx: BigInt(poolIdx), 28 | passive: { 29 | ambient: { isAdd: false, rollType: 0, liquidity: BigInt(0) }, 30 | concentrated: [] 31 | }, 32 | swap: { 33 | isBuy: false, 34 | inBaseQty: false, 35 | rollType: 0, 36 | qty: BigInt(0), 37 | limitPrice: BigInt(0) 38 | }, 39 | chain: { rollExit: false, swapDefer: false, offsetSurplus: false} 40 | }; 41 | (this.hops.at(-1) as HopDirective).pools.push(pool) 42 | return pool 43 | } 44 | 45 | appendRangeMint (lowTick: number, highTick: number, liq: bigint): ConcentratedDirective { 46 | const range = { lowTick: lowTick, highTick: highTick, 47 | isRelTick: false, 48 | isAdd: true, 49 | rollType: 0, 50 | liquidity: liq < 0 ? -liq : liq} 51 | const pool = ((this.hops.at(-1) as HopDirective).pools.at(-1) as PoolDirective) 52 | pool.passive.concentrated.push(range) 53 | return range 54 | } 55 | 56 | appendAmbientMint (liq: bigint): AmbientDirective { 57 | const pool = ((this.hops.at(-1) as HopDirective).pools.at(-1) as PoolDirective) 58 | pool.passive.ambient = { 59 | isAdd: true, 60 | rollType: 0, 61 | liquidity: liq < 0 ? -liq : liq 62 | } 63 | return pool.passive.ambient 64 | } 65 | 66 | appendRangeBurn (lowTick: number, highTick: number, liq: bigint): ConcentratedDirective { 67 | let range = this.appendRangeMint(lowTick, highTick, liq) 68 | range.isAdd = false 69 | return range 70 | } 71 | 72 | open: SettlementDirective 73 | hops: HopDirective[] 74 | } 75 | 76 | const LONG_FORM_SCHEMA_TYPE = 1 77 | 78 | function simpleSettle (token: string): SettlementDirective { 79 | return { token: token, limitQty: BigInt(2) ** BigInt(125), 80 | dustThresh: BigInt(0), useSurplus: false } 81 | } 82 | 83 | export interface OrderDirective { 84 | open: SettlementDirective 85 | hops: HopDirective[] 86 | } 87 | 88 | export interface SettlementDirective { 89 | token: string 90 | limitQty: bigint, 91 | dustThresh: bigint, 92 | useSurplus: boolean 93 | } 94 | 95 | export interface ImproveDirective { 96 | isEnabled: boolean, 97 | useBaseSide: boolean 98 | } 99 | 100 | export interface ChainingDirective { 101 | rollExit: boolean, 102 | swapDefer: boolean, 103 | offsetSurplus: boolean 104 | } 105 | 106 | export interface HopDirective { 107 | pools: PoolDirective[] 108 | settlement: SettlementDirective 109 | improve: ImproveDirective 110 | } 111 | 112 | export interface PoolDirective { 113 | poolIdx: bigint 114 | passive: PassiveDirective, 115 | swap: SwapDirective 116 | chain: ChainingDirective 117 | } 118 | 119 | export interface SwapDirective { 120 | isBuy: boolean, 121 | inBaseQty: boolean, 122 | qty: bigint, 123 | rollType?: number, 124 | limitPrice: bigint 125 | } 126 | 127 | export interface PassiveDirective { 128 | ambient: AmbientDirective 129 | concentrated: ConcentratedDirective[] 130 | } 131 | 132 | export interface AmbientDirective { 133 | isAdd: boolean, 134 | rollType?: number, 135 | liquidity: bigint 136 | } 137 | 138 | export interface ConcentratedDirective { 139 | lowTick: number, 140 | highTick: number, 141 | isRelTick: boolean, 142 | isAdd: boolean, 143 | rollType?: number, 144 | liquidity: bigint 145 | } 146 | 147 | 148 | function encodeSettlement (dir: SettlementDirective): BytesLike { 149 | let token = encodeToken(dir.token) 150 | let limit = encodeSigned(dir.limitQty) 151 | let dust = encodeFull(dir.dustThresh) 152 | let reserveFlag = encodeWord(dir.useSurplus ? 1 : 0) 153 | return ethers.concat([token, limit, dust, reserveFlag]) 154 | } 155 | 156 | function encodeHop (hop: HopDirective): BytesLike { 157 | let pools = listEncoding(hop.pools, encodePool) 158 | let settle = encodeSettlement(hop.settlement) 159 | let improve = encodeImprove(hop.improve) 160 | return ethers.concat([pools, settle, improve]) 161 | } 162 | 163 | function encodeImprove (improve: ImproveDirective): BytesLike { 164 | let abiCoder = new ethers.AbiCoder() 165 | return abiCoder.encode(["bool", "bool"], [improve.isEnabled, improve.useBaseSide]) 166 | } 167 | 168 | function encodeChain (chain: ChainingDirective): BytesLike { 169 | let abiCoder = new ethers.AbiCoder() 170 | return abiCoder.encode(["bool", "bool", "bool"], [chain.rollExit, chain.swapDefer, chain.offsetSurplus]) 171 | } 172 | 173 | function encodePool (pool: PoolDirective): BytesLike { 174 | let poolIdx = encodeFull(pool.poolIdx) 175 | let passive = encodePassive(pool.passive) 176 | let swap = encodeSwap(pool.swap) 177 | let chain = encodeChain(pool.chain) 178 | return ethers.concat([poolIdx, passive, swap, chain]) 179 | } 180 | 181 | function encodeSwap (swap: SwapDirective): BytesLike { 182 | let abiCoder = new ethers.AbiCoder() 183 | return abiCoder.encode(["bool", "bool", "uint8", "uint128", "uint128"], 184 | [swap.isBuy, swap.inBaseQty, swap.rollType ? swap.rollType : 0, swap.qty, swap.limitPrice]) 185 | } 186 | 187 | function encodePassive (passive: PassiveDirective): BytesLike { 188 | let ambAdd = encodeBool(passive.ambient.isAdd) 189 | let rollType = encodeWord(passive.ambient.rollType ? passive.ambient.rollType : 0) 190 | let ambLiq = encodeFull(passive.ambient.liquidity) 191 | let conc = listEncoding(passive.concentrated, encodeConc) 192 | return ethers.concat([ambAdd, rollType, ambLiq, conc]) 193 | } 194 | 195 | function encodeConc (conc: ConcentratedDirective): BytesLike { 196 | let openTick = encodeJsSigned(conc.lowTick) 197 | let closeTick = encodeJsSigned(conc.highTick) 198 | let isRelTick = encodeBool(conc.isRelTick) 199 | let isAdd = encodeBool(conc.isAdd) 200 | let rollType = encodeWord(conc.rollType ? conc.rollType : 0) 201 | let liq = encodeFull(conc.liquidity) 202 | return ethers.concat([openTick, closeTick, isRelTick, isAdd, rollType, liq]) 203 | } 204 | 205 | function listEncoding (elems: T[], encoderFn: (x: T) => BytesLike): BytesLike { 206 | let count = encodeWord(elems.length) 207 | let vals = elems.map(encoderFn) 208 | return ethers.concat([count].concat(vals)) 209 | } 210 | 211 | function encodeToken (tokenAddr: BytesLike): BytesLike { 212 | return ethers.zeroPadValue(tokenAddr, 32) 213 | } 214 | 215 | function encodeFull (val: bigint): BytesLike { 216 | let abiCoder = new ethers.AbiCoder() 217 | return abiCoder.encode(["uint256"], [val]); 218 | } 219 | 220 | function encodeSigned (val: bigint): BytesLike { 221 | let abiCoder = new ethers.AbiCoder() 222 | return abiCoder.encode(["int256"], [val]); 223 | } 224 | 225 | function encodeJsNum (val: number): BytesLike { 226 | return encodeFull(BigInt(val)) 227 | } 228 | 229 | function encodeJsSigned (val: number): BytesLike { 230 | return encodeSigned(BigInt(val)) 231 | } 232 | 233 | function encodeWord (val: number): BytesLike { 234 | return encodeJsNum(val) 235 | } 236 | 237 | function encodeBool (flag: boolean): BytesLike { 238 | return encodeWord(flag ? 1 : 0) 239 | } 240 | 241 | -------------------------------------------------------------------------------- /src/examples/demo.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers'; 2 | import { CrocEnv } from '../croc'; 3 | 4 | //const ETH = ethers.constants.ZeroAddress 5 | //const DAI = "0xdc31Ee1784292379Fbb2964b3B9C4124D8F89C60" 6 | 7 | // Scroll 8 | //const USDC = "0x06eFdBFf2a14a7c8E15944D1F4A48F9F95F663A4" 9 | 10 | // Mainnet 11 | //const USDC = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" 12 | 13 | // Sepolia 14 | //const USDC = "0x60bBA138A74C5e7326885De5090700626950d509" 15 | 16 | // Blast 17 | //const USDB = "0x4300000000000000000000000000000000000003" 18 | 19 | // deepcode ignore HardcodedSecret: testnet dummy key 20 | const KEY = process.env.WALLET_KEY || "0x7c5e2cfbba7b00ba95e5ed7cd80566021da709442e147ad3e08f23f5044a3d5a" 21 | 22 | async function demo() { 23 | const wallet = new ethers.Wallet(KEY) 24 | 25 | const croc = new CrocEnv("scroll", wallet) 26 | 27 | const tempestVault = "0x3E9E7861D68a82783FAfbaCaB21ea8F37231c56e" 28 | const tempestToken1 = "0x06eFdBFf2a14a7c8E15944D1F4A48F9F95F663A4" 29 | let vault = croc.tempestVault(tempestVault, tempestToken1, 'symetricAmbient') 30 | 31 | console.log(await vault.allowance(wallet.address)) 32 | console.log(await vault.minDeposit()) 33 | 34 | console.log(await vault.balanceVault(wallet.address)) 35 | console.log(await vault.balanceToken1(wallet.address)) 36 | 37 | console.log(await vault.queryConversionRate()) 38 | 39 | /*let balance = vault.balanceVault(wallet.address) 40 | console.log(await vault.redeemZap(await balance, 5.0))*/ 41 | 42 | //console.log(await vault.depositZap(10.0)) 43 | 44 | //let tx = croc.token(USDB).approve() 45 | //let tx = croc.tokenEth().deposit(0.01, wallet.address) 46 | //let tx = croc.token(USDB).deposit(100, wallet.address) 47 | //console.log(await tx) 48 | 49 | //console.log(await croc.approveBypassRouter()) 50 | //console.log(await croc.token(USDC).approveRouter(100)) 51 | 52 | //console.log((await croc.buy(USDC, 5).withEth().useRouter().swap())) 53 | /*console.log((await croc.sell(USDC, 0.01).forEth().useBypass().swap())) 54 | console.log((await croc.sellEth(0.00001).for(USDC).useBypass().swap())) 55 | console.log((await croc.buyEth(0.00001).with(USDC).useBypass().swap()))*/ 56 | 57 | /*const types = ["uint128","uint128","uint128","uint64","uint64","int24","bool","bool","uint8","uint128","uint128", 58 | "uint8","uint16","uint8","uint16","uint8","uint8","uint8","bytes32","address"] 59 | console.log(abi.decode(types, log))*/ 60 | 61 | //await croc.token(USDC).approve(0.001) 62 | //await croc.token(USDC).approveRouter() 63 | //await croc.token(USDC).approveBypassRouter() 64 | 65 | //console.log((await croc.sell(USDC, 7.5).forEth().useBypass().swap())) 66 | //console.log((await croc.sellEth(0).for(USDC).swap())) 67 | //console.log((await croc.buy(USDC, 1).withEth().useBypass().swap())) 68 | //console.log((await croc.buyEth(0).with(USDC).forceProxy().swap())) 69 | 70 | /*croc.token(DAI).deposit(1, wallet.address) 71 | croc.token(DAI).withdraw(0.25, wallet.address) 72 | croc.token(DAI).transfer(0.001, "0xd825D73CDD050ecbEBC0B3a8D9C5952d1F64722e")*/ 73 | 74 | //croc.tokenEth().deposit(0.001, wallet.address) 75 | //croc.tokenEth().withdraw(0.0001, wallet.address) 76 | /*let tx = croc.tokenEth().transfer(0.0001, "0xd825D73CDD050ecbEBC0B3a8D9C5952d1F64722e") 77 | 78 | await (await tx).wait() 79 | console.log(await croc.token(DAI).balanceDisplay(wallet.address)) 80 | console.log(await croc.token(DAI).balanceDisplay("0xd825D73CDD050ecbEBC0B3a8D9C5952d1F64722e")) 81 | console.log(await croc.tokenEth().balanceDisplay("0xd825D73CDD050ecbEBC0B3a8D9C5952d1F64722e")) 82 | console.log(await croc.token(DAI).walletDisplay(wallet.address))*/ 83 | 84 | //let ko = new CrocKnockoutHandle(DAI, ZeroAddress, 0.001, -78464+512, croc.context) 85 | /*ko = new CrocKnockoutHandle(DAI, ZeroAddress, 0.001, -78464+1024, croc.context) 86 | //ko = new CrocKnockoutHandle(DAI, ZeroAddress, 0.001, -78464+2048, croc.context) 87 | //ko = new CrocKnockoutHandle(ZeroAddress, DAI, 0.01, -78464-2048, croc.context) 88 | //ko = new CrocKnockoutHandle(ZeroAddress, DAI, 0.01, -78464-1024, croc.context) 89 | await (await ko.mint()).wait()*/ 90 | 91 | //let tx = croc.poolEth(DAI).burnAmbientLiq(BigInt(10).pow(7), [0.0001, 0.001]) 92 | //let tx = croc.poolEth(DAI).mintRangeBase(0.0001, [-640000, 640000], [0.0001, 0.001]) 93 | //let tx = croc.poolEth(DAI).mintRangeBase(1.0, [-100000, 0], [0.0001, 0.001]) 94 | 95 | //let tx = croc.poolEth(DAI).burnRangeLiq(BigInt(10).pow(7), [-640000, 640000], [0.0001, 0.001]) 96 | /*let tx = croc.poolEth(DAI).harvestRange([-640000, 640000], [0.0001, 0.001]) 97 | await (await tx).wait()*/ 98 | 99 | /*let ko = new CrocKnockoutHandle(DAI, ZeroAddress, 0.001, -73152, croc.context) 100 | await (await ko.mint()).wait() 101 | ko = new CrocKnockoutHandle(DAI, ZeroAddress, 0.001, -74432, croc.context) 102 | await (await ko.mint()).wait() 103 | 104 | croc.poolEth(DAI).spotTick().then(console.log) 105 | croc.poolEth(DAI).spotPrice().then(console.log)*/ 106 | 107 | /*croc.poolEth(DAI).spotPrice().then(console.log); 108 | croc.pool(DAI, ZeroAddress).displayPrice().then(console.log); 109 | croc.pool(ZeroAddress, DAI).displayPrice().then(console.log);*/ 110 | 111 | //await (await croc.pool(ZeroAddress, USDC).initPool(3000)).wait() 112 | 113 | /*croc.poolEth(USDC).spotPrice().then(console.log); 114 | croc.pool(USDC, ZeroAddress).displayPrice().then(console.log); 115 | croc.pool(ZeroAddress, USDC).displayPrice().then(console.log); 116 | 117 | croc.pool(ZeroAddress, USDC).mintAmbientQuote(100, [2000, 4000])*/ 118 | 119 | //croc.poolEth(DAI).initPool() 120 | 121 | /*await croc.poolEth(DAI).mintAmbientQuote(50, [0.0005, 0.000625]) 122 | await croc.poolEthQuote(DAI).mintAmbientBase(50, [1600, 1700])*/ 123 | //await croc.poolEthQuote(DAI).mintRangeBase(50, [-80000, -64000], [1600, 1700]) 124 | 125 | /*await croc.poolEthQuote(DAI).mintRangeBase(5, [-80000, -64000], [1600, 1700], { surplus: [true, false]}) 126 | await croc.poolEthQuote(DAI).mintRangeBase(5, [-80000, -64000], [1600, 1700], { surplus: [false, true]})*/ 127 | 128 | //await croc.poolEth(DAI).mintAmbientBase(0.0001, [0.0001, 0.001]) 129 | //await croc.poolEth(DAI).mintAmbientQuote(50, [0.0001, 0.001]) 130 | //await croc.poolEth(DAI).mintRangeBase(0.03, [-640000, 640000], [0.0001, 0.001]) 131 | //await croc.poolEth(DAI).mintRangeQuote(50, [-640000, 640000], [0.0001, 0.001]) 132 | 133 | //await croc.poolEth(DAI).burnAmbientAll([0.0001, 0.001]) 134 | 135 | //await croc.sellEth(0.0001).for(DAI).swap() 136 | //await croc.sell(DAI, 0.0001).forEth().swap() 137 | /*await croc.buy(DAI, 0.0001).withEth().swap()*/ 138 | //await croc.buyEth(0.01).with(DAI).swap() 139 | //await croc.sellEth(0.01).for(DAI, { slippage: 0.1}).swap() 140 | 141 | //console.log(await croc.sellEth(20).for(DAI, { slippage: .05}).calcImpact()) 142 | 143 | // Pay ETH from wallet, receive DAI to exchange balance 144 | /*await croc.sellEth(0.01).for(DAI).swap({surplus: [false, true]}) 145 | 146 | // Pay ETH from exchange balance, receive DAI to wallet 147 | await croc.sellEth(0.01).for(DAI).swap({surplus: [false, true]}) 148 | 149 | // Pay DAI from exchange balance, receive ETH to wallet 150 | await croc.buyEth(0.01).with(DAI).swap({surplus: [true, false]}) 151 | 152 | // Pay DAI from wallet, receive ETH to exchange balance 153 | await croc.buyEth(0.01).with(DAI).swap({surplus: [false, true]}) 154 | 155 | // Pay ETH to receive DAI, both to/from exchange balance 156 | await croc.buy(DAI, 100).withEth().swap({surplus: true}) 157 | 158 | // Pay ETH to receive DAI, both to/from wallet 159 | await croc.buy(DAI, 10).withEth().swap({surplus: false}) 160 | 161 | // Pay ETH to receive DAI, both to/from wallet 162 | await croc.buy(DAI, 10).withEth().swap()*/ 163 | 164 | // Pays DAI to wallet and ETH to exchange balance 165 | //await croc.pool(DAI, ETH).burnAmbientLiq(BigInt(10).pow(7), [0.0001, 0.001], {surplus: [false, true]}) 166 | 167 | // Pays DAI to exchange balance and ETH to wallet 168 | /*await croc.pool(DAI, ETH).burnAmbientLiq(BigInt(10).pow(7), [0.0001, 0.001], {surplus: [true, false]}) 169 | 170 | // Pays ETH to exchange balance and DAI to wallet 171 | await croc.pool(ETH, DAI).burnAmbientLiq(BigInt(10).pow(7), [1000, 10000], {surplus: [true, false]}) 172 | 173 | // Pays ETH to wallet and DAI to exchange balance 174 | await croc.pool(ETH, DAI).burnAmbientLiq(BigInt(10).pow(7), [1000, 10000], {surplus: [false, true]})*/ 175 | 176 | // Mint new limit order for $25. Pay from exchange balance 177 | /*croc.sell(DAI, 200).atLimit(ETH, -64000).burn({surplus: true})*/ 178 | 179 | // Burn $10 worth of existing limit order. Receive to wallet 180 | //croc.sell(DAI, 10).atLimit(ETH, -64000).mint({surplus: false}) 181 | 182 | // Burn 1 billion units of concentrated liquidity for the limit order 183 | //croc.sell(DAI, 2).atLimit(ETH, -64000).burnLiq(BigInt(1000000000)) 184 | 185 | /*console.log(await (await croc.token(DAI).balance(wallet.address)).toString()) 186 | console.log(await (await croc.tokenEth().balance(wallet.address)).toString()) 187 | 188 | ///console.log(baseTokenForQuoteConc(100, 1600, 1700)) 189 | 190 | //croc.buy(DAI, 10).atLimit(ETH, -80000).mint({surplus: false}) 191 | 192 | let plan = croc.buy(USDC, 1).with(ETH) 193 | console.log((await plan.impact)) 194 | console.log((await plan.calcSlipQty()).toString())*/ 195 | 196 | /*console.log(await croc.poolEthQuote(DAI).spotTick()) 197 | console.log(await croc.poolEthQuote(DAI).displayPrice())*/ 198 | 199 | //console.log(await croc.poolEth(DAI).mintAmbientQuote(50, [0.0001, 0.001])) 200 | //console.log(await croc.poolEthQuote(DAI).mintRangeBase(50, [-64000 - 3200, -64000,], [0.00000001, 100000.0])) 201 | //console.log(await croc.poolEthQuote(DAI).mintRangeBase(0.001, [-80000 - 3200, -80000,], [0.00000001, 100000.0])) 202 | 203 | //console.log(await croc.poolEthQuote(USDC).mintRangeBase(50, [208000 - 3200, 208000,], [0.00000001, 100000.0])) 204 | 205 | //console.log(await croc.poolEthQuote(DAI).mintRangeBase(0.001, [3180*64, 3182*64], [1600, 1700])) 206 | 207 | /*const pool = croc.poolEthQuote(DAI) 208 | console.log(await pool.displayToPinTick(1500)) 209 | console.log(await pool.displayToPinTick(1600)) 210 | 211 | 212 | let rebal = new CrocReposition(pool, { burn: [-64000 - 3200, -64000], mint: [-73792, -73088], 213 | liquidity: BigInt(10).pow(14) }) 214 | 215 | /*console.log((await rebal.currentCollateral()).toString()) 216 | console.log((await rebal.balancePercent()))*/ 217 | 218 | //console.log((await (await rebal.mintInput()))) 219 | //console.log((await (await rebal.swapOutput()))) 220 | //console.log(await rebal.rebal()) 221 | /*const burnRange: [number, number] = [-64000 - 3200, -64000] 222 | const mintRange: [number, number] = [-76032, -72000] 223 | console.log(await rebal.rebal(burnRange, mintRange))*/ 224 | 225 | //console.log(await croc.poolEthQuote(DAI).mintRangeBase(5, [-72000 - 3200, -64000,], [0.00000001, 100000.0])) 226 | //console.log(await croc.poolEthQuote(DAI).mintRangeBase(5, [-76032, -72000,], [1600, 1700])) 227 | //console.log(await croc.poolEth(DAI).mintRangeQuote(50, [-64000 - 3200, -64000,], [0.00000001, 100000.0])) 228 | //console.log(await croc.poolEthQuote(DAI).mintRangeBase(50, [-64000 - 3200, -64000,], [0.00000001, 100000.0])) 229 | 230 | //console.log(capitalConcFactor(1000, 250, 4000))*/ 231 | 232 | /*const pool = croc.poolEthQuote(DAI) 233 | /*console.log(await pool.displayToPinTick(1378.62)) 234 | console.log(await pool.displayToPinTick(1691.94))*/ 235 | 236 | /*const pool = croc.poolEthQuote(USDC) 237 | console.log(await pool.spotPrice()) 238 | 239 | console.log(await pool.cumAmbientGrowth()) 240 | 241 | const posView = new CrocPositionView(pool, "0x9ee66F4ac79395479d6A8Bb552AF6eC3F27049CC") 242 | 243 | console.log(await posView.queryRangePos(199308, 201312))*/ 244 | 245 | //console.log(await croc.poolEthQuote(USDC).displayPrice()) 246 | 247 | //croc.sell(DAI, 200).atLimit(ETH, -64000).burn({surplus: true}) 248 | 249 | /*console.log((await croc.tokenEth().balance("benwolski.eth")).toString()) 250 | console.log(await croc.tokenEth().balanceDisplay("benwolski.eth"))*/ 251 | 252 | /*croc.slotReader().isHotPathOpen().then(console.log) 253 | console.log(await croc.slotReader().proxyContract(1)) 254 | console.log(await croc.slotReader().proxyContract(131)) 255 | 256 | console.log(await croc.poolEthQuote(USDB).curveState())*/ 257 | } 258 | 259 | demo() 260 | -------------------------------------------------------------------------------- /src/examples/poolPrice.ts: -------------------------------------------------------------------------------- 1 | import { ETH, USDC } from '../constants/mainnetTokens'; 2 | import { CrocEnv } from '../croc'; 3 | 4 | 5 | async function demo() { 6 | 7 | const croc = new CrocEnv("mainnet") 8 | 9 | const spotPrice = await croc.pool(ETH, USDC).spotPrice() 10 | console.log(`ETH/USDC Spot Price: ${spotPrice.toString()}`) 11 | 12 | const displayPrice = await croc.poolEthQuote(USDC).displayPrice() 13 | console.log(`ETH/USDC Price: ${displayPrice}`) 14 | 15 | const invDispPrice = await croc.poolEth(USDC).displayPrice() 16 | console.log(`USDC/ETH Price: ${invDispPrice}`) 17 | } 18 | 19 | demo() 20 | -------------------------------------------------------------------------------- /src/examples/queryMinimal.ts: -------------------------------------------------------------------------------- 1 | /* Example script to make a query call to CrocQuery contract with 2 | * minimal syntactic sugar from the SDK, to make clear what's going on. */ 3 | 4 | import { Contract, ethers, } from "ethers" 5 | import { QUERY_ABI } from "../abis" 6 | import { CHAIN_SPECS } from "../constants" 7 | 8 | // Goerli network addresses 9 | const ETH = ethers.ZeroAddress 10 | const USDC = "0xD87Ba7A50B2E7E660f678A895E4B72E7CB4CCd9C" 11 | 12 | // CrocQuery adddress 13 | const QUERY_CONTRACT_ADDR = "0x93a4baFDd49dB0e06f3F3f9FddC1A67792F47518" 14 | 15 | // Standard pool type index on Goerli 16 | const POOL_IDX = 36000 17 | 18 | // Goerli RPC endpoint 19 | const rpc = CHAIN_SPECS["0x5"].nodeUrl 20 | 21 | async function queryContract() { 22 | const provider = new ethers.JsonRpcProvider(rpc) 23 | 24 | const query = new Contract(QUERY_CONTRACT_ADDR, QUERY_ABI, provider) 25 | 26 | // Note this is the on-chain representation, not the display price 27 | const chainPrice = await query.queryPrice(ETH, USDC, POOL_IDX) 28 | console.log("On chain pool price is ", chainPrice.toString()) 29 | } 30 | 31 | queryContract() 32 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./constants"; 2 | export * from "./utils"; 3 | export * from "./abis"; 4 | export * from "./pool"; 5 | export * from "./position"; 6 | export * from "./swap"; 7 | export * from "./croc"; 8 | 9 | export * from "./encoding/liquidity"; 10 | export * from "./encoding/longform"; 11 | export * from "./recipes/reposition"; 12 | -------------------------------------------------------------------------------- /src/knockout.ts: -------------------------------------------------------------------------------- 1 | import { TransactionResponse, ZeroAddress } from 'ethers'; 2 | import { ChainSpec } from "./constants"; 3 | import { CrocContext, ensureChain } from './context'; 4 | import { CrocSurplusFlags, decodeSurplusFlag, encodeSurplusArg } from "./encoding/flags"; 5 | import { KnockoutEncoder } from "./encoding/knockout"; 6 | import { CrocEthView, CrocTokenView, sortBaseQuoteViews, TokenQty } from './tokens'; 7 | import { baseTokenForQuoteConc, bigIntToFloat, floatToBigInt, GAS_PADDING, quoteTokenForBaseConc, roundForConcLiq } from "./utils"; 8 | 9 | 10 | export class CrocKnockoutHandle { 11 | 12 | constructor (sellToken: CrocTokenView, buyToken: CrocTokenView, qty: TokenQty, inSellQty: boolean, 13 | knockoutTick: number, context: Promise) { 14 | [this.baseToken, this.quoteToken] = sortBaseQuoteViews(sellToken, buyToken) 15 | this.sellBase = (this.baseToken === sellToken) 16 | this.qtyInBase = inSellQty ? this.sellBase : !this.sellBase 17 | 18 | const tokenView = this.qtyInBase ? this.baseToken : this.quoteToken 19 | const specQty = tokenView.normQty(qty) 20 | 21 | this.qty = inSellQty ? specQty : 22 | calcSellQty(specQty, !this.sellBase, knockoutTick, context) 23 | 24 | this.knockoutTick = knockoutTick 25 | this.context = context 26 | } 27 | 28 | async mint (opts?: CrocKnockoutOpts): Promise { 29 | const chain = (await this.context).chain 30 | const encoder = new KnockoutEncoder(this.baseToken.tokenAddr, 31 | this.quoteToken.tokenAddr, chain.poolIndex) 32 | const [lowerTick, upperTick] = this.tickRange(chain) 33 | const surplus = this.maskSurplusFlags(opts) 34 | 35 | const cmd = encoder.encodeKnockoutMint(await this.qty, lowerTick, upperTick, 36 | this.sellBase, surplus); 37 | return this.sendCmd(cmd, { value: await this.msgVal(surplus) }) 38 | } 39 | 40 | async burn (opts?: CrocKnockoutOpts): Promise { 41 | const chain = (await this.context).chain 42 | const encoder = new KnockoutEncoder(this.baseToken.tokenAddr, this.quoteToken.tokenAddr, 43 | chain.poolIndex) 44 | const [lowerTick, upperTick] = this.tickRange(chain) 45 | const surplus = this.maskSurplusFlags(opts) 46 | 47 | const cmd = encoder.encodeKnockoutBurnQty(await this.qty, lowerTick, upperTick, 48 | this.sellBase, surplus); 49 | return this.sendCmd(cmd) 50 | } 51 | 52 | async burnLiq (liq: bigint, opts?: CrocKnockoutOpts): Promise { 53 | const chain = (await this.context).chain 54 | const encoder = new KnockoutEncoder(this.baseToken.tokenAddr, this.quoteToken.tokenAddr, 55 | chain.poolIndex) 56 | const [lowerTick, upperTick] = this.tickRange(chain) 57 | const surplus = this.maskSurplusFlags(opts) 58 | 59 | const cmd = encoder.encodeKnockoutBurnLiq(roundForConcLiq(liq), lowerTick, upperTick, 60 | this.sellBase, surplus); 61 | return this.sendCmd(cmd) 62 | } 63 | 64 | async recoverPost (pivotTime: number, opts?: CrocKnockoutOpts): Promise { 65 | const chain = (await this.context).chain 66 | const encoder = new KnockoutEncoder(this.baseToken.tokenAddr, this.quoteToken.tokenAddr, 67 | chain.poolIndex) 68 | const [lowerTick, upperTick] = this.tickRange(chain) 69 | const surplus = this.maskSurplusFlags(opts) 70 | 71 | const cmd = encoder.encodeKnockoutRecover(pivotTime, lowerTick, upperTick, 72 | this.sellBase, surplus); 73 | return this.sendCmd(cmd) 74 | } 75 | 76 | async willMintFail(): Promise { 77 | const gridSize = this.context.then(c => c.chain.gridSize) 78 | const marketTick = this.context.then(c => c.query.queryCurveTick 79 | (this.baseToken.tokenAddr, this.quoteToken.tokenAddr, c.chain.poolIndex)) 80 | return this.sellBase ? 81 | (this.knockoutTick + await gridSize >= await marketTick) : 82 | (this.knockoutTick - await gridSize <= await marketTick) 83 | } 84 | 85 | private async sendCmd (calldata: string, txArgs?: { value?: bigint }): 86 | Promise { 87 | let cntx = await this.context 88 | if (txArgs === undefined) { txArgs = {} } 89 | await ensureChain(cntx) 90 | const gasEst = await cntx.dex.userCmd.estimateGas(KNOCKOUT_PATH, calldata, txArgs) 91 | Object.assign(txArgs, { gasLimit: gasEst + GAS_PADDING, chainId: cntx.chain.chainId }) 92 | return cntx.dex.userCmd(KNOCKOUT_PATH, calldata, txArgs); 93 | } 94 | 95 | private maskSurplusFlags (opts?: CrocKnockoutOpts): number { 96 | if (!opts || !opts.surplus) { return encodeSurplusArg(false) } 97 | else { 98 | return encodeSurplusArg(opts.surplus) } 99 | } 100 | 101 | private async msgVal (surplusFlags: number): Promise { 102 | if (this.baseToken.tokenAddr !== ZeroAddress || !this.sellBase) { 103 | return BigInt(0) 104 | } 105 | 106 | const useSurp = decodeSurplusFlag(surplusFlags)[0] 107 | if (useSurp) { 108 | return new CrocEthView(this.context).msgValOverSurplus(await this.qty) 109 | } else { 110 | return this.qty 111 | } 112 | } 113 | 114 | private tickRange (chain: ChainSpec): [number, number] { 115 | return tickRange(chain, this.knockoutTick, this.sellBase) 116 | } 117 | 118 | readonly baseToken: CrocTokenView 119 | readonly quoteToken: CrocTokenView 120 | readonly qty: Promise 121 | readonly sellBase: boolean 122 | readonly qtyInBase: boolean 123 | readonly knockoutTick: number 124 | readonly context: Promise 125 | } 126 | 127 | export interface CrocKnockoutOpts { 128 | surplus?: CrocSurplusFlags 129 | } 130 | 131 | const KNOCKOUT_PATH = 7 132 | 133 | async function calcSellQty (buyQty: Promise, isQtyInBase: boolean, knockoutTick: number, 134 | context: Promise): Promise { 135 | const sellQty = calcSellFloat(bigIntToFloat(await buyQty), isQtyInBase, knockoutTick, context) 136 | return sellQty.then(floatToBigInt) 137 | } 138 | 139 | async function calcSellFloat (buyQty: number, isQtyInBase: boolean, knockoutTick: number, 140 | context: Promise): Promise { 141 | const [lowerTick, upperTick] = tickRange((await context).chain, knockoutTick, !isQtyInBase) 142 | const lowerPrice = Math.pow(1.0001, lowerTick) 143 | const upperPrice = Math.pow(1.0001, upperTick) 144 | 145 | return isQtyInBase ? 146 | baseTokenForQuoteConc(buyQty, lowerPrice, upperPrice) : 147 | quoteTokenForBaseConc(buyQty, lowerPrice, upperPrice) 148 | } 149 | 150 | function tickRange (chain: ChainSpec, knockoutTick: number, sellBase: boolean): [number, number] { 151 | return sellBase ? 152 | [knockoutTick, knockoutTick + chain.gridSize] : 153 | [knockoutTick - chain.gridSize, knockoutTick] 154 | } 155 | -------------------------------------------------------------------------------- /src/pool.ts: -------------------------------------------------------------------------------- 1 | import { TransactionResponse, ZeroAddress } from 'ethers'; 2 | import { CrocContext, ensureChain } from "./context"; 3 | import { CrocSurplusFlags, decodeSurplusFlag, encodeSurplusArg } from "./encoding/flags"; 4 | import { PoolInitEncoder } from "./encoding/init"; 5 | import { WarmPathEncoder } from './encoding/liquidity'; 6 | import { CrocEthView, CrocTokenView, sortBaseQuoteViews, TokenQty } from './tokens'; 7 | import { bigIntToFloat, concBaseSlippagePrice, concDepositSkew, concQuoteSlippagePrice, decodeCrocPrice, fromDisplayPrice, GAS_PADDING, neighborTicks, pinTickLower, pinTickOutside, pinTickUpper, roundForConcLiq, tickToPrice, toDisplayPrice, toDisplayQty } from './utils'; 8 | 9 | type PriceRange = [number, number] 10 | type TickRange = [number, number] 11 | type BlockTag = number | string 12 | 13 | export class CrocPoolView { 14 | 15 | constructor (quoteToken: CrocTokenView, baseToken: CrocTokenView, context: Promise) { 16 | [this.baseToken, this.quoteToken] = 17 | sortBaseQuoteViews(baseToken, quoteToken) 18 | this.context = context 19 | 20 | this.baseDecimals = this.baseToken.decimals 21 | this.quoteDecimals = this.quoteToken.decimals 22 | 23 | this.useTrueBase = this.baseToken.tokenAddr === baseToken.tokenAddr 24 | } 25 | 26 | /* Checks to see if a canonical pool has been initialized for this pair. */ 27 | async isInit(): Promise { 28 | return this.spotPrice() 29 | .then(p => p > 0) 30 | } 31 | 32 | async spotPrice (block?: BlockTag): Promise { 33 | let txArgs = block ? { blockTag: block } : {} 34 | let sqrtPrice = (await this.context).query.queryPrice.staticCall 35 | (this.baseToken.tokenAddr, this.quoteToken.tokenAddr, 36 | (await this.context).chain.poolIndex, txArgs) 37 | return decodeCrocPrice(await sqrtPrice) 38 | } 39 | 40 | async displayPrice (block?: BlockTag): Promise { 41 | let spotPrice = this.spotPrice(block) 42 | return this.toDisplayPrice(await spotPrice) 43 | } 44 | 45 | async spotTick (block?: BlockTag): Promise { 46 | let txArgs = block ? { blockTag: block } : {} 47 | return (await this.context).query.queryCurveTick.staticCall 48 | (this.baseToken.tokenAddr, this.quoteToken.tokenAddr, 49 | (await this.context).chain.poolIndex, txArgs).then(Number) 50 | } 51 | 52 | async xykLiquidity (block?: BlockTag): Promise { 53 | let txArgs = block ? { blockTag: block } : {} 54 | return (await this.context).query.queryLiquidity.staticCall 55 | (this.baseToken.tokenAddr, this.quoteToken.tokenAddr, 56 | (await this.context).chain.poolIndex, txArgs) 57 | } 58 | 59 | async curveState (block?: BlockTag): Promise<{priceRoot_: bigint, ambientSeeds_: bigint, concLiq_: bigint, seedDeflator_: bigint, concGrowth_: bigint}> { 60 | let txArgs = block ? { blockTag: block } : {} 61 | return (await this.context).query.queryCurve.staticCall 62 | (this.baseToken.tokenAddr, this.quoteToken.tokenAddr, 63 | (await this.context).chain.poolIndex, txArgs) 64 | } 65 | 66 | async cumAmbientGrowth (block?: BlockTag): Promise { 67 | const seedDeflator = (await this.curveState(block)).seedDeflator_ 68 | return bigIntToFloat(seedDeflator) / Math.pow(2, 48) 69 | } 70 | 71 | async toDisplayPrice (spotPrice: number | bigint): Promise { 72 | return toDisplayPrice(spotPrice, await this.baseDecimals, await this.quoteDecimals, 73 | !this.useTrueBase) 74 | } 75 | 76 | async fromDisplayPrice (dispPrice: number): Promise { 77 | return fromDisplayPrice(dispPrice, await this.baseDecimals, await this.quoteDecimals, 78 | !this.useTrueBase) 79 | } 80 | 81 | async displayToPinTick (dispPrice: number): Promise<[number, number]> { 82 | const spotPrice = await this.fromDisplayPrice(dispPrice) 83 | const gridSize = (await this.context).chain.gridSize 84 | return [pinTickLower(spotPrice, gridSize), pinTickUpper(spotPrice, gridSize)] 85 | } 86 | 87 | async displayToNeighborTicks (dispPrice: number, nNeighbors: number = 3): 88 | Promise<{below: number[], above: number[]}> { 89 | const spotPrice = await this.fromDisplayPrice(dispPrice) 90 | const gridSize = (await this.context).chain.gridSize 91 | return neighborTicks(spotPrice, gridSize, nNeighbors) 92 | } 93 | 94 | async displayToNeighborTickPrices (dispPrice: number, nNeighbors: number = 3): 95 | Promise<{below: number[], above: number[]}> { 96 | const ticks = await this.displayToNeighborTicks(dispPrice, nNeighbors) 97 | const toPriceFn = (tick: number) => this.toDisplayPrice(tickToPrice(tick)) 98 | 99 | const belowPrices = Promise.all(ticks.below.map(toPriceFn)) 100 | const abovePrices = Promise.all(ticks.above.map(toPriceFn)) 101 | 102 | return this.useTrueBase ? 103 | { below: await belowPrices, above: await abovePrices } : 104 | { below: await abovePrices, above: await belowPrices } 105 | } 106 | 107 | async displayToOutsidePin (dispPrice: number): 108 | Promise<{ tick: number, price: number, isTickBelow: boolean, isPriceBelow: boolean }> { 109 | const spotPrice = this.fromDisplayPrice(dispPrice) 110 | const gridSize = (await this.context).chain.gridSize 111 | 112 | const pinTick = pinTickOutside(await spotPrice, await this.spotPrice(), gridSize) 113 | const pinPrice = this.toDisplayPrice(tickToPrice(pinTick.tick)) 114 | 115 | return Object.assign(pinTick, { price: await pinPrice, 116 | isPriceBelow: (await pinPrice) < dispPrice }) 117 | } 118 | 119 | async initPool (initPrice: number): Promise { 120 | // Very small amount of ETH in economic terms but more than sufficient for min init burn 121 | const ETH_INIT_BURN = BigInt(10) ** BigInt(12) 122 | let txArgs = this.baseToken.tokenAddr === ZeroAddress ? { value: ETH_INIT_BURN } : { } 123 | 124 | let encoder = new PoolInitEncoder(this.baseToken.tokenAddr, this.quoteToken.tokenAddr, 125 | (await this.context).chain.poolIndex) 126 | let spotPrice = this.fromDisplayPrice(initPrice) 127 | let calldata = encoder.encodeInitialize(await spotPrice) 128 | 129 | let cntx = await this.context 130 | await ensureChain(cntx) 131 | const gasEst = await cntx.dex.userCmd.estimateGas(cntx.chain.proxyPaths.cold, calldata, txArgs) 132 | Object.assign(txArgs, { gasLimit: gasEst + GAS_PADDING, chainId: cntx.chain.chainId }) 133 | return cntx.dex.userCmd(cntx.chain.proxyPaths.cold, calldata, txArgs) 134 | } 135 | 136 | async mintAmbientBase (qty: TokenQty, limits: PriceRange, opts?: CrocLpOpts): 137 | Promise { 138 | return this.mintAmbient(qty, this.useTrueBase, limits, opts) 139 | } 140 | 141 | async mintAmbientQuote (qty: TokenQty, limits: PriceRange, opts?: CrocLpOpts): 142 | Promise { 143 | return this.mintAmbient(qty, !this.useTrueBase, limits, opts) 144 | } 145 | 146 | async mintRangeBase (qty: TokenQty, range: TickRange, limits: PriceRange, opts?: CrocLpOpts): 147 | Promise { 148 | return this.mintRange(qty, this.useTrueBase, range, await limits, opts) 149 | } 150 | 151 | async mintRangeQuote (qty: TokenQty, range: TickRange, limits: PriceRange, opts?: CrocLpOpts): 152 | Promise { 153 | return this.mintRange(qty, !this.useTrueBase, range, await limits, opts) 154 | } 155 | 156 | async burnAmbientLiq (liq: bigint, limits: PriceRange, opts?: CrocLpOpts): 157 | Promise { 158 | let [lowerBound, upperBound] = await this.transformLimits(limits) 159 | const calldata = (await this.makeEncoder()).encodeBurnAmbient 160 | (liq, lowerBound, upperBound, this.maskSurplusFlag(opts)) 161 | return this.sendCmd(calldata) 162 | } 163 | 164 | async burnAmbientAll (limits: PriceRange, opts?: CrocLpOpts): Promise { 165 | let [lowerBound, upperBound] = await this.transformLimits(limits) 166 | const calldata = (await this.makeEncoder()).encodeBurnAmbientAll 167 | (lowerBound, upperBound, this.maskSurplusFlag(opts)) 168 | return this.sendCmd(calldata) 169 | } 170 | 171 | async burnRangeLiq (liq: bigint, range: TickRange, limits: PriceRange, opts?: CrocLpOpts): 172 | Promise { 173 | let [lowerBound, upperBound] = await this.transformLimits(limits) 174 | let roundLotLiq = roundForConcLiq(liq) 175 | const calldata = (await this.makeEncoder()).encodeBurnConc 176 | (range[0], range[1], roundLotLiq, lowerBound, upperBound, this.maskSurplusFlag(opts)) 177 | return this.sendCmd(calldata) 178 | } 179 | 180 | async harvestRange (range: TickRange, limits: PriceRange, opts?: CrocLpOpts): 181 | Promise { 182 | let [lowerBound, upperBound] = await this.transformLimits(limits) 183 | const calldata = (await this.makeEncoder()).encodeHarvestConc 184 | (range[0], range[1], lowerBound, upperBound, this.maskSurplusFlag(opts)) 185 | return this.sendCmd(calldata) 186 | } 187 | 188 | private async sendCmd (calldata: string, txArgs?: { value?: bigint }): 189 | Promise { 190 | let cntx = await this.context 191 | if (txArgs === undefined) { txArgs = {} } 192 | await ensureChain(cntx) 193 | const gasEst = await cntx.dex.userCmd.estimateGas(cntx.chain.proxyPaths.liq, calldata, txArgs) 194 | Object.assign(txArgs, { gasLimit: gasEst + GAS_PADDING, chainId: cntx.chain.chainId }) 195 | return cntx.dex.userCmd(cntx.chain.proxyPaths.liq, calldata, txArgs); 196 | } 197 | 198 | private async mintAmbient (qty: TokenQty, isQtyBase: boolean, 199 | limits: PriceRange, opts?: CrocLpOpts): Promise { 200 | let msgVal = this.msgValAmbient(qty, isQtyBase, limits, opts) 201 | let weiQty = this.normQty(qty, isQtyBase) 202 | let [lowerBound, upperBound] = await this.transformLimits(limits) 203 | 204 | const calldata = (await this.makeEncoder()).encodeMintAmbient( 205 | await weiQty, isQtyBase, lowerBound, upperBound, this.maskSurplusFlag(opts)) 206 | return this.sendCmd(calldata, {value: await msgVal}) 207 | } 208 | 209 | private async boundLimits (range: TickRange, limits: PriceRange, isQtyBase: boolean, 210 | floatingSlippage: number = 0.1): Promise { 211 | let spotPrice = this.spotPrice() 212 | const [lowerPrice, upperPrice] = this.rangeToPrice(range) 213 | const [boundLower, boundUpper] = await this.transformLimits(limits) 214 | const BOUND_PREC = 1.0001 215 | 216 | let [amplifyLower, amplifyUpper] = [boundLower, boundUpper] 217 | 218 | if (upperPrice < await spotPrice) { 219 | amplifyLower = upperPrice*BOUND_PREC 220 | } else if (lowerPrice > await spotPrice) { 221 | amplifyUpper = lowerPrice/BOUND_PREC 222 | } else { 223 | if (isQtyBase) { 224 | amplifyLower = concBaseSlippagePrice(await spotPrice, upperPrice, floatingSlippage) 225 | } else { 226 | amplifyUpper = concQuoteSlippagePrice(await spotPrice, lowerPrice, floatingSlippage) 227 | } 228 | } 229 | 230 | return this.untransformLimits( 231 | [Math.max(amplifyLower, boundLower), Math.min(amplifyUpper, boundUpper)]) 232 | } 233 | 234 | private rangeToPrice (range: TickRange): PriceRange { 235 | const lowerPrice = Math.pow(1.0001, range[0]) 236 | const upperPrice = Math.pow(1.0001, range[1]) 237 | return [lowerPrice, upperPrice] 238 | } 239 | 240 | private async transformLimits (limits: PriceRange): Promise { 241 | let left = this.fromDisplayPrice(limits[0]) 242 | let right = this.fromDisplayPrice(limits[1]) 243 | return (await left < await right) ? 244 | [await left, await right] : 245 | [await right, await left] 246 | } 247 | 248 | private async untransformLimits (limits: PriceRange): Promise { 249 | let left = this.toDisplayPrice(limits[0]) 250 | let right = this.toDisplayPrice(limits[1]) 251 | return (await left < await right) ? 252 | [await left, await right] : 253 | [await right, await left] 254 | } 255 | 256 | private async mintRange (qty: TokenQty, isQtyBase: boolean, 257 | range: TickRange, limits: PriceRange, opts?: CrocLpOpts): Promise { 258 | const saneLimits = await this.boundLimits(range, limits, isQtyBase, opts?.floatingSlippage) 259 | 260 | let msgVal = this.msgValRange(qty, isQtyBase, range, await saneLimits, opts) 261 | let weiQty = this.normQty(qty, isQtyBase) 262 | let [lowerBound, upperBound] = await this.transformLimits(await saneLimits) 263 | 264 | const calldata = (await this.makeEncoder()).encodeMintConc(range[0], range[1], 265 | await weiQty, isQtyBase, lowerBound, upperBound, this.maskSurplusFlag(opts)) 266 | return this.sendCmd(calldata, { value: await msgVal }) 267 | } 268 | 269 | private maskSurplusFlag (opts?: CrocLpOpts): number { 270 | if (!opts || opts.surplus === undefined) { return this.maskSurplusFlag({surplus: false})} 271 | return encodeSurplusArg(opts.surplus, this.useTrueBase) 272 | } 273 | 274 | private async msgValAmbient (qty: TokenQty, isQtyBase: boolean, limits: PriceRange, 275 | opts?: CrocLpOpts): Promise { 276 | let ethQty = isQtyBase ? qty : 277 | this.ethForAmbientQuote(qty, limits) 278 | return this.ethToAttach(await ethQty, opts) 279 | } 280 | 281 | private async msgValRange (qty: TokenQty, isQtyBase: boolean, range: TickRange, 282 | limits: PriceRange, opts?: CrocLpOpts): Promise { 283 | let ethQty = isQtyBase ? qty : 284 | this.ethForRangeQuote(qty, range, limits) 285 | return this.ethToAttach(await ethQty, opts) 286 | } 287 | 288 | private async ethToAttach (neededQty: TokenQty, opts?: CrocLpOpts): Promise { 289 | if (this.baseToken.tokenAddr !== ZeroAddress) { return BigInt(0) } 290 | 291 | const ethQty = await this.normEth(neededQty) 292 | let useSurplus = decodeSurplusFlag(this.maskSurplusFlag(opts))[0] 293 | 294 | if (useSurplus) { 295 | return new CrocEthView(this.context).msgValOverSurplus(ethQty) 296 | } else { 297 | return ethQty 298 | } 299 | } 300 | 301 | private async ethForAmbientQuote (quoteQty: TokenQty, limits: PriceRange): Promise { 302 | const weiEth = this.calcEthInQuote(quoteQty, limits) 303 | return toDisplayQty(await weiEth, await this.baseDecimals) 304 | } 305 | 306 | private async calcEthInQuote (quoteQty: TokenQty, limits: PriceRange, 307 | precAdj: number = 1.001): Promise { 308 | const weiQty = await this.normQty(quoteQty, false); 309 | const [, boundPrice] = await this.transformLimits(limits) 310 | return Math.round(bigIntToFloat(weiQty) * boundPrice * precAdj) 311 | } 312 | 313 | private async ethForRangeQuote (quoteQty: TokenQty, range: TickRange, 314 | limits: PriceRange): Promise { 315 | const spotPrice = await this.spotPrice() ; 316 | const [lowerPrice, upperPrice] = this.rangeToPrice(range) 317 | 318 | let skew = concDepositSkew( spotPrice , lowerPrice, upperPrice) 319 | let ambiQty = this.calcEthInQuote(quoteQty, limits) 320 | let concQty = ambiQty.then(aq => Math.ceil(aq * skew)) 321 | 322 | return toDisplayQty(await concQty, await this.baseDecimals) 323 | } 324 | 325 | private async normEth (ethQty: TokenQty): Promise { 326 | return this.normQty(ethQty, true) // ETH is always on base side 327 | } 328 | 329 | private async normQty (qty: TokenQty, isBase: boolean): Promise { 330 | let token = isBase ? this.baseToken : this.quoteToken 331 | return token.normQty(qty) 332 | } 333 | 334 | private async makeEncoder(): Promise { 335 | return new WarmPathEncoder(this.baseToken.tokenAddr, this.quoteToken.tokenAddr, 336 | (await this.context).chain.poolIndex) 337 | } 338 | 339 | readonly baseToken: CrocTokenView 340 | readonly quoteToken: CrocTokenView 341 | readonly baseDecimals: Promise 342 | readonly quoteDecimals: Promise 343 | readonly useTrueBase: boolean 344 | readonly context: Promise 345 | } 346 | 347 | export interface CrocLpOpts { 348 | surplus?: CrocSurplusFlags 349 | floatingSlippage?: number 350 | } 351 | -------------------------------------------------------------------------------- /src/position.ts: -------------------------------------------------------------------------------- 1 | import { CrocContext } from './context'; 2 | import { CrocTokenView, sortBaseQuoteViews } from './tokens'; 3 | 4 | type Address = string 5 | export type BlockTag = number | string 6 | 7 | export class CrocPositionView { 8 | constructor (base: CrocTokenView, quote: CrocTokenView, owner: Address, context: Promise) { 9 | [this.baseToken, this.quoteToken] = 10 | sortBaseQuoteViews(base, quote) 11 | this.owner = owner 12 | this.context = context 13 | } 14 | 15 | async queryRangePos (lowerTick: number, upperTick: number, block?: BlockTag) { 16 | let blockArg = toCallArg(block) 17 | let context = await this.context 18 | return context.query.queryRangePosition(this.owner, 19 | this.baseToken.tokenAddr, this.quoteToken.tokenAddr, 20 | context.chain.poolIndex, lowerTick, upperTick, blockArg) 21 | } 22 | 23 | async queryAmbient (block?: BlockTag) { 24 | let blockArg = toCallArg(block) 25 | let context = await this.context 26 | return context.query.queryAmbientPosition(this.owner, 27 | this.baseToken.tokenAddr, this.quoteToken.tokenAddr, 28 | context.chain.poolIndex, blockArg) 29 | } 30 | 31 | async queryAmbientPos (block?: BlockTag) { 32 | let blockArg = toCallArg(block) 33 | let context = await this.context 34 | return context.query.queryAmbientTokens(this.owner, 35 | this.baseToken.tokenAddr, this.quoteToken.tokenAddr, 36 | context.chain.poolIndex, blockArg) 37 | } 38 | 39 | async queryKnockoutLivePos (isBid: boolean, lowerTick: number, upperTick: number, block?: BlockTag) { 40 | let blockArg = toCallArg(block) 41 | let context = await this.context 42 | let pivotTick = isBid ? lowerTick : upperTick 43 | 44 | const pivotTime = (await context.query.queryKnockoutPivot( 45 | this.baseToken.tokenAddr, this.quoteToken.tokenAddr, 46 | context.chain.poolIndex, isBid, pivotTick, blockArg)).pivot 47 | 48 | return context.query.queryKnockoutTokens(this.owner, 49 | this.baseToken.tokenAddr, this.quoteToken.tokenAddr, 50 | context.chain.poolIndex, pivotTime, isBid, lowerTick, upperTick, blockArg) 51 | } 52 | 53 | async queryRewards (lowerTick: number, upperTick: number, block?: BlockTag) { 54 | let blockArg = toCallArg(block) 55 | let context = await this.context 56 | return (await context.query.queryConcRewards(this.owner, 57 | this.baseToken.tokenAddr, this.quoteToken.tokenAddr, 58 | context.chain.poolIndex, lowerTick, upperTick, blockArg)) 59 | } 60 | 61 | readonly owner: Address 62 | readonly baseToken: CrocTokenView 63 | readonly quoteToken: CrocTokenView 64 | readonly context: Promise 65 | } 66 | 67 | function toCallArg (block?: BlockTag): { blockTag?: BlockTag } { 68 | return block ? { blockTag: block } : {} 69 | } 70 | -------------------------------------------------------------------------------- /src/recipes/reposition.ts: -------------------------------------------------------------------------------- 1 | import { TransactionResponse } from "ethers"; 2 | import { ensureChain } from "../context"; 3 | import { OrderDirective, PoolDirective } from "../encoding/longform"; 4 | import { CrocPoolView } from "../pool"; 5 | import { CrocSwapPlan } from "../swap"; 6 | import { CrocTokenView } from "../tokens"; 7 | import { encodeCrocPrice, GAS_PADDING, tickToPrice } from "../utils"; 8 | import { baseTokenForConcLiq, concDepositBalance, quoteTokenForConcLiq } from "../utils/liquidity"; 9 | 10 | 11 | interface RepositionTarget { 12 | mint: TickRange | AmbientRange 13 | burn: TickRange 14 | liquidity: bigint 15 | } 16 | 17 | type AmbientRange = "ambient" 18 | 19 | export interface CrocRepositionOpts { 20 | impact?: number 21 | } 22 | 23 | export class CrocReposition { 24 | 25 | constructor (pool: CrocPoolView, target: RepositionTarget, opts: CrocRepositionOpts = { }) { 26 | this.pool = pool 27 | this.burnRange = target.burn 28 | this.mintRange = target.mint 29 | this.liquidity = target.liquidity 30 | this.spotPrice = this.pool.spotPrice() 31 | this.spotTick = this.pool.spotTick() 32 | this.impact = opts?.impact || DEFAULT_REBAL_SLIPPAGE 33 | } 34 | 35 | async rebal(): Promise { 36 | const directive = await this.formatDirective() 37 | const cntx = await this.pool.context 38 | const path = cntx.chain.proxyPaths.long 39 | await ensureChain(cntx) 40 | const gasEst = await cntx.dex.userCmd.estimateGas(path, directive.encodeBytes()) 41 | return cntx.dex.userCmd(path, directive.encodeBytes(), { gasLimit: gasEst + GAS_PADDING, chainId: cntx.chain.chainId }) 42 | } 43 | 44 | async simStatic() { 45 | const directive = await this.formatDirective() 46 | const path = (await this.pool.context).chain.proxyPaths.long 47 | return (await this.pool.context).dex.userCmd.staticCall(path, directive.encodeBytes()) 48 | } 49 | 50 | async balancePercent(): Promise { 51 | if (this.mintRange === "ambient") { 52 | return 0.5 // Ambient positions are 50/50 balance 53 | 54 | } else { 55 | const baseQuoteBal = 56 | concDepositBalance(await this.spotPrice, 57 | tickToPrice(this.mintRange[0]), tickToPrice(this.mintRange[1])) 58 | return await this.isBaseOutOfRange() ? 59 | (1.0 - baseQuoteBal) : baseQuoteBal 60 | } 61 | } 62 | 63 | async currentCollateral(): Promise { 64 | const tokenFn = await this.isBaseOutOfRange() ? baseTokenForConcLiq : quoteTokenForConcLiq 65 | return tokenFn(await this.spotPrice, this.liquidity, 66 | tickToPrice(this.burnRange[0]), 67 | tickToPrice(this.burnRange[1])) 68 | } 69 | 70 | async convertCollateral(): Promise { 71 | const balance = await this.swapFraction() 72 | const collat = await this.currentCollateral() 73 | return collat * balance / BigInt(10000) 74 | } 75 | 76 | async postBalance(): Promise<[number, number]> { 77 | const outside = this.mintInput().then(parseFloat) 78 | const inside = this.swapOutput().then(parseFloat) 79 | return await this.isBaseOutOfRange() ? 80 | [await outside, await inside] : 81 | [await inside, await outside] 82 | } 83 | 84 | async mintInput(): Promise { 85 | const collat = (await this.currentCollateral()) - (await this.convertCollateral()) 86 | const pool = this.pool 87 | return await this.isBaseOutOfRange() ? 88 | pool.baseToken.toDisplay(collat) : 89 | pool.quoteToken.toDisplay(collat) 90 | } 91 | 92 | async swapOutput(): Promise { 93 | const [sellToken, buyToken] = await this.pivotTokens() 94 | 95 | const swap = new CrocSwapPlan(sellToken, buyToken, await this.convertCollateral(), 96 | false, this.pool.context, { slippage: this.impact }) 97 | const impact = await swap.calcImpact() 98 | return impact.buyQty 99 | } 100 | 101 | private async isBaseOutOfRange(): Promise { 102 | const spot = await this.spotTick 103 | if (spot >= this.burnRange[1]) { 104 | return true 105 | } else if (spot < this.burnRange[0]) { 106 | return false 107 | } else { 108 | throw new Error("Rebalance position not out of range") 109 | } 110 | } 111 | 112 | private async pivotTokens(): Promise<[CrocTokenView, CrocTokenView]> { 113 | return await this.isBaseOutOfRange() ? 114 | [this.pool.baseToken, this.pool.quoteToken] : 115 | [this.pool.quoteToken, this.pool.baseToken] 116 | } 117 | 118 | private async formatDirective(): Promise { 119 | const [openToken, closeToken] = await this.pivotTokens() 120 | 121 | const directive = new OrderDirective(openToken.tokenAddr) 122 | directive.appendHop(closeToken.tokenAddr) 123 | const pool = directive.appendPool((await this.pool.context).chain.poolIndex) 124 | 125 | directive.appendRangeBurn(this.burnRange[0], this.burnRange[1], this.liquidity) 126 | await this.setupSwap(pool) 127 | 128 | directive.appendPool((await this.pool.context).chain.poolIndex) 129 | 130 | if (this.mintRange === "ambient") { 131 | const mint = directive.appendAmbientMint(BigInt(0)) 132 | mint.rollType = 5 133 | } else { 134 | const mint = directive.appendRangeMint(this.mintRange[0], this.mintRange[1], BigInt(0)) 135 | mint.rollType = 5 136 | } 137 | 138 | directive.open.limitQty = BigInt(0) 139 | directive.hops[0].settlement.limitQty = BigInt(0) 140 | return directive 141 | } 142 | 143 | private async setupSwap (pool: PoolDirective) { 144 | pool.chain.swapDefer = true 145 | pool.swap.rollType = 4 146 | pool.swap.qty = await this.swapFraction() 147 | 148 | const sellBase = await this.isBaseOutOfRange() 149 | pool.swap.isBuy = sellBase 150 | pool.swap.inBaseQty = sellBase 151 | 152 | const priceMult = sellBase ? (1 + this.impact) : (1 - this.impact) 153 | pool.swap.limitPrice = encodeCrocPrice((await this.spotPrice) * priceMult) 154 | } 155 | 156 | private async swapFraction(): Promise { 157 | const swapProp = await this.balancePercent() + this.impact 158 | return BigInt(Math.floor(Math.min(swapProp, 1.0) * 10000)) 159 | } 160 | 161 | pool: CrocPoolView 162 | burnRange: TickRange 163 | mintRange: TickRange | AmbientRange 164 | liquidity: bigint 165 | spotPrice: Promise 166 | spotTick: Promise 167 | impact: number 168 | } 169 | 170 | 171 | type TickRange = [number, number] 172 | 173 | const DEFAULT_REBAL_SLIPPAGE = .02 174 | -------------------------------------------------------------------------------- /src/slots.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers'; 2 | import { CrocContext } from './context'; 3 | 4 | export class CrocSlotReader { 5 | constructor (context: Promise) { 6 | this.provider = context.then(p => p.provider) 7 | this.dex = context.then(c => c.dex.getAddress()) 8 | } 9 | 10 | async isHotPathOpen(): Promise { 11 | const STATE_SLOT = 0; 12 | const HOT_OPEN_OFFSET = 22; 13 | 14 | const hotShiftBits = BigInt(8 * (32 - HOT_OPEN_OFFSET)); 15 | const slot = await this.readSlot(STATE_SLOT); 16 | const slotVal = BigInt(slot); 17 | 18 | return (slotVal << hotShiftBits) >> BigInt(255) > BigInt(0); 19 | } 20 | 21 | async readSlot (slot: number): Promise { 22 | return (await this.provider).getStorage(await this.dex, slot) 23 | } 24 | 25 | async proxyContract (proxyIdx: number): Promise { 26 | const PROXY_SLOT_OFFSET = 1 27 | 28 | const slotVal = await this.readSlot(PROXY_SLOT_OFFSET + proxyIdx) 29 | return "0x" + slotVal.slice(26) 30 | } 31 | 32 | readonly provider: Promise 33 | readonly dex: Promise 34 | } 35 | -------------------------------------------------------------------------------- /src/swap.ts: -------------------------------------------------------------------------------- 1 | import { ethers, TransactionResponse, ZeroAddress } from "ethers"; 2 | import { MAX_SQRT_PRICE, MIN_SQRT_PRICE } from "./constants"; 3 | import { CrocContext, ensureChain } from './context'; 4 | import { CrocSurplusFlags, decodeSurplusFlag, encodeSurplusArg } from "./encoding/flags"; 5 | import { CrocPoolView } from './pool'; 6 | import { CrocSlotReader } from "./slots"; 7 | import { CrocEthView, CrocTokenView, sortBaseQuoteViews, TokenQty } from './tokens'; 8 | import { decodeCrocPrice, GAS_PADDING, getUnsignedRawTransaction } from './utils'; 9 | 10 | /* Describes the predicted impact of a given swap. 11 | * @property sellQty The total quantity of tokens predicted to be sold by the swapper to the dex. 12 | * @property buyQty The total quantity of tokens predicted to be bought by the swapper from the dex. 13 | * @property finalPrice The final price of the pool after the swap. *Note* this is not the same as the 14 | * realized swap price. 15 | * @property percentChange The percent change in the pool price after the swap. Note this is not the same 16 | * as the swapper's slippage against the pool. 17 | */ 18 | export interface CrocImpact { 19 | sellQty: string, 20 | buyQty: string, 21 | finalPrice: number, 22 | percentChange: number 23 | } 24 | 25 | /* Options for the */ 26 | export interface CrocSwapExecOpts { 27 | settlement?: boolean | 28 | { buyDexSurplus: boolean, sellDexSurplus: boolean } 29 | gasEst?: bigint 30 | } 31 | 32 | export interface CrocSwapPlanOpts { 33 | slippage?: number 34 | } 35 | 36 | export class CrocSwapPlan { 37 | 38 | constructor (sellToken: CrocTokenView, buyToken: CrocTokenView, qty: TokenQty, qtyIsBuy: boolean, 39 | context: Promise, opts: CrocSwapPlanOpts = DFLT_SWAP_ARGS) { 40 | [this.baseToken, this.quoteToken] = sortBaseQuoteViews(sellToken, buyToken) 41 | this.sellBase = (this.baseToken === sellToken) 42 | this.qtyInBase = (this.sellBase !== qtyIsBuy) 43 | 44 | this.poolView = new CrocPoolView(this.baseToken, this.quoteToken, context) 45 | const tokenView = this.qtyInBase ? this.baseToken : this.quoteToken 46 | this.qty = tokenView.normQty(qty) 47 | 48 | this.slippage = opts.slippage || DFLT_SWAP_ARGS.slippage 49 | this.priceSlippage = this.slippage * PRICE_SLIP_MULT 50 | this.context = context 51 | 52 | this.impact = this.calcImpact() 53 | this.callType = "" 54 | } 55 | 56 | async swap (args: CrocSwapExecOpts = { }): Promise { 57 | await ensureChain(await this.context); 58 | const gasEst = await this.estimateGas(args) 59 | const callArgs = Object.assign({ gasEst: gasEst, chainId: (await this.context).chain.chainId }, args) 60 | return this.sendTx(Object.assign({}, args, callArgs)) 61 | } 62 | 63 | async simulate (args: CrocSwapExecOpts = { }): Promise { 64 | const gasEst = await this.estimateGas(args) 65 | const callArgs = Object.assign({gasEst: gasEst }, args) 66 | return this.callStatic(Object.assign({}, args, callArgs)) 67 | } 68 | 69 | private async sendTx (args: CrocSwapExecOpts): Promise { 70 | return this.hotPathCall(await this.txBase(), 'send', args) 71 | } 72 | 73 | private async callStatic (args: CrocSwapExecOpts): Promise { 74 | return this.hotPathCall(await this.txBase(), 'staticCall', args) 75 | } 76 | 77 | async estimateGas (args: CrocSwapExecOpts = { }): Promise { 78 | return this.hotPathCall(await this.txBase(), 'estimateGas', args) 79 | } 80 | 81 | private async txBase() { 82 | if (this.callType === "router") { 83 | let router = (await this.context).router 84 | if (!router) { throw new Error("Router not available on network") } 85 | return router 86 | 87 | } else if (this.callType === "bypass" && (await this.context).routerBypass) { 88 | let router = (await this.context).routerBypass 89 | if (!router) { throw new Error("Router not available on network") } 90 | return router || (await this.context).dex 91 | 92 | } else { 93 | return (await this.context).dex 94 | } 95 | } 96 | 97 | private async hotPathCall(contract: ethers.Contract, callType: 'send' | 'staticCall' | 'estimateGas', args: CrocSwapExecOpts) { 98 | const reader = new CrocSlotReader(this.context) 99 | if (this.callType === "router") { 100 | return this.swapCall(contract, callType, args) 101 | } else if (this.callType === "bypass") { 102 | return this.swapCall(contract, callType, args) 103 | } else if (this.callType === "proxy" || (await this.context).chain.proxyPaths.dfltColdSwap) { 104 | return this.userCmdCall(contract, callType, args) 105 | } else { 106 | return await reader.isHotPathOpen() ? 107 | this.swapCall(contract, callType, args) : this.userCmdCall(contract, callType, args) 108 | } 109 | } 110 | 111 | private async swapCall(contract: ethers.Contract, callType: 'send' | 'staticCall' | 'estimateGas', args: CrocSwapExecOpts) { 112 | const TIP = 0 113 | const surplusFlags = this.maskSurplusArgs(args) 114 | 115 | return contract.swap[callType](this.baseToken.tokenAddr, this.quoteToken.tokenAddr, (await this.context).chain.poolIndex, 116 | this.sellBase, this.qtyInBase, await this.qty, TIP, 117 | await this.calcLimitPrice(), await this.calcSlipQty(), surplusFlags, 118 | await this.buildTxArgs(surplusFlags, args.gasEst), ) 119 | } 120 | 121 | private async userCmdCall(contract: ethers.Contract, callType: 'send' | 'staticCall' | 'estimateGas', args: CrocSwapExecOpts) { 122 | const TIP = 0 123 | const surplusFlags = this.maskSurplusArgs(args) 124 | 125 | const HOT_PROXY_IDX = 1 126 | 127 | let abi = new ethers.AbiCoder() 128 | let cmd = abi.encode(["address", "address", "uint256", "bool", "bool", "uint128", "uint16", "uint128", "uint128", "uint8"], 129 | [this.baseToken.tokenAddr, this.quoteToken.tokenAddr, (await this.context).chain.poolIndex, 130 | this.sellBase, this.qtyInBase, await this.qty, TIP, 131 | await this.calcLimitPrice(), await this.calcSlipQty(), surplusFlags]) 132 | 133 | return contract.userCmd[callType](HOT_PROXY_IDX, cmd, await this.buildTxArgs(surplusFlags, args.gasEst)) 134 | } 135 | 136 | /** 137 | * Utility function to generate a "signed" raw transaction for a swap, used for L1 gas estimation on L2's like Scroll. 138 | * Extra 0xFF...F is appended to the unsigned raw transaction to simulate the signature and other missing fields. 139 | * 140 | * Note: This function is only intended for L1 gas estimation, and does not generate valid signed transactions. 141 | */ 142 | async getFauxRawTx (args: CrocSwapExecOpts = { }): Promise<`0x${string}`> { 143 | const TIP = 0 144 | const surplusFlags = this.maskSurplusArgs(args) 145 | 146 | const unsignedTx = await (await this.context).dex.swap.populateTransaction 147 | (this.baseToken.tokenAddr, this.quoteToken.tokenAddr, (await this.context).chain.poolIndex, 148 | this.sellBase, this.qtyInBase, await this.qty, TIP, 149 | await this.calcLimitPrice(), await this.calcSlipQty(), surplusFlags, 150 | await this.buildTxArgs(surplusFlags)) 151 | 152 | // append 160 'f's to the end of the raw transaction to simulate the signature and other missing fields 153 | return getUnsignedRawTransaction(unsignedTx) + "f".repeat(160) as `0x${string}` 154 | } 155 | 156 | async calcImpact(): Promise { 157 | const TIP = 0 158 | const limitPrice = this.sellBase ? MAX_SQRT_PRICE : MIN_SQRT_PRICE 159 | 160 | const impact = await (await this.context).slipQuery.calcImpact.staticCall 161 | (this.baseToken.tokenAddr, this.quoteToken.tokenAddr, (await this.context).chain.poolIndex, 162 | this.sellBase, this.qtyInBase, await this.qty, TIP, limitPrice); 163 | 164 | if ((impact[0] > 0 && impact[1] > 0) || (impact[0] < 0 && impact[1] < 0)) 165 | throw new Error("Invalid impact: base and quote flows have matching signs") 166 | 167 | const baseQty = this.baseToken.toDisplay(impact[0] < 0 ? -impact[0] : impact[0]) 168 | const quoteQty = this.quoteToken.toDisplay(impact[1] < 0 ? -impact[1] : impact[1]) 169 | const spotPrice = decodeCrocPrice(impact[2]) 170 | 171 | const startPrice = this.poolView.displayPrice() 172 | const finalPrice = this.poolView.toDisplayPrice(spotPrice) 173 | 174 | const ret = { 175 | sellQty: this.sellBase ? await baseQty : await quoteQty, 176 | buyQty: this.sellBase ? await quoteQty : await baseQty, 177 | finalPrice: await finalPrice, 178 | percentChange: (await finalPrice - await startPrice) / await startPrice 179 | } 180 | return ret 181 | } 182 | 183 | private maskSurplusArgs (args?: CrocSwapExecOpts): number { 184 | return encodeSurplusArg(this.maskSurplusFlags(args)) 185 | } 186 | 187 | private maskSurplusFlags (args?: CrocSwapExecOpts): CrocSurplusFlags { 188 | if (!args || !args.settlement) { 189 | return [false, false] 190 | } else if (typeof args.settlement === "boolean") { 191 | return [args.settlement, args.settlement] 192 | } else { 193 | return this.sellBase ? 194 | [args.settlement.sellDexSurplus, args.settlement.buyDexSurplus] : 195 | [args.settlement.buyDexSurplus, args.settlement.sellDexSurplus] 196 | } 197 | } 198 | 199 | private async buildTxArgs (surplusArg: number, gasEst?: bigint) { 200 | const txArgs = await this.attachEthMsg(surplusArg) 201 | 202 | if (gasEst) { 203 | Object.assign(txArgs, { gasLimit: gasEst + GAS_PADDING }); 204 | } 205 | 206 | return txArgs 207 | } 208 | 209 | private async attachEthMsg (surplusEncoded: number): Promise { 210 | // Only need msg.val if one token is native ETH (will always be base side) 211 | if (!this.sellBase || this.baseToken.tokenAddr !== ZeroAddress) { return { }} 212 | 213 | // Calculate the maximum amount of ETH we'll need. If on the floating side 214 | // account for potential slippage. (Contract will refund unused ETH) 215 | const val = this.qtyInBase ? this.qty : this.calcSlipQty() 216 | 217 | if (decodeSurplusFlag(surplusEncoded)[0]) { 218 | // If using surplus calculate the amount of ETH not covered by the surplus 219 | // collateral. 220 | const needed = new CrocEthView(this.context).msgValOverSurplus(await val) 221 | return { value: await needed } 222 | 223 | } else { 224 | // Othwerise we need to send the entire balance in msg.val 225 | return { value: await val} 226 | } 227 | } 228 | 229 | async calcSlipQty(): Promise { 230 | const qtyIsBuy = (this.sellBase === this.qtyInBase) 231 | 232 | const slipQty = !qtyIsBuy ? 233 | parseFloat((await this.impact).sellQty) * (1 + this.slippage) : 234 | parseFloat((await this.impact).buyQty) * (1 - this.slippage) 235 | 236 | return !this.qtyInBase ? 237 | this.baseToken.roundQty(slipQty) : 238 | this.quoteToken.roundQty(slipQty) 239 | } 240 | 241 | async calcLimitPrice(): Promise { 242 | return this.sellBase ? MAX_SQRT_PRICE : MIN_SQRT_PRICE 243 | } 244 | 245 | forceProxy(): CrocSwapPlan { 246 | this.callType = "proxy" 247 | return this 248 | } 249 | 250 | useRouter(): CrocSwapPlan { 251 | this.callType = "router" 252 | return this 253 | } 254 | 255 | useBypass(): CrocSwapPlan { 256 | this.callType = "bypass" 257 | return this 258 | } 259 | 260 | readonly baseToken: CrocTokenView 261 | readonly quoteToken: CrocTokenView 262 | readonly qty: Promise 263 | readonly sellBase: boolean 264 | readonly qtyInBase: boolean 265 | readonly slippage: number 266 | readonly priceSlippage: number 267 | readonly poolView: CrocPoolView 268 | readonly context: Promise 269 | readonly impact: Promise 270 | private callType: string 271 | } 272 | 273 | // Price slippage limit multiplies normal slippage tolerance by amount that should 274 | // be reasonable (300%) 275 | const PRICE_SLIP_MULT = 3.0 276 | 277 | // Default slippage is set to 1%. User should evaluate this carefully for low liquidity 278 | // pools of when swapping large amounts. 279 | const DFLT_SWAP_ARGS = { 280 | slippage: 0.01 281 | } 282 | -------------------------------------------------------------------------------- /src/tokens.ts: -------------------------------------------------------------------------------- 1 | import { Contract, ethers, MaxUint256, TransactionResponse, ZeroAddress } from "ethers"; 2 | import { MAX_LIQ } from "./constants"; 3 | import { CrocContext, ensureChain } from "./context"; 4 | import { BlockTag } from "./position"; 5 | import { GAS_PADDING } from "./utils"; 6 | import { fromDisplayQty, toDisplayQty } from "./utils/token"; 7 | 8 | /* Type representing specified token quantities. This type can either represent the raw non-decimalized 9 | * on-chain value in wei, if passed as a BigNuber. Or it can represent the decimalized value if passed 10 | * as a string or Javascript float. */ 11 | export type TokenQty = bigint | string | number; 12 | 13 | /* General top-level class for interacting with specific ERC20 tokens. Handles functionality for 14 | * approval, getting token balances both in wallet and on dex, and display/decimalization. */ 15 | export class CrocTokenView { 16 | 17 | /* Creates a new CrocTokenView for specificied token address. 18 | * 19 | * @param context The CrocContext environment context. Specific to a given chain. 20 | * @param tokenAddr The address of the token contract. Use zero address for native ETH token. */ 21 | constructor(context: Promise, tokenAddr: string) { 22 | this.context = context; 23 | this.tokenAddr = tokenAddr; 24 | this.isNativeEth = tokenAddr == ZeroAddress; 25 | if (this.isNativeEth) { 26 | this.decimals = Promise.resolve(18); 27 | } else { 28 | this.decimals = this.resolve().then(async (c) => c.decimals().then(Number)); 29 | } 30 | } 31 | 32 | /* Sends a signed transaction to approve the CrocSwap contract for the ERC20 token contract. 33 | * 34 | * @param approveQty Optional arugment to specify the quantity to approve. Defaults to 2^120 35 | * if unspecified. */ 36 | async approve (approveQty?: TokenQty): Promise { 37 | return this.approveAddr(await (await this.context).dex.getAddress(), approveQty) 38 | } 39 | 40 | async approveRouter (approveQty?: TokenQty): Promise { 41 | let router = (await this.context).router 42 | return router && this.approveAddr(await router.getAddress(), approveQty) 43 | } 44 | 45 | async approveAddr (addr: string, approveQty?: TokenQty): Promise { 46 | if (this.isNativeEth) { 47 | return undefined; 48 | } 49 | 50 | const weiQty = approveQty !== undefined ? await this.normQty(approveQty) : MaxUint256 51 | 52 | await ensureChain(await this.context) 53 | // We want to hardcode the gas limit, so we can manually pad it from the estimated 54 | // transaction. The default value is low gas calldata, but Metamask and other wallets 55 | // will often ask users to change the approval amount. Without the padding, approval 56 | // transactions can run out of gas. 57 | const gasEst = (await this.resolveWrite()).approve.estimateGas( 58 | addr, 59 | weiQty 60 | ); 61 | 62 | return (await this.resolveWrite()).approve( 63 | addr, weiQty, { gasLimit: (await gasEst) + BigInt(15000), chainId: ((await this.context).chain).chainId } 64 | ); 65 | } 66 | 67 | async approveBypassRouter(): Promise { 68 | let router = (await this.context).router 69 | if (!router) { 70 | return undefined 71 | } 72 | 73 | let abiCoder = new ethers.AbiCoder() 74 | const MANY_CALLS = 1000000000 75 | const HOT_PROXY_IDX = 1 76 | const COLD_PROXY_IDX = 3 77 | const cmd = abiCoder.encode(["uint8", "address", "uint32", "uint16[]"], 78 | [72, router.address, MANY_CALLS, [HOT_PROXY_IDX]]) 79 | await ensureChain(await this.context) 80 | return (await this.context).dex.userCmd(COLD_PROXY_IDX, cmd, { chainId: ((await this.context).chain).chainId }) 81 | } 82 | 83 | async wallet (address: string, block: BlockTag = "latest"): Promise { 84 | if (this.isNativeEth) { 85 | return (await this.context).provider.getBalance(address, block); 86 | } else { 87 | return (await this.resolve()).balanceOf(address, { blockTag: block }); 88 | } 89 | } 90 | 91 | async walletDisplay (address: string, block: BlockTag = "latest"): Promise { 92 | const balance = this.wallet(address, block); 93 | return toDisplayQty(await balance, await this.decimals); 94 | } 95 | 96 | async balance (address: string, block: BlockTag = "latest"): Promise { 97 | return (await this.context).query.querySurplus(address, this.tokenAddr, { blockTag: block }) 98 | } 99 | 100 | async balanceDisplay (address: string, block: BlockTag = "latest"): Promise { 101 | const balance = this.balance(address, block); 102 | return toDisplayQty(await balance, await this.decimals); 103 | } 104 | 105 | async allowance(address: string, spender?: string): Promise { 106 | if (this.isNativeEth) { 107 | return MAX_LIQ; 108 | } 109 | return (await this.resolve()).allowance( 110 | address, 111 | spender ? spender : await (await this.context).dex.getAddress() 112 | ); 113 | } 114 | 115 | async roundQty (qty: TokenQty): Promise { 116 | if (typeof qty === "number" || typeof qty === "string") { 117 | return this.normQty(this.truncFraction(qty, await this.decimals)) 118 | } else { 119 | return qty; 120 | } 121 | } 122 | 123 | private truncFraction (qty: string | number, decimals: number): number { 124 | if (typeof(qty) === "number") { 125 | const exp = Math.pow(10, decimals) 126 | return Math.floor(qty * exp) / exp 127 | } else { 128 | return this.truncFraction(parseFloat(qty), decimals) 129 | } 130 | } 131 | 132 | async normQty(qty: TokenQty): Promise { 133 | if (typeof qty === "number" || typeof qty === "string") { 134 | return fromDisplayQty(qty.toString(), await this.decimals); 135 | } else { 136 | return qty; 137 | } 138 | } 139 | 140 | async toDisplay(qty: TokenQty): Promise { 141 | if (typeof qty === "number" || typeof qty === "string") { 142 | return qty.toString(); 143 | } else { 144 | return toDisplayQty(qty, await this.decimals); 145 | } 146 | } 147 | 148 | private async resolve(): Promise { 149 | return (await this.context).erc20Read.attach(this.tokenAddr) as Contract; 150 | } 151 | 152 | private async resolveWrite(): Promise { 153 | return (await this.context).erc20Write.attach(this.tokenAddr) as Contract; 154 | } 155 | 156 | async deposit (qty: TokenQty, recv: string): Promise { 157 | return this.surplusOp(73, qty, recv, this.isNativeEth) 158 | } 159 | 160 | async withdraw (qty: TokenQty, recv: string): Promise { 161 | return this.surplusOp(74, qty, recv) 162 | } 163 | 164 | async transfer (qty: TokenQty, recv: string): Promise { 165 | return this.surplusOp(75, qty, recv) 166 | } 167 | 168 | private async surplusOp (subCode: number, qty: TokenQty, recv: string, 169 | useMsgVal: boolean = false): Promise { 170 | const abiCoder = new ethers.AbiCoder() 171 | const weiQty = this.normQty(qty) 172 | const cmd = abiCoder.encode(["uint8", "address", "uint128", "address"], 173 | [subCode, recv, await weiQty, this.tokenAddr]) 174 | 175 | const txArgs = useMsgVal ? { value: await weiQty } : { } 176 | let cntx = await this.context 177 | await ensureChain(cntx) 178 | const gasEst = await cntx.dex.userCmd.estimateGas(cntx.chain.proxyPaths.cold, cmd, txArgs) 179 | Object.assign(txArgs, { gasLimit: gasEst + GAS_PADDING, chainId: cntx.chain.chainId }) 180 | return cntx.dex.userCmd(cntx.chain.proxyPaths.cold, cmd, txArgs) 181 | } 182 | 183 | readonly tokenAddr: string; 184 | readonly context: Promise; 185 | readonly decimals: Promise; 186 | readonly isNativeEth: boolean; 187 | } 188 | 189 | export class CrocEthView extends CrocTokenView { 190 | constructor (context: Promise) { 191 | super(context, ZeroAddress) 192 | } 193 | 194 | /* Returns the amount needed to attach to msg.value when spending 195 | * ETH from surplus collateral. (I.e. the difference between the 196 | * two, or 0 if surplus collateral is sufficient) */ 197 | async msgValOverSurplus (ethNeeded: bigint): Promise { 198 | const sender = (await this.context).senderAddr 199 | 200 | if (!sender) { 201 | console.warn("No sender address known, returning 0") 202 | return BigInt(0) 203 | } 204 | 205 | const ethView = new CrocTokenView(this.context, ZeroAddress) 206 | const surpBal = await ethView.balance(sender) 207 | 208 | const hasEnough = surpBal > ethNeeded 209 | return hasEnough ? BigInt(0) : 210 | ethNeeded - surpBal 211 | } 212 | } 213 | 214 | export function sortBaseQuoteViews (tokenA: CrocTokenView, tokenB: CrocTokenView): 215 | [CrocTokenView, CrocTokenView] { 216 | return tokenA.tokenAddr.toLowerCase() < tokenB.tokenAddr.toLowerCase() ? 217 | [tokenA, tokenB] : [tokenB, tokenA] 218 | } 219 | -------------------------------------------------------------------------------- /src/utils/gas.ts: -------------------------------------------------------------------------------- 1 | import { Contract, ContractTransaction, Transaction, keccak256 } from "ethers"; 2 | import { L1_GAS_PRICE_ORACLE_ABI } from "../abis/external/L1GasPriceOracle"; 3 | import { CrocEnv } from "../croc"; 4 | 5 | // Applied to all gas estimates. 6 | export const GAS_PADDING = BigInt(30000); 7 | 8 | /** 9 | * Compute the raw transaction data for a given transaction. 10 | * 11 | * ref: https://docs.ethers.org/v5/cookbook/transactions/#cookbook--compute-raw-transaction 12 | */ 13 | export function getRawTransaction(tx: Transaction) { 14 | // Serialize the signed transaction 15 | const raw = Transaction.from(tx).serialized; 16 | 17 | // Double check things went well 18 | if (keccak256(raw) !== tx.hash) { throw new Error("serializing failed!"); } 19 | 20 | return raw as `0x${string}`; 21 | } 22 | 23 | /** 24 | * Compute the raw transaction data for a given transaction without the signature. 25 | * 26 | * ref: https://docs.ethers.org/v5/cookbook/transactions/#cookbook--compute-raw-transaction 27 | */ 28 | export function getUnsignedRawTransaction(tx: ContractTransaction) { 29 | // Serialize the signed transaction 30 | const raw = Transaction.from(tx).unsignedSerialized; 31 | 32 | return raw as `0x${string}`; 33 | } 34 | 35 | /** 36 | * Estimates the additional L1 gas on Scroll for any data which is a RLP-encoded transaction with signature. 37 | */ 38 | export async function estimateScrollL1Gas(crocEnv: CrocEnv, rawTransaction: `0x${string}`): Promise { 39 | const crocContext = await crocEnv.context; 40 | const chainId = crocContext.chain.chainId; 41 | const isScroll = chainId === "0x82750" || chainId === "0x8274f"; 42 | if (!isScroll) { 43 | return BigInt(0); 44 | } 45 | 46 | const L1_GAS_PRICE_ORACLE_ADDRESS = "0x5300000000000000000000000000000000000002"; 47 | const l1GasPriceOracle = new Contract(L1_GAS_PRICE_ORACLE_ADDRESS, L1_GAS_PRICE_ORACLE_ABI, crocContext.provider); 48 | 49 | // function getL1Fee(bytes memory _data) external view override returns (uint256); 50 | const l1Gas = await l1GasPriceOracle.getL1Fee(rawTransaction); 51 | return l1Gas; 52 | } 53 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./math"; 2 | export * from "./price"; 3 | export * from "./token"; 4 | export * from "./liquidity"; 5 | export * from "./gas"; 6 | -------------------------------------------------------------------------------- /src/utils/liquidity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | bigIntToFloat, 3 | floatToBigInt, 4 | truncateRightBits, 5 | } from "./"; 6 | 7 | /* Converts a fixed base token collateral amount to pool liquidity units. This conversion only applies 8 | * to the current pool price. If price moves the ratio between token collateral and liquidity will also 9 | * change. Note that this function will only work when token qty or liquidity is less than 2^64 10 | 11 | * @param price The current (non-display) price ratio in the pool. 12 | * @param qty The quantity (in non-display wei) of base token to convert 13 | * @return The amount of virtual liquidity (in sqrt(X*Y)) supported by this base token quantity. */ 14 | export function liquidityForBaseQty( 15 | price: number, 16 | qty: bigint, 17 | mult: number = 1.0 18 | ): bigint { 19 | return floatToBigInt( 20 | Math.floor((bigIntToFloat(qty) / Math.sqrt(price)) * mult) 21 | ); 22 | } 23 | 24 | /* Converts a fixed quote token collateral amount to pool liquidity units. This conversion only applies 25 | * to the current pool price. If price moves the ratio between token collateral and liquidity will also 26 | * change. Note that this function will only work when token qty or liquidity is less than 2^64 27 | * 28 | * @param price The current (non-display) price ratio in the pool. 29 | * @param qty The quantity (in non-display wei) of quote token to convert 30 | * @return The amount of virtual liquidity (in sqrt(X*Y)) supported by this quote token quantity. */ 31 | export function liquidityForQuoteQty( 32 | price: number, 33 | qty: bigint, 34 | mult = 1.0 35 | ): bigint { 36 | return floatToBigInt( 37 | Math.floor(bigIntToFloat(qty) * Math.sqrt(price) * mult) 38 | ); 39 | } 40 | 41 | export function baseVirtualReserves( 42 | price: number, 43 | liq: bigint, 44 | mult: number = 1.0 45 | ): bigint { 46 | return floatToBigInt(bigIntToFloat(liq) * Math.sqrt(price) * mult); 47 | } 48 | 49 | export function quoteVirtualReserves( 50 | price: number, 51 | liq: bigint, 52 | mult: number = 1.0 53 | ): bigint { 54 | return floatToBigInt((bigIntToFloat(liq) / Math.sqrt(price)) * mult); 55 | } 56 | 57 | /* Converts a fixed amount of base token deposits to liquidity for a concentrated range order 58 | * 59 | * @param price The current (non-display) price ratio in the pool. 60 | * @param qty The quantity (in non-display wei) of base token to convert 61 | * @param lower The lower boundary price of the range order 62 | * @param upper The upper boundary price of the range order 63 | * @return The amount of virtual liquidity (in sqrt(X*Y)) supported by this base token quantity. */ 64 | export function liquidityForBaseConc( 65 | price: number, 66 | qty: bigint, 67 | lower: number, 68 | upper: number 69 | ): bigint { 70 | const concFactor = baseConcFactor(price, lower, upper); 71 | return liquidityForBaseQty(price, qty, concFactor); 72 | } 73 | 74 | /* Converts a fixed amount of quote token deposits to liquidity for a concentrated range order 75 | * 76 | * @param price The current (non-display) price ratio in the pool. 77 | * @param qty The quantity (in non-display wei) of base token to convert 78 | * @param lower The lower boundary price of the range order 79 | * @param upper The upper boudnary price of the range order 80 | * @return The amount of virtual liquidity (in sqrt(X*Y)) supported by this quote token quantity. */ 81 | export function liquidityForQuoteConc( 82 | price: number, 83 | qty: bigint, 84 | lower: number, 85 | upper: number 86 | ): bigint { 87 | const concFactor = quoteConcFactor(price, lower, upper); 88 | return liquidityForQuoteQty(price, qty, concFactor); 89 | } 90 | 91 | export function baseTokenForConcLiq( 92 | price: number, 93 | liq: bigint, 94 | lower: number, 95 | upper: number 96 | ): bigint { 97 | const concFactor = baseConcFactor(price, lower, upper); 98 | return baseVirtualReserves(price, liq, 1 / concFactor); 99 | } 100 | 101 | export function quoteTokenForConcLiq( 102 | price: number, 103 | liq: bigint, 104 | lower: number, 105 | upper: number 106 | ): bigint { 107 | const concFactor = quoteConcFactor(price, lower, upper); 108 | return quoteVirtualReserves(price, liq, 1 / concFactor); 109 | } 110 | 111 | export function baseTokenForQuoteConc (baseQty: number, 112 | lower: number, upper: number): number { 113 | const growth = Math.sqrt(upper/lower) - 1 114 | const virtBase = baseQty / growth; 115 | const virtQuote = virtBase / lower 116 | return virtQuote * (1 / (1 - growth) - 1) 117 | } 118 | 119 | export function quoteTokenForBaseConc (quoteQty: number, 120 | lower: number, upper: number): number { 121 | return baseTokenForQuoteConc(quoteQty, 1/upper, 1/lower) 122 | } 123 | 124 | /* Calculates the concentration leverage factor for the base token given the range relative to 125 | * the current price in the pool. 126 | * 127 | * @param price The current price of the pool 128 | * @param lower The lower price boundary of the range order 129 | * @param upper The upper price boundary of the range order 130 | * @return The fraction of base tokens needed relative to an ambient position with the same 131 | * liquidity */ 132 | export function baseConcFactor( 133 | price: number, 134 | lower: number, 135 | upper: number 136 | ): number { 137 | if (price < lower) { 138 | return Infinity; 139 | } else if (price > upper) { 140 | return Math.sqrt(price) / (Math.sqrt(upper) - Math.sqrt(lower)); 141 | } else { 142 | return 1 / (1 - Math.sqrt(lower) / Math.sqrt(price)); 143 | } 144 | } 145 | 146 | /* Calculates the concentration leverage factor for the quote token given the range relative to 147 | * the current price in the pool. 148 | * 149 | * @param price The current price of the pool 150 | * @param lower The lower price boundary of the range order 151 | * @param upper The upper price boundary of the range order 152 | * @return The fraction of quote tokens needed relative to an ambient position with the same 153 | * liquidity */ 154 | export function quoteConcFactor( 155 | price: number, 156 | lower: number, 157 | upper: number 158 | ): number { 159 | return baseConcFactor(1 / price, 1 / upper, 1 / lower); 160 | } 161 | 162 | /* Calculates the deposit ratio multiplier for a concentrated liquidity range order. 163 | * 164 | * @param price The current price of the pool 165 | * @param lower The lower price boundary of the range order 166 | * @param upper The upper price boundary of the range order 167 | * @return The ratio of base to quote token deposit amounts for this concentrated range 168 | * order *relative* to full-range ambient deposit ratio. */ 169 | export function concDepositSkew( 170 | price: number, 171 | lower: number, 172 | upper: number 173 | ): number { 174 | const base = baseConcFactor(price, lower, upper); 175 | const quote = quoteConcFactor(price, lower, upper); 176 | 177 | return quote / base; 178 | } 179 | 180 | export function concDepositBalance( 181 | price: number, 182 | lower: number, 183 | upper: number 184 | ): number { 185 | const base = baseConcFactor(price, lower, upper); 186 | const quote = quoteConcFactor(price, lower, upper); 187 | 188 | return quote / (base + quote); 189 | } 190 | 191 | export function capitalConcFactor( 192 | price: number, 193 | lower: number, 194 | upper: number 195 | ): number { 196 | const base = 1 / baseConcFactor(price, lower, upper); 197 | const quote = 1 / quoteConcFactor(price, lower, upper); 198 | return 1 / ((base + quote) / 2.0) 199 | } 200 | 201 | export function concBaseSlippagePrice (spotPrice: number, upperPrice: number, slippage: number): number { 202 | const delta = Math.sqrt(upperPrice) - Math.sqrt(spotPrice) 203 | const lowerSqrt = Math.sqrt(upperPrice) - delta * (1 + slippage) 204 | return Math.pow(lowerSqrt, 2) 205 | } 206 | 207 | export function concQuoteSlippagePrice (spotPrice: number, lowerPrice: number, slippage: number): number { 208 | const delta = Math.sqrt(spotPrice) - Math.sqrt(lowerPrice) 209 | const upperSqrt = ((1 + slippage) * delta) + Math.sqrt(lowerPrice) 210 | return Math.pow(upperSqrt, 2) 211 | } 212 | 213 | /* Rounds a liquidity magnitude to a multiple that can be used inside the protocol. */ 214 | export function roundForConcLiq(liq: bigint): bigint { 215 | const CONC_LOTS_BITS = 11; 216 | return truncateRightBits(liq, CONC_LOTS_BITS); 217 | } 218 | -------------------------------------------------------------------------------- /src/utils/math.ts: -------------------------------------------------------------------------------- 1 | export function toFixedNumber(num: number, digits: number, base?: number) { 2 | const pow = Math.pow(base || 10, digits); 3 | return Math.round(num * pow) / pow; 4 | } 5 | 6 | export function bigIntToFloat (val: bigint): number { 7 | return val < BigInt(Number.MAX_SAFE_INTEGER - 1) 8 | ? Number(val) 9 | : parseFloat(val.toString()); 10 | } 11 | 12 | export function floatToBigInt (x: number): bigint { 13 | let floatPrice = x 14 | let scale = 0; 15 | 16 | const PRECISION_BITS = 16; 17 | while (floatPrice > Number.MAX_SAFE_INTEGER) { 18 | floatPrice = floatPrice / (2 ** PRECISION_BITS); 19 | scale = scale + PRECISION_BITS; 20 | } 21 | 22 | const pinPrice = Math.round(floatPrice); 23 | const mult = BigInt(2) ** BigInt(scale) 24 | return BigInt(pinPrice) * mult; 25 | } 26 | 27 | export function truncateRightBits(x: bigint, bits: number): bigint { 28 | const mult = BigInt(2) ** BigInt(bits) 29 | return x / mult * mult 30 | } 31 | 32 | export function fromFixedGrowth (x: bigint): number { 33 | return 1 + bigIntToFloat(x) / (2 ** 48) 34 | } 35 | -------------------------------------------------------------------------------- /src/utils/price.ts: -------------------------------------------------------------------------------- 1 | import { MAX_TICK, MIN_TICK } from "../constants"; 2 | 3 | type Tick = number; 4 | 5 | export function encodeCrocPrice(price: number): bigint { 6 | let floatPrice = Math.sqrt(price) * 2 ** 64; 7 | let scale = BigInt(0); 8 | 9 | const PRECISION_BITS = 16; 10 | while (floatPrice > Number.MAX_SAFE_INTEGER) { 11 | floatPrice = floatPrice / 2 ** PRECISION_BITS; 12 | scale = scale + BigInt(PRECISION_BITS); 13 | } 14 | 15 | const pinPrice = Math.round(floatPrice); 16 | const bnSeed = BigInt(pinPrice); 17 | 18 | return bnSeed * (BigInt(2) ** scale); 19 | } 20 | 21 | export function decodeCrocPrice(val: bigint) { 22 | const x = val < (Number.MAX_SAFE_INTEGER - 1) 23 | ? Number(val) 24 | : parseFloat(val.toString()); 25 | const sq = x / 2 ** 64; 26 | return sq * sq; 27 | } 28 | 29 | export function toDisplayPrice( 30 | price: number | bigint, 31 | baseDecimals: number, 32 | quoteDecimals: number, 33 | isInverted = false 34 | ): number { 35 | const scaled = Number(price) * Math.pow(10, Number(quoteDecimals) - Number(baseDecimals)) 36 | return isInverted ? 1 / scaled : scaled 37 | } 38 | 39 | export function fromDisplayPrice( 40 | price: number, 41 | baseDecimals: number, 42 | quoteDecimals: number, 43 | isInverted = false 44 | ): number { 45 | const scaled = isInverted ? 1 / price : price 46 | return scaled * Math.pow(10, baseDecimals - quoteDecimals) 47 | } 48 | 49 | export function pinTickLower( 50 | price: number, 51 | nTicksGrid: number): Tick { 52 | const priceInTicks = Math.log(price) / Math.log(1.0001); 53 | const tickGrid = Math.floor(priceInTicks / nTicksGrid) * nTicksGrid; 54 | const horizon = Math.floor(MIN_TICK / nTicksGrid) * nTicksGrid; 55 | return Math.max(tickGrid, horizon); 56 | } 57 | 58 | export function priceHalfBelowTick( 59 | tick: number, 60 | nTicksGrid: number): Tick { 61 | const halfTickBelow = (tick - (.5 * nTicksGrid)) 62 | return Math.pow(1.0001, halfTickBelow); 63 | } 64 | 65 | export function pinTickUpper( 66 | price: number, 67 | nTicksGrid: number): Tick { 68 | const priceInTicks = priceToTick(price) 69 | const tickGrid = Math.ceil(priceInTicks / nTicksGrid) * nTicksGrid; 70 | const horizon = Math.ceil(MAX_TICK / nTicksGrid) * nTicksGrid; 71 | return Math.min(tickGrid, horizon); 72 | } 73 | 74 | 75 | /* Returns the closest on-grid tick tick that's to the outside of a given price 76 | * relative to a pool price. */ 77 | export function pinTickOutside( 78 | price: number, 79 | poolPrice: number, 80 | nTicksGrid: number): { tick: Tick, isTickBelow: boolean } { 81 | 82 | const priceInTicks = priceToTick(price) 83 | const poolInTicks = priceToTick(poolPrice) 84 | const [poolLower, poolUpper] = 85 | [pinTickLower(poolPrice, nTicksGrid), pinTickUpper(poolPrice, nTicksGrid)] 86 | 87 | if (priceInTicks < poolInTicks) { 88 | if (priceInTicks >= poolLower) { 89 | return { tick: poolLower - nTicksGrid, isTickBelow: true } 90 | } else { 91 | return { tick: pinTickLower(price, nTicksGrid), isTickBelow: true } 92 | } 93 | 94 | } else { 95 | if (priceInTicks <= poolUpper) { 96 | return { tick: poolUpper + nTicksGrid, isTickBelow: false } 97 | } else { 98 | return { tick: pinTickUpper(price, nTicksGrid), isTickBelow: false } 99 | } 100 | } 101 | } 102 | 103 | 104 | /* Returns the neighboring N on-grid ticks to a given price. Ticks will be 105 | * sorted from closest to furthers */ 106 | export function neighborTicks (price: number, nTicksGrid: number, 107 | nNeighbors: number = 1): { 108 | below: number[], above: number[] } { 109 | const priceInTicks = pinTickLower(price, nTicksGrid) 110 | 111 | return { 112 | below: Array.from({length: nNeighbors}). 113 | map((_, idx: number) => priceInTicks - idx * nTicksGrid), 114 | above: Array.from({length: nNeighbors}). 115 | map((_, idx: number) => priceInTicks + (idx + 1) * nTicksGrid) 116 | } 117 | } 118 | 119 | export function priceToTick (price: number): Tick { 120 | return Math.floor(Math.log(price) / Math.log(1.0001)) 121 | } 122 | 123 | export function tickToPrice(tick: Tick): number { 124 | return Math.pow(1.0001, tick); 125 | } 126 | 127 | export function priceHalfAboveTick( 128 | tick: number, 129 | nTicksGrid: number): Tick { 130 | const halfTickAbove = (tick + (.5 * nTicksGrid)) 131 | return Math.pow(1.0001, halfTickAbove); 132 | } 133 | 134 | /* Returns the ratio of quote to base tokens necessary to support the collateral for a given 135 | * range order over the specified ticks. If no quote token collateral is required returns 0 136 | * if no base token collateral is required returns Infinity */ 137 | export function calcRangeTilt( 138 | mktPrice: number, 139 | lowerTick: Tick, 140 | upperTick: Tick 141 | ): number { 142 | const lowerPrice = tickToPrice(lowerTick); 143 | const upperPrice = tickToPrice(upperTick); 144 | 145 | if (mktPrice > upperPrice) { 146 | return Infinity; 147 | } else if (mktPrice < lowerPrice) { 148 | return 0; 149 | } else { 150 | const basePartial = Math.sqrt(lowerPrice / mktPrice); 151 | const quotePartial = Math.sqrt(mktPrice / upperPrice); 152 | return quotePartial / basePartial; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/utils/token.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "ethers"; 2 | 3 | export function getBaseTokenAddress(token1: string, token2: string): string { 4 | let baseTokenAddress = ""; 5 | 6 | if (!!token1 && !!token2) { 7 | const token1BigNum = BigInt(token1); 8 | const token2BigNum = BigInt(token2); 9 | // On-chain base token is always the smaller of the two 10 | baseTokenAddress = token1BigNum < token2BigNum ? token1 : token2; 11 | } 12 | return baseTokenAddress; 13 | } 14 | 15 | export function getQuoteTokenAddress(token1: string, token2: string): string { 16 | let quoteTokenAddress = ""; 17 | 18 | if (!!token1 && !!token2) { 19 | const token1BigNum = BigInt(token1); 20 | const token2BigNum = BigInt(token2); 21 | // On-chain quote token is always the larger of the two 22 | quoteTokenAddress = token1BigNum > token2BigNum ? token1 : token2; 23 | } 24 | return quoteTokenAddress; 25 | } 26 | 27 | export function sortBaseQuoteTokens( 28 | token1: string, 29 | token2: string 30 | ): [string, string] { 31 | return [ 32 | getBaseTokenAddress(token1, token2), 33 | getQuoteTokenAddress(token1, token2), 34 | ]; 35 | } 36 | 37 | export function fromDisplayQty(qty: string, tokenDecimals: number): bigint { 38 | try { 39 | // First try to directly parse the string, so there's no loss of precision for 40 | // long fixed strings. 41 | return ethers.parseUnits(qty, tokenDecimals); 42 | 43 | } catch { 44 | // If that fails (e.g. with scientific notation floats), then cast to float and 45 | // back to fixed string 46 | const sanitQty = parseFloat(qty).toFixed(tokenDecimals) 47 | return ethers.parseUnits(sanitQty, tokenDecimals) 48 | } 49 | } 50 | 51 | export function toDisplayQty( 52 | qty: string | number | bigint, 53 | tokenDecimals: number 54 | ): string { 55 | 56 | // formatUnits is temperamental with Javascript numbers, so convert string to 57 | // fullwide string to avoid scientific notation (which BigInt pukes on) 58 | if (typeof(qty) === "number" ) { 59 | const qtyString = qty.toLocaleString('fullwide', {useGrouping:false}) 60 | return toDisplayQty(qtyString, tokenDecimals) 61 | } 62 | 63 | return ethers.formatUnits(qty, tokenDecimals); 64 | } 65 | -------------------------------------------------------------------------------- /src/vaults/tempest.ts: -------------------------------------------------------------------------------- 1 | import { Contract, Signer, TransactionResponse, Typed } from "ethers"; 2 | import { TEMPEST_VAULT_ABI } from "../abis/external/TempestVaultAbi"; 3 | import { CrocContext, ensureChain } from "../context"; 4 | import { CrocTokenView, TokenQty } from "../tokens"; 5 | 6 | export type TempestStrategy = 'rswEth' | 'symetricAmbient' 7 | 8 | /* @notice Class for interacting with a specific Tempest pair vault. */ 9 | export class TempestVault { 10 | constructor (vaultToken: CrocTokenView, token1: CrocTokenView, strategy: TempestStrategy, context: Promise) { 11 | this.vaultAddr = vaultToken.tokenAddr 12 | this.token1 = token1 13 | this.vaultToken = vaultToken 14 | this.strategy = strategy 15 | this.vaultWrite = context.then(c => new Contract(this.vaultAddr, TEMPEST_VAULT_ABI, c.actor)); 16 | this.vaultRead = context.then(c => new Contract(this.vaultAddr, TEMPEST_VAULT_ABI, c.provider)); 17 | this.context = context 18 | } 19 | 20 | /* @notice Sends a transaction to zap and deposit token1 into the vault 21 | * @param qty The quantity of token1 to deposit */ 22 | async depositZap (qty: TokenQty): Promise { 23 | let owner = ((await this.context).actor as Signer).getAddress() 24 | let weiQty = this.token1.normQty(qty); 25 | let txArgs = {}; 26 | if (this.token1.isNativeEth) { 27 | txArgs = { value: await weiQty }; 28 | } 29 | await ensureChain(await this.context) 30 | switch (this.strategy) { 31 | case 'symetricAmbient': 32 | return (await this.vaultWrite).deposit(await weiQty, owner, Typed.bool(true), txArgs) 33 | case 'rswEth': 34 | return (await this.vaultWrite).deposit(await weiQty, owner, Typed.bytes('0x'), txArgs) 35 | } 36 | } 37 | 38 | /* @notice Sends a transaction to redeem shares in vault position back into token1 39 | * @param vaultTokenQty The quantity of vault tokens to withdraw 40 | * @param minToken1Qty The minimum quantity of token1 to receive */ 41 | async redeemZap (vaultQty: TokenQty, minToken1Qty: TokenQty): Promise { 42 | let owner = ((await this.context).actor as Signer).getAddress() 43 | let weiQty = this.vaultToken.normQty(vaultQty); 44 | let minWeiQty = this.token1.normQty(minToken1Qty); 45 | await ensureChain(await this.context) 46 | switch (this.strategy) { 47 | case 'symetricAmbient': 48 | return (await this.vaultWrite).redeem(await weiQty, owner, owner, Typed.uint256(await minWeiQty), Typed.bool(true)) 49 | case 'rswEth': 50 | return (await this.vaultWrite).redeem(await weiQty, owner, owner, Typed.bytes('0x')) 51 | } 52 | } 53 | 54 | /* @notice Retrieves the min deposit quantity in token1 for the Tempest vault */ 55 | async minDeposit(): Promise { 56 | if (!this.minDepositCache) { 57 | this.minDepositCache = (await this.vaultRead).minimumDeposit(); 58 | } 59 | return this.minDepositCache 60 | } 61 | 62 | /* @notice Queries the vault token balance of a wallet */ 63 | async balanceVault (wallet: string): Promise { 64 | return this.vaultToken.wallet(wallet) 65 | } 66 | 67 | /* @notice Queries the implied token1 balance based on the share to asset conversion. */ 68 | async balanceToken1 (wallet: string): Promise { 69 | let balance = await this.balanceVault(wallet); 70 | if (balance === BigInt(0)) 71 | return BigInt(0) 72 | return (await this.vaultRead).convertToAssets(balance) 73 | } 74 | 75 | /* @notice Returns the conversion rate between vault tokens and token1 collateral. */ 76 | async queryConversionRate(): Promise { 77 | let denom = 1000000 78 | let numer = await (await this.vaultRead).convertToShares(denom) 79 | return denom / Number(numer) 80 | } 81 | 82 | /* @notice Checks a wallet's token approval for the vault's token1. */ 83 | async allowance(wallet: string): Promise { 84 | return this.token1.allowance(wallet, this.vaultAddr); 85 | } 86 | 87 | /* @notice Sends transaction to approve token1 on the vault contract */ 88 | async approve (approveQty?: TokenQty): Promise { 89 | return this.token1.approveAddr(this.vaultAddr, approveQty); 90 | } 91 | 92 | 93 | private vaultAddr: string; 94 | private token1: CrocTokenView; 95 | private vaultToken: CrocTokenView; 96 | private strategy: TempestStrategy 97 | private vaultWrite: Promise; 98 | private vaultRead: Promise; 99 | private minDepositCache: Promise | undefined 100 | private context: Promise 101 | } 102 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2016", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "declaration": true, 7 | "strict": true, 8 | "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, 9 | "strictNullChecks": true /* Enable strict null checks. */, 10 | "strictFunctionTypes": true /* Enable strict checking of function types. */, 11 | "noUnusedLocals": true /* Report errors on unused locals. */, 12 | "noUnusedParameters": true /* Report errors on unused parameters. */, 13 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, 14 | "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, 15 | "importHelpers": true, 16 | "skipLibCheck": true, 17 | "esModuleInterop": true, 18 | "allowSyntheticDefaultImports": true, 19 | "experimentalDecorators": true, 20 | "sourceMap": true, 21 | "outDir": "./dist/", 22 | "types": ["node"], 23 | "lib": ["ES2016", "DOM"] 24 | }, 25 | "include": ["src/**/*.ts"], 26 | "exclude": ["node_modules", "**/*.test.ts"] 27 | } 28 | --------------------------------------------------------------------------------