├── .circleci └── config.yml ├── .dockerignore ├── .env.example ├── .eslintignore ├── .eslintrc.js ├── .github ├── dependabot.yml └── workflows │ ├── dependency-review.yml │ ├── deploy.yml │ └── publish.yml ├── .gitignore ├── .npmignore ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .solhint.json ├── .solhintignore ├── CODEOWNERS ├── Dockerfile ├── LICENCE ├── README.md ├── cloudbuild.yml ├── contracts ├── AtomicDepositorTransferProxy.sol ├── AtomicWethDepositor.sol ├── MockArbitrumEvents.sol ├── MockAtomicWethDepositor.sol ├── MockCctpEvents.sol ├── MockHubPool.sol ├── MockLineaEvents.sol ├── MockMulticallReceiver.sol ├── MockOpStackEvents.sol ├── MockPolygonEvents.sol ├── MockScrollBridge.sol ├── MockSpokePool.sol ├── MockWETH9.sol └── MockZKSyncEvents.sol ├── deploy ├── 001_deploy_atomic_depositor.ts └── 002_deploy_atomic_depositor_transfer_proxy.ts ├── deployments ├── mainnet │ ├── .chainId │ ├── AtomicDepositorTransferProxy.json │ ├── AtomicWethDepositor.json │ └── solcInputs │ │ ├── 09d14bfcadf8389a93b601d52e9ec0c7.json │ │ ├── 0b9a38081e5f67aae8aeed6c044fb8c5.json │ │ ├── 1944a92f3b3fe0511b7227d2b1c11e20.json │ │ ├── 30dfeae412ba7ba24be6d29ee0e15ed9.json │ │ ├── 4ccb5bf241263116bb3663cf313b9e1c.json │ │ ├── 5700d0188c8ea770a99799abe93c0e83.json │ │ ├── 5c785432cfb35b0626411450852e3748.json │ │ ├── 68cb79cd539c6a9785b32ed27fb708b7.json │ │ ├── 7095cb2ac075e899237564ebc82cedbc.json │ │ ├── 9e2546c9a25f1566d7cda23de27622e1.json │ │ ├── a4875bd777d9afdf683a4e5b67e80d39.json │ │ ├── c704b7dd9dde90da1811337714ae187e.json │ │ └── dfa1a8c5a6b030fb38fb617a6ddc8435.json └── sepolia │ ├── AtomicWethDepositor.json │ └── solcInputs │ ├── 37ed92c125868e00a3e658e73545b1c6.json │ └── a4875bd777d9afdf683a4e5b67e80d39.json ├── funding.json ├── hardhat.config.ts ├── index.ts ├── package.json ├── scripts ├── constructEmergencyRoot.ts ├── disableDepositRoutes.ts ├── fetch-addresses.ts ├── hubpool.ts ├── runCommand.sh ├── runRelayerOnGoerli.sh ├── sendTokens.ts ├── spokepool.ts ├── testTargetChainRoutes.sh ├── unwrapWeth.ts ├── updateAtomicDepositor.ts ├── utils.ts ├── withdrawFromArbitrumOrbit.ts ├── withdrawFromOpStack.ts └── zkSyncDemo.ts ├── src ├── adapter │ ├── BaseChainAdapter.ts │ ├── bridges │ │ ├── ArbitrumOrbitBridge.ts │ │ ├── BaseBridgeAdapter.ts │ │ ├── BinanceCEXBridge.ts │ │ ├── BinanceCEXNativeBridge.ts │ │ ├── BlastBridge.ts │ │ ├── DaiOptimismBridge.ts │ │ ├── HyperlaneXERC20Bridge.ts │ │ ├── LineaBridge.ts │ │ ├── LineaWethBridge.ts │ │ ├── OFTBridge.ts │ │ ├── OpStackDefaultErc20Bridge.ts │ │ ├── OpStackUSDCBridge.ts │ │ ├── OpStackWethBridge.ts │ │ ├── PolygonERC20Bridge.ts │ │ ├── PolygonWethBridge.ts │ │ ├── ScrollERC20Bridge.ts │ │ ├── SnxOptimismBridge.ts │ │ ├── SolanaUsdcCCTPBridge.ts │ │ ├── UsdcCCTPBridge.ts │ │ ├── UsdcTokenSplitterBridge.ts │ │ ├── ZKStackBridge.ts │ │ ├── ZKStackUSDCBridge.ts │ │ ├── ZKStackWethBridge.ts │ │ └── index.ts │ ├── index.ts │ ├── l2Bridges │ │ ├── ArbitrumOrbitBridge.ts │ │ ├── BaseL2BridgeAdapter.ts │ │ ├── BinanceCEXBridge.ts │ │ ├── HyperlaneXERC20Bridge.ts │ │ ├── OpStackBridge.ts │ │ ├── OpStackUSDCBridge.ts │ │ ├── OpStackWethBridge.ts │ │ └── index.ts │ └── utils.ts ├── caching │ └── RedisCache.ts ├── clients │ ├── AcrossAPIClient.ts │ ├── BalanceAllocator.ts │ ├── ConfigStoreClient.ts │ ├── HubPoolClient.ts │ ├── InventoryClient.ts │ ├── MultiCallerClient.ts │ ├── ProfitClient.ts │ ├── SpokePoolClient.ts │ ├── TokenClient.ts │ ├── TokenTransferClient.ts │ ├── TransactionClient.ts │ ├── bridges │ │ ├── AdapterManager.ts │ │ ├── CrossChainTransferClient.ts │ │ ├── index.ts │ │ └── utils.ts │ └── index.ts ├── common │ ├── ClientHelper.ts │ ├── Config.ts │ ├── Constants.ts │ ├── ContractAddresses.ts │ ├── abi │ │ ├── ArbSysL2.json │ │ ├── ArbitrumErc20GatewayL1.json │ │ ├── ArbitrumErc20GatewayL2.json │ │ ├── ArbitrumErc20GatewayRouterL1.json │ │ ├── ArbitrumErc20GatewayRouterL2.json │ │ ├── ArbitrumOutbox.json │ │ ├── AtomicDepositor.json │ │ ├── AtomicDepositorTransferProxy.json │ │ ├── BlastBridge.json │ │ ├── BlastDaiRetriever.json │ │ ├── BlastOptimismPortal.json │ │ ├── BlastYieldManager.json │ │ ├── CctpMessageTransmitter.json │ │ ├── CctpTokenMessenger.json │ │ ├── CctpV2TokenMessenger.json │ │ ├── DaiOptimismBridgeL1.json │ │ ├── DaiOptimismBridgeL2.json │ │ ├── HubPool.json │ │ ├── HubPoolStore.json │ │ ├── IHypXERC20Router.json │ │ ├── IOFT.json │ │ ├── LineaMessageService.json │ │ ├── LineaTokenBridge.json │ │ ├── LineaUsdcBridge.json │ │ ├── MinimalERC20.json │ │ ├── OpStackPortalL1.json │ │ ├── OpStackStandardBridgeL1.json │ │ ├── OpStackStandardBridgeL2.json │ │ ├── OpStackUSDCBridge.json │ │ ├── PolygonBridge.json │ │ ├── PolygonRootChainManager.json │ │ ├── PolygonWithdrawableErc20.json │ │ ├── SP1Helios.json │ │ ├── ScrollGasPriceOracle.json │ │ ├── ScrollGatewayRouterL1.json │ │ ├── ScrollGatewayRouterL2.json │ │ ├── ScrollRelayMessenger.json │ │ ├── SnxOptimismBridgeL1.json │ │ ├── SnxOptimismBridgeL2.json │ │ ├── Universal_SpokePool.json │ │ ├── V3SpokePool.json │ │ ├── VotingV2.json │ │ ├── Weth.json │ │ ├── ZkStackBridgeHub.json │ │ ├── ZkStackNativeTokenVault.json │ │ ├── ZkStackSharedBridge.json │ │ └── ZkStackUSDCBridge.json │ └── index.ts ├── dataworker │ ├── Dataworker.ts │ ├── DataworkerClientHelper.ts │ ├── DataworkerConfig.ts │ ├── DataworkerUtils.ts │ ├── PoolRebalanceUtils.ts │ ├── README.md │ ├── RelayerRefundUtils.ts │ └── index.ts ├── finalizer │ ├── index.ts │ ├── types.ts │ └── utils │ │ ├── arbStack.ts │ │ ├── binance.ts │ │ ├── cctp │ │ ├── index.ts │ │ ├── l1ToL2.ts │ │ └── l2ToL1.ts │ │ ├── helios.ts │ │ ├── index.ts │ │ ├── linea │ │ ├── common.ts │ │ ├── imports.ts │ │ ├── index.ts │ │ ├── l1ToL2.ts │ │ └── l2ToL1.ts │ │ ├── opStack.ts │ │ ├── polygon.ts │ │ ├── scroll.ts │ │ └── zkSync.ts ├── interfaces │ ├── Arweave.ts │ ├── BundleData.ts │ ├── Error.ts │ ├── Helios.ts │ ├── InventoryManagement.ts │ ├── Report.ts │ ├── SpokePool.ts │ ├── Token.ts │ ├── Universal.ts │ ├── ZkApi.ts │ └── index.ts ├── libexec │ ├── RelayerSpokePoolListener.ts │ ├── RelayerSpokePoolListenerHTTPS.ts │ ├── types.ts │ └── util │ │ ├── evm │ │ ├── index.ts │ │ └── util.ts │ │ └── ipc.ts ├── monitor │ ├── .env.sample │ ├── Monitor.ts │ ├── MonitorClientHelper.ts │ ├── MonitorConfig.ts │ └── index.ts ├── relayer │ ├── Relayer.ts │ ├── RelayerClientHelper.ts │ ├── RelayerConfig.ts │ └── index.ts ├── scripts │ ├── validateRootBundle.ts │ └── validateRunningBalances.ts └── utils │ ├── AddressUtils.ts │ ├── AnchorUtils.ts │ ├── BNUtils.ts │ ├── BinanceUtils.ts │ ├── BlockUtils.ts │ ├── CCTPUtils.ts │ ├── CLIUtils.ts │ ├── ContractUtils.ts │ ├── DepositUtils.ts │ ├── EventUtils.ts │ ├── ExecutionUtils.ts │ ├── FillUtils.ts │ ├── GckmsUtils.ts │ ├── HeliosUtils.ts │ ├── Help.ts │ ├── LogUtils.ts │ ├── MerkleTreeUtils.ts │ ├── NetworkUtils.ts │ ├── ObjectUtils.ts │ ├── ProviderUtils.ts │ ├── RedisUtils.ts │ ├── RetryUtils.ts │ ├── SDKUtils.ts │ ├── SignerUtils.ts │ ├── SuperstructUtils.ts │ ├── SvmSignerUtils.ts │ ├── TokenUtils.ts │ ├── TransactionUtils.ts │ ├── TypeGuards.ts │ ├── TypeUtils.ts │ ├── UmaUtils.ts │ ├── UniversalUtils.ts │ ├── ZkApiUtils.ts │ ├── chains │ ├── index.ts │ └── zkSync.ts │ ├── fsUtils.ts │ └── index.ts ├── tasks ├── index.ts └── integration-tests.ts ├── test ├── BalanceAllocator.ts ├── Dataworker.blockRangeUtils.ts ├── Dataworker.buildRoots.ts ├── Dataworker.customSpokePoolClients.ts ├── Dataworker.executePoolRebalanceUtils.ts ├── Dataworker.executePoolRebalances.ts ├── Dataworker.executeRelayerRefunds.ts ├── Dataworker.executeSlowRelay.ts ├── Dataworker.loadData.deposit.ts ├── Dataworker.loadData.fill.ts ├── Dataworker.loadData.prefill.ts ├── Dataworker.loadData.slowFill.ts ├── Dataworker.loadData.unexecutableSlowFill.ts ├── Dataworker.proposeRootBundle.ts ├── Dataworker.validateRootBundle.ts ├── DataworkerUtils.ts ├── EventManager.ts ├── EventUtils.ts ├── InventoryClient.InventoryRebalance.ts ├── InventoryClient.RefundChain.ts ├── Monitor.ts ├── MultiCallerClient.ts ├── ProfitClient.ConsiderProfitability.ts ├── ProfitClient.PriceRetrieval.ts ├── Relayer.BasicFill.ts ├── Relayer.IndexedSpokePoolClient.ts ├── Relayer.SlowFill.ts ├── Relayer.TokenShortfall.ts ├── Relayer.UnfilledDeposits.ts ├── RetryUtils.ts ├── TokenClient.Approval.ts ├── TokenClient.BalanceAlowance.ts ├── TokenClient.TokenShortfall.ts ├── TransactionClient.ts ├── TryMulticallClient.ts ├── constants.ts ├── fixtures │ ├── Dataworker.Fixture.ts │ └── UmaEcosystemFixture.ts ├── generic-adapters │ ├── AdapterManager.SendTokensCrossChain.ts │ ├── Arbitrum.ts │ ├── Linea.ts │ ├── OpStack.ts │ ├── Polygon.ts │ ├── Scroll.ts │ └── zkSync.ts ├── mocks │ ├── MockAdapterManager.ts │ ├── MockArweaveClient.ts │ ├── MockBaseChainAdapter.ts │ ├── MockBundleDataClient.ts │ ├── MockConfigStoreClient.ts │ ├── MockCrossChainTransferClient.ts │ ├── MockHubPoolClient.ts │ ├── MockInventoryClient.ts │ ├── MockMultiCallerClient.ts │ ├── MockProfitClient.ts │ ├── MockSpokePoolClient.ts │ ├── MockTokenClient.ts │ ├── MockTransactionClient.ts │ └── index.ts ├── types │ └── index.ts └── utils │ ├── BlockchainUtils.ts │ ├── SpokePoolUtils.ts │ ├── UBAUtils.ts │ ├── index.ts │ └── utils.ts ├── tsconfig.eslint.json ├── tsconfig.json ├── tsconfig.test.json └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | jobs: 3 | install: 4 | docker: 5 | - image: cimg/node:20.18.0 6 | working_directory: ~/relayer-v2 7 | resource_class: medium+ 8 | steps: 9 | - checkout 10 | - restore_cache: 11 | name: Restore Yarn Package Cache 12 | keys: 13 | - cache-node-v1-{{ checksum "yarn.lock" }} 14 | - run: 15 | name: Install Dependencies 16 | command: yarn install --immutable 17 | - save_cache: 18 | name: Save Yarn Package Cache 19 | key: cache-node-v1-{{ checksum "yarn.lock" }} 20 | paths: 21 | - node_modules 22 | test: 23 | docker: 24 | - image: cimg/node:20.18.0 25 | working_directory: ~/relayer-v2 26 | resource_class: medium+ 27 | parallelism: 20 28 | steps: 29 | - checkout 30 | - restore_cache: 31 | name: Restore Yarn Package Cache 32 | keys: 33 | - cache-node-v1-{{ checksum "yarn.lock" }} 34 | - run: 35 | name: Run build 36 | command: yarn build 37 | - run: 38 | name: Run tests 39 | command: | 40 | circleci tests glob "test/**/*.ts" | circleci tests split > /tmp/test-files 41 | yarn test --bail $(cat /tmp/test-files) 42 | lint: 43 | docker: 44 | - image: cimg/node:20.18.0 45 | working_directory: ~/relayer-v2 46 | resource_class: medium+ 47 | steps: 48 | - checkout 49 | - restore_cache: 50 | name: Restore Yarn Package Cache 51 | keys: 52 | - cache-node-v1-{{ checksum "yarn.lock" }} 53 | - run: 54 | name: Run lint 55 | command: yarn lint 56 | workflows: 57 | version: 2.1 58 | build_and_test: 59 | jobs: 60 | - install 61 | - test: 62 | requires: 63 | - install 64 | - lint: 65 | requires: 66 | - install 67 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .github 2 | node_modules 3 | package-lock.json 4 | env 5 | dist 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | artifacts 3 | dist 4 | cache 5 | typechain* 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: false, 4 | es2021: true, 5 | mocha: true, 6 | node: true, 7 | }, 8 | plugins: ["node", "prettier", "@typescript-eslint", "mocha", "chai-expect"], 9 | extends: [ 10 | "plugin:prettier/recommended", 11 | "eslint:recommended", 12 | "plugin:@typescript-eslint/recommended", 13 | "plugin:@typescript-eslint/eslint-recommended", 14 | "plugin:node/recommended", 15 | ], 16 | parser: "@typescript-eslint/parser", 17 | parserOptions: { 18 | ecmaVersion: 12, 19 | project: "./tsconfig.eslint.json", 20 | }, 21 | rules: { 22 | "prettier/prettier": ["warn"], 23 | indent: 0, // avoid conflict with prettier's indent system 24 | "linebreak-style": ["error", "unix"], 25 | quotes: ["error", "double", { avoidEscape: true }], 26 | semi: ["error", "always"], 27 | curly: ["error", "all"], 28 | "spaced-comment": ["error", "always", { exceptions: ["-", "+"] }], 29 | "no-console": 2, 30 | camelcase: "off", 31 | "@typescript-eslint/camelcase": "off", 32 | "mocha/no-exclusive-tests": "error", 33 | "@typescript-eslint/no-var-requires": "off", 34 | "node/no-unsupported-features/es-syntax": ["error", { ignores: ["modules"] }], 35 | // Disable warnings for { a, b, ...rest } variables, since this is typically used to remove variables. 36 | "@typescript-eslint/no-unused-vars": ["error", { ignoreRestSiblings: true }], 37 | "chai-expect/missing-assertion": 2, 38 | "no-duplicate-imports": "error", 39 | "@typescript-eslint/no-floating-promises": ["error"], 40 | "@typescript-eslint/no-misused-promises": ["error", { checksVoidReturn: false }], 41 | "no-restricted-imports": [ 42 | "error", 43 | { 44 | patterns: [ 45 | { group: ["@ethersproject/bignumber"], message: "Use 'src/utils/BNUtils' instead" }, 46 | { group: ["hardhat"], message: "Use 'src/utils or 'ethers'' instead" }, 47 | ], 48 | paths: [ 49 | { name: "ethers", importNames: ["BigNumber"], message: "Use 'src/utils/BNUtils' instead" }, 50 | { name: "ethers", importNames: ["Event"], message: "Use Log from 'src/interfaces/Common' instead" }, 51 | ], 52 | }, 53 | ], 54 | }, 55 | settings: { 56 | node: { 57 | tryExtensions: [".js", ".ts"], 58 | }, 59 | }, 60 | overrides: [ 61 | { 62 | files: ["scripts/*.ts", "tasks/*.ts", "src/scripts/*.ts"], 63 | rules: { 64 | "no-console": 0, 65 | }, 66 | }, 67 | { 68 | files: ["test/**/*.ts", "hardhat.config.ts", "tasks/*.ts"], 69 | rules: { 70 | "no-restricted-imports": "off", 71 | }, 72 | }, 73 | ], 74 | }; 75 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "npm" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | # Create a group of dependencies to be updated together in one pull request 12 | groups: 13 | # Specify a name for the group, which will be used in pull request titles 14 | # and branch names 15 | all-dependencies: 16 | # Define patterns to include dependencies in the group (based on 17 | # dependency name) 18 | patterns: 19 | - "*" # A wildcard that matches all dependencies in the packages 20 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | name: "Dependency Review" 2 | on: [pull_request] 3 | 4 | permissions: 5 | contents: read 6 | 7 | jobs: 8 | dependency-review: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: "Checkout Repository" 12 | uses: actions/checkout@v4 13 | - name: "Dependency Review" 14 | uses: actions/dependency-review-action@v4.3.4 15 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | Deploy: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Curl build and deploy webhook for GCP for UMA protocol repo 11 | run: ${{ secrets.PROTOCOL_BUILD_DEPLOY_WEBHOOK }} 12 | shell: bash 13 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package to npmjs 2 | on: 3 | release: 4 | types: [created] 5 | jobs: 6 | publish: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | # Setup .npmrc file to publish to npm 11 | - uses: actions/setup-node@v4 12 | with: 13 | node-version: "20" 14 | registry-url: "https://registry.npmjs.org" 15 | cache: "yarn" 16 | - run: yarn 17 | - run: yarn publish 18 | env: 19 | NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dotfiles are ignored by default and must be added explicitly. Use 2 | # `git add -f ` add a dotfile. Only do this for public files. 3 | .* 4 | 5 | node_modules 6 | coverage 7 | coverage.json 8 | typechain* 9 | dump.rdb* 10 | 11 | # Hardhat files 12 | cache 13 | artifacts 14 | dist 15 | 16 | # Debugging files 17 | *.log 18 | 19 | # Address lists 20 | addresses.json 21 | 22 | # AI context files 23 | CLAUDE.md 24 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | hardhat.config.ts 2 | scripts 3 | test 4 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/iron 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | artifacts 3 | cache 4 | coverage* 5 | gasReporterOutput.json 6 | dist 7 | typechain* 8 | addresses.json 9 | *.md 10 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "bracketSpacing": true, 4 | "tabWidth": 2, 5 | "overrides": [ 6 | { 7 | "files": "*.sol", 8 | "options": { 9 | "tabWidth": 4, 10 | "useTabs": false, 11 | "singleQuote": false 12 | } 13 | }, 14 | { 15 | "files": "*.md", 16 | "options": { 17 | "semi": false 18 | } 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "rules": { 4 | "compiler-version": ["error", "^0.8.0"], 5 | "func-visibility": ["warn", { "ignoreConstructors": true }] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.solhintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # This is a comment. 2 | # Each line is a file pattern followed by one or more owners. 3 | # Note: CODEOWNERS are automatically requested for review on relevant PRs. 4 | # Order is important; the last matching pattern takes the most 5 | # precedence. 6 | 7 | # These owners will be the default owners for everything in 8 | # the repo unless a later match takes precedence. 9 | * @mrice32 @nicholaspai @pxrl @james-a-morris @dohaki @bmzig 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20 2 | 3 | WORKDIR /relayer 4 | 5 | COPY . ./ 6 | 7 | RUN apt-get update 8 | RUN apt-get install -y libudev-dev libusb-1.0-0-dev jq yarn rsync 9 | RUN yarn 10 | 11 | RUN yarn build 12 | 13 | ENTRYPOINT ["/bin/bash", "scripts/runCommand.sh"] 14 | -------------------------------------------------------------------------------- /cloudbuild.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - name: "gcr.io/cloud-builders/docker" 3 | args: ["build", "-t", "gcr.io/images-across-6363/across-v2", "."] 4 | options: 5 | machineType: "E2_HIGHCPU_32" 6 | timeout: 3600s 7 | images: ["gcr.io/images-across-6363/across-v2"] 8 | -------------------------------------------------------------------------------- /contracts/AtomicDepositorTransferProxy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity ^0.8.0; 3 | 4 | /** 5 | * @notice Contract deployed on Ethereum which is meant to be a whitelisted bridge on the AtomicWethDepositor for all networks which 6 | * rebalance WETH via a centralized exchange that uses (non-static) EOA deposit addresses. 7 | */ 8 | contract AtomicDepositorTransferProxy { 9 | /** 10 | * @notice Transfers `msg.value` to the input `to` address. 11 | * @param to The address to receive `msg.value`. 12 | */ 13 | function transfer(address to) external payable { 14 | (bool success, ) = to.call{ value: msg.value }(""); 15 | require(success, "ETH transfer failed."); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /contracts/MockArbitrumEvents.sol: -------------------------------------------------------------------------------- 1 | /// This file contains contracts that can be used to unit test the src/clients/bridges/arbitrum 2 | /// code which reads events from Arbitrum contracts facilitating cross chain transfers. 3 | 4 | pragma solidity ^0.8.0; 5 | 6 | contract ArbitrumERC20Bridge { 7 | event DepositInitiated( 8 | address l1Token, 9 | address indexed _from, 10 | address indexed _to, 11 | uint256 indexed _sequenceNumber, 12 | uint256 _amount 13 | ); 14 | event DepositFinalized(address indexed l1Token, address indexed from, address indexed to, uint256 amount); 15 | 16 | function emitDepositInitiated( 17 | address l1Token, 18 | address from, 19 | address to, 20 | uint256 sequenceNumber, 21 | uint256 amount 22 | ) external { 23 | emit DepositInitiated(l1Token, from, to, sequenceNumber, amount); 24 | } 25 | 26 | function emitDepositFinalized(address l1Token, address from, address to, uint256 amount) external { 27 | emit DepositFinalized(l1Token, from, to, amount); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /contracts/MockAtomicWethDepositor.sol: -------------------------------------------------------------------------------- 1 | // This file contains events to simulate the Atomic Depositor 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | contract MockAtomicWethDepositor { 6 | event AtomicWethDepositInitiated(address indexed from, uint256 indexed chainId, uint256 amount); 7 | 8 | function bridgeWeth( 9 | uint256 chainId, 10 | uint256 netAmount, 11 | uint256 bridgeAmount, 12 | uint256 feeAmount, 13 | bytes calldata 14 | ) public { 15 | emit AtomicWethDepositInitiated(msg.sender, chainId, bridgeAmount); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /contracts/MockCctpEvents.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | contract CctpTokenMessenger { 4 | event DepositForBurn( 5 | uint64 indexed nonce, 6 | address indexed burnToken, 7 | uint256 amount, 8 | address indexed depositor, 9 | bytes32 mintRecipient, 10 | uint32 destinationDomain, 11 | bytes32 destinationTokenMessenger, 12 | bytes32 destinationCaller 13 | ); 14 | event MintAndWithdraw(address indexed mintRecipient, uint256 amount, address indexed mintToken); 15 | 16 | function emitDepositForBurn( 17 | uint64 nonce, 18 | address burnToken, 19 | uint256 amount, 20 | address depositor, 21 | bytes32 mintRecipient, 22 | uint32 destinationDomain, 23 | bytes32 destinationTokenMessenger, 24 | bytes32 destinationCaller 25 | ) external { 26 | emit DepositForBurn( 27 | nonce, 28 | burnToken, 29 | amount, 30 | depositor, 31 | mintRecipient, 32 | destinationDomain, 33 | destinationTokenMessenger, 34 | destinationCaller 35 | ); 36 | } 37 | 38 | function emitMintAndWithdraw(address mintRecipient, uint256 amount, address mintToken) external { 39 | emit MintAndWithdraw(mintRecipient, amount, mintToken); 40 | } 41 | } 42 | 43 | contract CctpV2TokenMessenger { 44 | event DepositForBurn( 45 | address indexed burnToken, 46 | uint256 amount, 47 | address indexed depositor, 48 | bytes32 mintRecipient, 49 | uint32 destinationDomain, 50 | bytes32 destinationTokenMessenger, 51 | bytes32 destinationCaller, 52 | uint256 maxFee, 53 | uint32 indexed minFinalityThreshold, 54 | bytes hookData 55 | ); 56 | event MintAndWithdraw(address indexed mintRecipient, uint256 amount, address indexed mintToken); 57 | 58 | function emitDepositForBurn( 59 | address burnToken, 60 | uint256 amount, 61 | address depositor, 62 | bytes32 mintRecipient, 63 | uint32 destinationDomain, 64 | bytes32 destinationTokenMessenger, 65 | bytes32 destinationCaller 66 | ) external { 67 | emit DepositForBurn( 68 | burnToken, 69 | amount, 70 | depositor, 71 | mintRecipient, 72 | destinationDomain, 73 | destinationTokenMessenger, 74 | destinationCaller, 75 | 0, 76 | 2000, 77 | "" 78 | ); 79 | } 80 | 81 | function emitMintAndWithdraw(address mintRecipient, uint256 amount, address mintToken) external { 82 | emit MintAndWithdraw(mintRecipient, amount, mintToken); 83 | } 84 | } 85 | 86 | contract CctpMessageTransmitter { 87 | mapping(bytes32 => uint256) public usedNonces; 88 | 89 | function setUsedNonce(bytes32 nonceHash, bool isUsed) external { 90 | usedNonces[nonceHash] = isUsed ? 1 : 0; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /contracts/MockHubPool.sol: -------------------------------------------------------------------------------- 1 | // This file contains contracts that can be used to unit test bridge adapters. 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | contract MockHubPool { 6 | event TokensRelayed(address l1Token, address l2Token, uint256 amount, address to); 7 | 8 | function relayTokens(address l1Token, address l2Token, uint256 amount, address to) public { 9 | emit TokensRelayed(l1Token, l2Token, amount, to); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /contracts/MockLineaEvents.sol: -------------------------------------------------------------------------------- 1 | /// This file contains contracts that can be used to unit test the src/clients/bridges/LineaAdapter.ts 2 | /// code which reads events from Linea contracts facilitating cross chain transfers. 3 | 4 | pragma solidity ^0.8.0; 5 | 6 | contract LineaWethBridge { 7 | event MessageClaimed(bytes32 indexed _messageHash); 8 | event MessageSent( 9 | address indexed _from, 10 | address indexed _to, 11 | uint256 _fee, 12 | uint256 _value, 13 | uint256 _nonce, 14 | bytes _calldata, 15 | bytes32 indexed _messageHash 16 | ); 17 | 18 | function emitMessageSent(address from, address to, uint256 value) external { 19 | emit MessageSent(from, to, 0, value, 0, new bytes(0), bytes32(0)); 20 | } 21 | 22 | function emitMessageSentWithMessageHash(address from, address to, uint256 value, bytes32 messageHash) external { 23 | emit MessageSent(from, to, 0, value, 0, new bytes(0), messageHash); 24 | } 25 | 26 | function emitMessageClaimed(bytes32 messageHash) external { 27 | emit MessageClaimed(messageHash); 28 | } 29 | } 30 | 31 | contract LineaUsdcBridge { 32 | event Deposited(address indexed depositor, uint256 amount, address indexed to); 33 | event ReceivedFromOtherLayer(address indexed recipient, uint256 amount); 34 | 35 | function emitDeposited(address depositor, address to) external { 36 | emit Deposited(depositor, 0, to); 37 | } 38 | 39 | function emitReceivedFromOtherLayer(address recipient) external { 40 | emit ReceivedFromOtherLayer(recipient, 0); 41 | } 42 | } 43 | 44 | contract LineaERC20Bridge { 45 | event BridgingInitiatedV2(address indexed sender, address indexed recipient, address indexed token, uint256 amount); 46 | event BridgingFinalizedV2( 47 | address indexed nativeToken, 48 | address indexed bridgedToken, 49 | uint256 amount, 50 | address indexed recipient 51 | ); 52 | 53 | function emitBridgingInitiated(address sender, address recipient, address token) external { 54 | emit BridgingInitiatedV2(sender, recipient, token, 0); 55 | } 56 | 57 | function emitBridgingFinalized(address l1Token, address recipient) external { 58 | emit BridgingFinalizedV2(l1Token, address(0), 0, recipient); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /contracts/MockMulticallReceiver.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: Unlicense 2 | pragma solidity ^0.8.0; 3 | 4 | /** 5 | * @title MockMulticallReceiver 6 | */ 7 | contract MockMulticallReceiver { 8 | struct Result { 9 | bool success; 10 | bytes returnData; 11 | } 12 | 13 | /// @custom:oz-upgrades-unsafe-allow constructor 14 | constructor() {} // solhint-disable-line no-empty-blocks 15 | 16 | // Entrypoint for tryMulticall tests. For simplicity, a call will succeed if its MSB is nonzero. 17 | function tryMulticall(bytes[] calldata calls) external pure returns (Result[] memory) { 18 | Result[] memory results = new Result[](calls.length); 19 | for (uint256 i = 0; i < calls.length; ++i) { 20 | bool success = uint256(bytes32(calls[i])) != 0; 21 | results[i] = Result(success, calls[i]); 22 | } 23 | return results; 24 | } 25 | 26 | // In the MultiCallerClient, we require the contract we are calling to contain 27 | // multicall in order for the transaction to not be marked as unsendable. 28 | // https://github.com/across-protocol/relayer/blob/e56a6874419eeded58d64e77a1628b6541861b65/src/clients/MultiCallerClient.ts#L374 29 | function multicall() external {} 30 | } 31 | -------------------------------------------------------------------------------- /contracts/MockOpStackEvents.sol: -------------------------------------------------------------------------------- 1 | /// This file contains contracts that can be used to unit test the src/clients/bridges/op-stack 2 | /// code which reads events from OpStack contracts facilitating cross chain transfers. 3 | 4 | pragma solidity ^0.8.0; 5 | 6 | contract OpStackWethBridge { 7 | event ETHDepositInitiated(address indexed _from, address indexed _to, uint256 _amount, bytes _data); 8 | event DepositFinalized( 9 | address indexed _l1Token, 10 | address indexed _l2Token, 11 | address indexed _from, 12 | address _to, 13 | uint256 _amount, 14 | bytes _data 15 | ); 16 | 17 | function emitDepositInitiated(address from, address to, uint256 amount) external { 18 | emit ETHDepositInitiated(from, to, amount, new bytes(0)); 19 | } 20 | 21 | function emitDepositFinalized(address from, address to, uint256 amount) external { 22 | emit DepositFinalized(address(0), address(0), from, to, amount, new bytes(0)); 23 | } 24 | } 25 | 26 | contract OpStackStandardBridge { 27 | event ERC20DepositInitiated( 28 | address indexed _l1Token, 29 | address indexed _l2Token, 30 | address indexed _from, 31 | address _to, 32 | uint256 _amount, 33 | bytes _data 34 | ); 35 | event DepositFinalized( 36 | address indexed _l1Token, 37 | address indexed _l2Token, 38 | address indexed _from, 39 | address _to, 40 | uint256 _amount, 41 | bytes _data 42 | ); 43 | 44 | function emitDepositInitiated(address l1Token, address l2Token, address from, address to, uint256 amount) external { 45 | emit ERC20DepositInitiated(l1Token, l2Token, from, to, amount, new bytes(0)); 46 | } 47 | 48 | function emitDepositFinalized(address l1Token, address l2Token, address from, address to, uint256 amount) external { 49 | emit DepositFinalized(l1Token, l2Token, from, to, amount, new bytes(0)); 50 | } 51 | } 52 | 53 | contract OpStackSnxBridge { 54 | event DepositInitiated(address indexed _from, address indexed _to, uint256 _amount); 55 | event DepositFinalized(address indexed _to, uint256 _amount); 56 | 57 | function emitDepositInitiated(address from, address to, uint256 amount) external { 58 | emit DepositInitiated(from, to, amount); 59 | } 60 | 61 | function emitDepositFinalized(address to, uint256 amount) external { 62 | emit DepositFinalized(to, amount); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /contracts/MockPolygonEvents.sol: -------------------------------------------------------------------------------- 1 | // This file contains contracts that can be used to unit test the src/clients/bridges/ZkSyncAdapter.ts 2 | // code which reads events from zkSync contracts facilitating cross chain transfers. 3 | 4 | pragma solidity ^0.8.0; 5 | 6 | contract Polygon_L1Bridge { 7 | event LockedERC20( 8 | address indexed depositor, 9 | address indexed depositReceiver, 10 | address indexed rootToken, 11 | uint256 amount 12 | ); 13 | 14 | event LockedEther(address indexed depositor, address indexed depositReceiver, uint256 amount); 15 | 16 | function depositFor(address depositor, address depositReceiver, address rootToken, uint256 amount) external { 17 | emit LockedERC20(depositor, depositReceiver, rootToken, amount); 18 | } 19 | 20 | function depositEtherFor(address depositor, address depositReceiver, uint256 amount) external { 21 | emit LockedEther(depositor, depositReceiver, amount); 22 | } 23 | } 24 | 25 | contract Polygon_L2Bridge { 26 | event Transfer(address indexed from, address indexed to, uint256 value); 27 | 28 | function transfer(address from, address to, uint256 value) external { 29 | emit Transfer(from, to, value); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /contracts/MockScrollBridge.sol: -------------------------------------------------------------------------------- 1 | // This file emulates a Scroll ERC20 Gateway. For testing, it is used as both the L1 and L2 bridge contract. 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | contract ScrollBridge { 6 | event DepositERC20( 7 | address indexed l1Token, 8 | address indexed l2Token, 9 | address indexed from, 10 | address to, 11 | uint256 amount, 12 | bytes data 13 | ); 14 | event FinalizeDepositERC20( 15 | address indexed l1Token, 16 | address indexed l2Token, 17 | address indexed from, 18 | address to, 19 | uint256 amount, 20 | bytes data 21 | ); 22 | 23 | function deposit(address l1Token, address l2Token, address from, address to, uint256 amount) public { 24 | emit DepositERC20(l1Token, l2Token, from, to, amount, new bytes(0)); 25 | } 26 | 27 | function finalize(address l1Token, address l2Token, address from, address to, uint256 amount) public { 28 | emit FinalizeDepositERC20(l1Token, l2Token, from, to, amount, new bytes(0)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /contracts/MockSpokePool.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: Unlicense 2 | pragma solidity ^0.8.0; 3 | 4 | import "@across-protocol/contracts/contracts/test/MockSpokePool.sol"; 5 | 6 | /** 7 | * @title MockSpokePool 8 | * @dev For some reason, the @openzeppelin/hardhat-upgrades plugin fails to find the MockSpokePool ABI unless 9 | * this contract is explicitly defined here. 10 | */ 11 | contract _MockSpokePool is MockSpokePool { 12 | /// @custom:oz-upgrades-unsafe-allow constructor 13 | constructor(address _wrappedNativeTokenAddress) MockSpokePool(_wrappedNativeTokenAddress) {} // solhint-disable-line no-empty-blocks 14 | } 15 | -------------------------------------------------------------------------------- /contracts/MockWETH9.sol: -------------------------------------------------------------------------------- 1 | // This file contains contracts that can be used to unit test the src/clients/bridges/ZkSyncAdapter.ts 2 | // code which reads events from zkSync contracts facilitating cross chain transfers. 3 | 4 | pragma solidity ^0.8.0; 5 | 6 | contract MockWETH9 { 7 | event Transfer(address indexed from, address indexed _to, uint256 _amount); 8 | 9 | function transfer(address from, address to, uint256 amount) external { 10 | emit Transfer(from, to, amount); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /deploy/001_deploy_atomic_depositor.ts: -------------------------------------------------------------------------------- 1 | import { DeployFunction } from "hardhat-deploy/types"; 2 | 3 | const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 4 | const { 5 | deployments: { deploy }, 6 | getNamedAccounts, 7 | } = hre; 8 | 9 | const { deployer } = await getNamedAccounts(); 10 | 11 | await deploy("AtomicWethDepositor", { 12 | from: deployer, 13 | log: true, 14 | skipIfAlreadyDeployed: false, 15 | }); 16 | }; 17 | module.exports = func; 18 | func.tags = ["AtomicWethDepositor"]; 19 | -------------------------------------------------------------------------------- /deploy/002_deploy_atomic_depositor_transfer_proxy.ts: -------------------------------------------------------------------------------- 1 | import { DeployFunction } from "hardhat-deploy/types"; 2 | 3 | const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 4 | const { 5 | deployments: { deploy }, 6 | getNamedAccounts, 7 | } = hre; 8 | 9 | const { deployer } = await getNamedAccounts(); 10 | 11 | await deploy("AtomicDepositorTransferProxy", { 12 | from: deployer, 13 | log: true, 14 | skipIfAlreadyDeployed: false, 15 | }); 16 | }; 17 | module.exports = func; 18 | func.tags = ["AtomicDepositorTransferProxy"]; 19 | -------------------------------------------------------------------------------- /deployments/mainnet/.chainId: -------------------------------------------------------------------------------- 1 | 1 -------------------------------------------------------------------------------- /funding.json: -------------------------------------------------------------------------------- 1 | { 2 | "opRetro": { 3 | "projectId": "0x72723e07fe409557489a6643b43d9493a94c10ba68230b0527f01834cb6a550f" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import minimist from "minimist"; 2 | import { 3 | config, 4 | delay, 5 | exit, 6 | retrieveSignerFromCLIArgs, 7 | help, 8 | Logger, 9 | usage, 10 | waitForLogger, 11 | stringifyThrownValue, 12 | } from "./src/utils"; 13 | import { runRelayer } from "./src/relayer"; 14 | import { runDataworker } from "./src/dataworker"; 15 | import { runMonitor } from "./src/monitor"; 16 | import { runFinalizer } from "./src/finalizer"; 17 | import { version } from "./package.json"; 18 | 19 | let logger: typeof Logger; 20 | let cmd: string; 21 | 22 | export async function run(args: { [k: string]: boolean | string }): Promise { 23 | logger = Logger; 24 | 25 | const cmds = { 26 | dataworker: runDataworker, 27 | finalizer: runFinalizer, 28 | help: help, 29 | monitor: runMonitor, 30 | relayer: runRelayer, 31 | }; 32 | 33 | // todo Make the mode of operation an operand, rather than an option. 34 | // i.e. ts-node ./index.ts [options] 35 | // Note: ts does not produce a narrow type from Object.keys, so we have to help. 36 | cmd = Object.keys(cmds).find((_cmd) => !!args[_cmd]); 37 | 38 | if (cmd === "help") { 39 | cmds[cmd](); 40 | } // no return 41 | else if (cmd === undefined) { 42 | usage(""); 43 | } // no return 44 | else if (typeof args["wallet"] !== "string" || args["wallet"].length === 0) { 45 | // todo: Update usage() to provide a hint that wallet is missing/malformed. 46 | usage(""); // no return 47 | } else { 48 | // One global signer for use with a specific per-chain provider. 49 | // todo: Support a void signer for monitor mode (only) if no wallet was supplied. 50 | const signer = await retrieveSignerFromCLIArgs(); 51 | await cmds[cmd](logger, signer); 52 | } 53 | } 54 | 55 | if (require.main === module) { 56 | // Inject version into process.env so CommonConfig and all subclasses inherit it. 57 | process.env.ACROSS_BOT_VERSION = version; 58 | config(); 59 | 60 | const opts = { 61 | boolean: ["dataworker", "finalizer", "help", "monitor", "relayer"], 62 | string: ["wallet", "keys", "address", "binanceSecretKey"], 63 | default: { wallet: "secret" }, 64 | alias: { h: "help" }, 65 | unknown: usage, 66 | }; 67 | 68 | const args = minimist(process.argv.slice(2), opts); 69 | 70 | let exitCode = 0; 71 | run(args) 72 | .catch(async (error) => { 73 | exitCode = 1; 74 | const stringifiedError = stringifyThrownValue(error); 75 | logger.error({ 76 | at: cmd ?? "unknown process", 77 | message: "There was an execution error!", 78 | error: stringifiedError, 79 | args, 80 | notificationPath: "across-error", 81 | }); 82 | }) 83 | .finally(async () => { 84 | await waitForLogger(logger); 85 | await delay(5); // Wait 5s for logger to flush. 86 | exit(exitCode); 87 | }); 88 | } 89 | -------------------------------------------------------------------------------- /scripts/fetch-addresses.ts: -------------------------------------------------------------------------------- 1 | import { writeFile } from "node:fs/promises"; 2 | import { config } from "dotenv"; 3 | import { addressAdapters, AddressAggregator, Logger } from "../src/utils"; 4 | 5 | const OUTPUT_PATH = "addresses.json"; 6 | 7 | let logger: typeof Logger; 8 | 9 | async function run(): Promise { 10 | const addressList = new AddressAggregator( 11 | [new addressAdapters.risklabs.AddressList({ throwOnError: false })], 12 | logger 13 | ); 14 | const addresses = await addressList.update(); 15 | await writeFile(OUTPUT_PATH, JSON.stringify(addresses, null, 4)); 16 | console.log(`Stored ${addresses.size} addresses at ${OUTPUT_PATH}.`); 17 | 18 | return 0; 19 | } 20 | 21 | if (require.main === module) { 22 | logger = Logger; 23 | config(); // Pull in any .env-configured addresses. 24 | run() 25 | .then((result: number) => { 26 | process.exitCode = result; 27 | }) 28 | .catch((error) => { 29 | console.error("Process exited with", error); 30 | process.exitCode = 127; 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /scripts/runCommand.sh: -------------------------------------------------------------------------------- 1 | 2 | #!/bin/bash 3 | 4 | # Simple script that simply runs a command input as an environment variable. 5 | $COMMAND -------------------------------------------------------------------------------- /scripts/runRelayerOnGoerli.sh: -------------------------------------------------------------------------------- 1 | 2 | #!/bin/bash 3 | echo "Running relayer on Goerli. Will fill any deposits for origin chain [5/421613] to destination chain [421613/5]" 4 | 5 | # Use a nonstandard Redis port to avoid data base corruption on any existing Redis instance 6 | export REDIS_URL=redis://localhost:3636 7 | 8 | export RELAYER_IGNORE_LIMITS="true" 9 | export RELAYER_TOKENS='["0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6"]' 10 | export MAX_RELAYER_DEPOSIT_LOOK_BACK=14400 11 | export NODE_DISABLE_PROVIDER_CACHING=true 12 | export RELAYER_GAS_MULTIPLIER=.01 13 | export RELAYER_MIN_FEE_PCT=.00001 14 | export MAX_BLOCK_LOOK_BACK='{ "1": 20000, "10": 10000, "137": 3499, "288": 4990, "42161": 10000, "5": 10000, "421613": 10000 }' 15 | export HUB_CHAIN_ID=5 16 | export SPOKE_POOL_CHAINS_OVERRIDE="[5, 421613]" 17 | export CHAIN_ID_LIST_OVERRIDE="[5,421613]" 18 | 19 | 20 | ts-node ./index.ts --relayer --wallet mnemonic -------------------------------------------------------------------------------- /scripts/updateAtomicDepositor.ts: -------------------------------------------------------------------------------- 1 | import { ethers, Contract, CHAIN_IDs, ZERO_ADDRESS, isDefined } from "../src/utils"; 2 | import { CONTRACT_ADDRESSES } from "../src/common/ContractAddresses"; 3 | import { askYesNoQuestion } from "./utils"; 4 | 5 | import minimist from "minimist"; 6 | const args = minimist(process.argv.slice(2), { 7 | string: ["chainId", "function", "bridge"], 8 | }); 9 | 10 | const { MAINNET } = CHAIN_IDs; 11 | 12 | // The atomic depositor should not change on each new chain deployment. 13 | const atomicDepositor = new Contract( 14 | CONTRACT_ADDRESSES[MAINNET].atomicDepositor.address, 15 | CONTRACT_ADDRESSES[MAINNET].atomicDepositor.abi 16 | ); 17 | 18 | // Example run: 19 | // ts-node ./scripts/updateAtomicDepositor.ts \ 20 | // --chainId 480 21 | // --function "depositETHTo(address,uint32,bytes)" 22 | // --bridge 0x470458C91978D2d929704489Ad730DC3E3001113 23 | 24 | export async function run(): Promise { 25 | if (!Object.keys(args).includes("chainId")) { 26 | throw new Error("Define `chainId` as the chain you want to connect on"); 27 | } 28 | if (!Object.keys(args).includes("function")) { 29 | throw new Error("Define `function` as the ETH deposit function the atomic depositor should call"); 30 | } 31 | if (!Object.keys(args).includes("bridge")) { 32 | throw new Error("Define `bridge` as address of the canonical bridge which bridges raw ETH to L2"); 33 | } 34 | const feeToken = isDefined(args.feeToken) ? args.feeToken : ZERO_ADDRESS; 35 | const gateway = isDefined(args.gateway) ? args.gateway : ZERO_ADDRESS; 36 | if ( 37 | (feeToken !== ZERO_ADDRESS && gateway === ZERO_ADDRESS) || 38 | (feeToken === ZERO_ADDRESS && gateway !== ZERO_ADDRESS) 39 | ) { 40 | throw new Error("One of the fee token or gateway cannot be defined while the other is not"); 41 | } 42 | console.log( 43 | `Confirmation: target chain ID: ${args.chainId}, function: ${args.function}, bridge address: ${args.bridge}, bridge fee token ${feeToken}, bridge gateway: ${gateway}` 44 | ); 45 | if (!(await askYesNoQuestion("Do these arguments match with your expectations?"))) { 46 | throw new Error("Exiting due to incorrect arguments"); 47 | } 48 | const functionSignatureToWhitelist = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(args.function)).slice(0, 10); 49 | 50 | // We need the bridge address still. 51 | console.log( 52 | `Call ${atomicDepositor.address} with chain ID ${args.chainId}, bridge ${args.bridge}, and function signature ${functionSignatureToWhitelist}` 53 | ); 54 | const calldata = atomicDepositor.interface.encodeFunctionData("whitelistBridge", [ 55 | args.chainId, 56 | [args.bridge, gateway, feeToken, functionSignatureToWhitelist], 57 | ]); 58 | console.log(`Alternatively, call ${atomicDepositor.address} directly with calldata ${calldata}`); 59 | } 60 | 61 | if (require.main === module) { 62 | run() 63 | .then(async () => { 64 | // eslint-disable-next-line no-process-exit 65 | process.exit(0); 66 | }) 67 | .catch(async (error) => { 68 | console.error("Process exited with", error); 69 | // eslint-disable-next-line no-process-exit 70 | process.exit(1); 71 | }); 72 | } 73 | -------------------------------------------------------------------------------- /src/adapter/bridges/BaseBridgeAdapter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Contract, 3 | BigNumber, 4 | EventSearchConfig, 5 | Signer, 6 | getTranslatedTokenAddress, 7 | assert, 8 | isDefined, 9 | EvmAddress, 10 | Address, 11 | } from "../../utils"; 12 | import { SortableEvent } from "../../interfaces"; 13 | 14 | export interface BridgeTransactionDetails { 15 | readonly contract: Contract; 16 | readonly method: string; 17 | readonly args: unknown[]; 18 | readonly value?: BigNumber; 19 | } 20 | 21 | export type BridgeEvent = SortableEvent & { 22 | amount: BigNumber; 23 | }; 24 | 25 | export type BridgeEvents = { [l2Token: string]: BridgeEvent[] }; 26 | 27 | export abstract class BaseBridgeAdapter { 28 | protected l1Bridge: Contract; 29 | protected l2Bridge: Contract; 30 | public gasToken: EvmAddress | undefined; 31 | 32 | constructor( 33 | protected l2chainId: number, 34 | protected hubChainId: number, 35 | protected l1Signer: Signer, 36 | public l1Gateways: EvmAddress[] 37 | ) {} 38 | 39 | abstract constructL1ToL2Txn( 40 | toAddress: Address, 41 | l1Token: EvmAddress, 42 | l2Token: Address, 43 | amount: BigNumber 44 | ): Promise; 45 | 46 | abstract queryL1BridgeInitiationEvents( 47 | l1Token: EvmAddress, 48 | fromAddress: Address, 49 | toAddress: Address, 50 | eventConfig: EventSearchConfig 51 | ): Promise; 52 | 53 | abstract queryL2BridgeFinalizationEvents( 54 | l1Token: EvmAddress, 55 | fromAddress: Address, 56 | toAddress: Address, 57 | eventConfig: EventSearchConfig 58 | ): Promise; 59 | 60 | protected resolveL2TokenAddress(l1Token: EvmAddress): string { 61 | return getTranslatedTokenAddress(l1Token.toAddress(), this.hubChainId, this.l2chainId, false); 62 | } 63 | 64 | protected getL1Bridge(): Contract { 65 | assert(isDefined(this.l1Bridge), "Cannot access L1 Bridge when it is undefined."); 66 | return this.l1Bridge; 67 | } 68 | 69 | protected getL2Bridge(): Contract { 70 | assert(isDefined(this.l2Bridge), "Cannot access L2 Bridge when it is undefined."); 71 | return this.l2Bridge; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/adapter/bridges/BinanceCEXNativeBridge.ts: -------------------------------------------------------------------------------- 1 | import { Contract, BigNumber, Signer, Provider, EvmAddress, assert, bnZero } from "../../utils"; 2 | import { CONTRACT_ADDRESSES } from "../../common"; 3 | import { BridgeTransactionDetails } from "./BaseBridgeAdapter"; 4 | import { BinanceCEXBridge } from "./"; 5 | 6 | export class BinanceCEXNativeBridge extends BinanceCEXBridge { 7 | protected readonly atomicDepositor: Contract; 8 | protected readonly transferProxy: Contract; 9 | constructor( 10 | l2chainId: number, 11 | hubChainId: number, 12 | l1Signer: Signer, 13 | l2SignerOrProvider: Signer | Provider, 14 | l1Token: EvmAddress 15 | ) { 16 | // No L1 gateways needed since no L1 bridge transfers tokens from the EOA. 17 | super(l2chainId, hubChainId, l1Signer, l2SignerOrProvider, l1Token); 18 | 19 | const { address: atomicDepositorAddress, abi: atomicDepositorAbi } = 20 | CONTRACT_ADDRESSES[this.hubChainId].atomicDepositor; 21 | this.atomicDepositor = new Contract(atomicDepositorAddress, atomicDepositorAbi, this.l1Signer); 22 | 23 | const { address: transferProxyAddress, abi: transferProxyAbi } = 24 | CONTRACT_ADDRESSES[this.hubChainId].atomicDepositorTransferProxy; 25 | this.transferProxy = new Contract(transferProxyAddress, transferProxyAbi); 26 | // Overwrite the token symbol to ETH. 27 | this.tokenSymbol = "ETH"; 28 | // Also overwrite the l1Gateway to the atomic depositor. 29 | this.l1Gateways = [EvmAddress.from(this.atomicDepositor.address)]; 30 | } 31 | 32 | override async constructL1ToL2Txn( 33 | _toAddress: EvmAddress, 34 | l1Token: EvmAddress, 35 | _l2Token: EvmAddress, 36 | amount: BigNumber 37 | ): Promise { 38 | assert(l1Token.toAddress() === this.getL1Bridge().address); 39 | // Fetch the deposit address from the binance API. 40 | 41 | const binanceApiClient = await this.getBinanceClient(); 42 | const depositAddress = await binanceApiClient.depositAddress({ 43 | coin: this.tokenSymbol, 44 | network: "ETH", 45 | }); 46 | // We need to call the atomic depositor, which calls the transfer proxy, which transfers raw ETH to the deposit Address. 47 | const bridgeCalldata = this.transferProxy.interface.encodeFunctionData("transfer", [depositAddress.address]); 48 | 49 | // Once we have the address, create a transfer to the deposit address routed via the multicall handler. Then, call the atomic depositor. 50 | return { 51 | method: "bridgeWeth", 52 | args: [this.l2chainId, amount, amount, bnZero, bridgeCalldata], 53 | contract: this.atomicDepositor, 54 | }; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/adapter/bridges/BlastBridge.ts: -------------------------------------------------------------------------------- 1 | import { Contract, BigNumber, paginatedEventQuery, EventSearchConfig, Signer, Provider, EvmAddress } from "../../utils"; 2 | import { CONTRACT_ADDRESSES } from "../../common"; 3 | import { BaseBridgeAdapter, BridgeTransactionDetails, BridgeEvents } from "./BaseBridgeAdapter"; 4 | import { processEvent } from "../utils"; 5 | 6 | export class BlastBridge extends BaseBridgeAdapter { 7 | private readonly l2Gas = 200000; 8 | 9 | constructor(l2chainId: number, hubChainId: number, l1Signer: Signer, l2SignerOrProvider: Signer | Provider) { 10 | const { address: l1Address, abi: l1Abi } = CONTRACT_ADDRESSES[hubChainId].blastBridge; 11 | const { address: l2Address, abi: l2Abi } = CONTRACT_ADDRESSES[l2chainId].blastBridge; 12 | super(l2chainId, hubChainId, l1Signer, [EvmAddress.from(l1Address)]); 13 | 14 | this.l1Bridge = new Contract(l1Address, l1Abi, l1Signer); 15 | this.l2Bridge = new Contract(l2Address, l2Abi, l2SignerOrProvider); 16 | } 17 | 18 | async constructL1ToL2Txn( 19 | toAddress: EvmAddress, 20 | l1Token: EvmAddress, 21 | l2Token: EvmAddress, 22 | amount: BigNumber 23 | ): Promise { 24 | return Promise.resolve({ 25 | contract: this.getL1Bridge(), 26 | method: "bridgeERC20", 27 | args: [l1Token.toAddress(), l2Token.toAddress(), amount, this.l2Gas, "0x"], 28 | }); 29 | } 30 | 31 | async queryL1BridgeInitiationEvents( 32 | l1Token: EvmAddress, 33 | fromAddress: EvmAddress, 34 | toAddress: EvmAddress, 35 | eventConfig: EventSearchConfig 36 | ): Promise { 37 | const l1Bridge = this.getL1Bridge(); 38 | const events = await paginatedEventQuery( 39 | l1Bridge, 40 | l1Bridge.filters.ERC20BridgeInitiated(l1Token.toAddress(), undefined, fromAddress.toAddress()), 41 | eventConfig 42 | ); 43 | return { 44 | [this.resolveL2TokenAddress(l1Token)]: events.map((event) => processEvent(event, "amount")), 45 | }; 46 | } 47 | 48 | async queryL2BridgeFinalizationEvents( 49 | l1Token: EvmAddress, 50 | fromAddress: EvmAddress, 51 | toAddress: EvmAddress, 52 | eventConfig: EventSearchConfig 53 | ): Promise { 54 | const l2Bridge = this.getL2Bridge(); 55 | const events = await paginatedEventQuery( 56 | l2Bridge, 57 | l2Bridge.filters.ERC20BridgeFinalized(undefined, l1Token.toAddress(), fromAddress.toAddress()), 58 | eventConfig 59 | ); 60 | return { 61 | [this.resolveL2TokenAddress(l1Token)]: events.map((event) => processEvent(event, "amount")), 62 | }; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/adapter/bridges/DaiOptimismBridge.ts: -------------------------------------------------------------------------------- 1 | import { Contract, Signer, Provider, EvmAddress } from "../../utils"; 2 | import { CONTRACT_ADDRESSES } from "../../common"; 3 | import { OpStackDefaultERC20Bridge } from "./OpStackDefaultErc20Bridge"; 4 | 5 | export class DaiOptimismBridge extends OpStackDefaultERC20Bridge { 6 | constructor(l2chainId: number, hubChainId: number, l1Signer: Signer, l2SignerOrProvider: Signer | Provider) { 7 | const { address: l1Address, abi: l1Abi } = CONTRACT_ADDRESSES[hubChainId].daiOptimismBridge; 8 | super(l2chainId, hubChainId, l1Signer, l2SignerOrProvider); 9 | 10 | this.l1Bridge = new Contract(l1Address, l1Abi, l1Signer); 11 | 12 | const { address: l2Address, abi: l2Abi } = CONTRACT_ADDRESSES[l2chainId].daiOptimismBridge; 13 | this.l2Bridge = new Contract(l2Address, l2Abi, l2SignerOrProvider); 14 | 15 | // Since we define this bridge as an extension of the OpStackDefaultERC20Bridge, 16 | // we will need to overwrite the l1Gateways parameter, since when calling the super() 17 | // constructor, l1Gateways will be incorrectly set to the OVM standard bridge address, 18 | // not the DaiOptimismBridgeAddress. 19 | this.l1Gateways = [EvmAddress.from(l1Address)]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/adapter/bridges/LineaBridge.ts: -------------------------------------------------------------------------------- 1 | import { Contract, BigNumber, paginatedEventQuery, Signer, EventSearchConfig, Provider, EvmAddress } from "../../utils"; 2 | import { CONTRACT_ADDRESSES } from "../../common"; 3 | import { BridgeTransactionDetails, BaseBridgeAdapter, BridgeEvents } from "./BaseBridgeAdapter"; 4 | import { processEvent } from "../utils"; 5 | 6 | export class LineaBridge extends BaseBridgeAdapter { 7 | constructor(l2chainId: number, hubChainId: number, l1Signer: Signer, l2SignerOrProvider: Signer | Provider) { 8 | const { address: l1Address, abi: l1Abi } = CONTRACT_ADDRESSES[hubChainId].lineaL1TokenBridge; 9 | const { address: l2Address, abi: l2Abi } = CONTRACT_ADDRESSES[l2chainId].lineaL2TokenBridge; 10 | super(l2chainId, hubChainId, l1Signer, [EvmAddress.from(l1Address)]); 11 | 12 | this.l1Bridge = new Contract(l1Address, l1Abi, l1Signer); 13 | this.l2Bridge = new Contract(l2Address, l2Abi, l2SignerOrProvider); 14 | } 15 | 16 | async constructL1ToL2Txn( 17 | toAddress: EvmAddress, 18 | l1Token: EvmAddress, 19 | l2Token: EvmAddress, 20 | amount: BigNumber 21 | ): Promise { 22 | return Promise.resolve({ 23 | contract: this.getL1Bridge(), 24 | method: "bridgeToken", 25 | args: [l1Token.toAddress(), amount, toAddress.toAddress()], 26 | }); 27 | } 28 | 29 | async queryL1BridgeInitiationEvents( 30 | l1Token: EvmAddress, 31 | fromAddress: EvmAddress, 32 | toAddress: EvmAddress, 33 | eventConfig: EventSearchConfig 34 | ): Promise { 35 | const events = await paginatedEventQuery( 36 | this.getL1Bridge(), 37 | this.getL1Bridge().filters.BridgingInitiatedV2(undefined, toAddress.toAddress(), l1Token.toAddress()), 38 | eventConfig 39 | ); 40 | return { 41 | [this.resolveL2TokenAddress(l1Token)]: events.map((event) => processEvent(event, "amount")), 42 | }; 43 | } 44 | 45 | async queryL2BridgeFinalizationEvents( 46 | l1Token: EvmAddress, 47 | fromAddress: EvmAddress, 48 | toAddress: EvmAddress, 49 | eventConfig: EventSearchConfig 50 | ): Promise { 51 | const events = await paginatedEventQuery( 52 | this.getL2Bridge(), 53 | this.getL2Bridge().filters.BridgingFinalizedV2(l1Token.toAddress(), undefined, undefined, toAddress.toAddress()), 54 | eventConfig 55 | ); 56 | // There is no "from" field in this event, so we set it to the L2 token received. 57 | return { 58 | [this.resolveL2TokenAddress(l1Token)]: events.map((event) => processEvent(event, "amount")), 59 | }; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/adapter/bridges/OpStackDefaultErc20Bridge.ts: -------------------------------------------------------------------------------- 1 | import { Contract, BigNumber, paginatedEventQuery, Signer, EventSearchConfig, Provider, EvmAddress } from "../../utils"; 2 | import { CONTRACT_ADDRESSES } from "../../common"; 3 | import { BridgeTransactionDetails, BaseBridgeAdapter, BridgeEvents } from "./BaseBridgeAdapter"; 4 | import { processEvent } from "../utils"; 5 | 6 | export class OpStackDefaultERC20Bridge extends BaseBridgeAdapter { 7 | private readonly l2Gas = 200000; 8 | 9 | constructor(l2chainId: number, hubChainId: number, l1Signer: Signer, l2SignerOrProvider: Signer | Provider) { 10 | super(l2chainId, hubChainId, l1Signer, [ 11 | EvmAddress.from(CONTRACT_ADDRESSES[hubChainId][`ovmStandardBridge_${l2chainId}`].address), 12 | ]); 13 | 14 | const { address: l1Address, abi: l1Abi } = CONTRACT_ADDRESSES[hubChainId][`ovmStandardBridge_${l2chainId}`]; 15 | this.l1Bridge = new Contract(l1Address, l1Abi, l1Signer); 16 | 17 | const { address: l2Address, abi: l2Abi } = CONTRACT_ADDRESSES[l2chainId].ovmStandardBridge; 18 | this.l2Bridge = new Contract(l2Address, l2Abi, l2SignerOrProvider); 19 | } 20 | 21 | async constructL1ToL2Txn( 22 | toAddress: EvmAddress, 23 | l1Token: EvmAddress, 24 | l2Token: EvmAddress, 25 | amount: BigNumber 26 | ): Promise { 27 | return Promise.resolve({ 28 | contract: this.getL1Bridge(), 29 | method: "depositERC20", 30 | args: [l1Token.toAddress(), l2Token.toAddress(), amount, this.l2Gas, "0x"], 31 | }); 32 | } 33 | 34 | async queryL1BridgeInitiationEvents( 35 | l1Token: EvmAddress, 36 | fromAddress: EvmAddress, 37 | toAddress: EvmAddress, 38 | eventConfig: EventSearchConfig 39 | ): Promise { 40 | const events = await paginatedEventQuery( 41 | this.getL1Bridge(), 42 | this.getL1Bridge().filters.ERC20DepositInitiated(l1Token.toAddress(), undefined, fromAddress.toAddress()), 43 | eventConfig 44 | ); 45 | return { 46 | [this.resolveL2TokenAddress(l1Token)]: events.map((event) => processEvent(event, "_amount")), 47 | }; 48 | } 49 | 50 | async queryL2BridgeFinalizationEvents( 51 | l1Token: EvmAddress, 52 | fromAddress: EvmAddress, 53 | toAddress: EvmAddress, 54 | eventConfig: EventSearchConfig 55 | ): Promise { 56 | const events = await paginatedEventQuery( 57 | this.getL2Bridge(), 58 | this.getL2Bridge().filters.DepositFinalized(l1Token.toAddress(), undefined, fromAddress.toAddress()), 59 | eventConfig 60 | ); 61 | return { 62 | [this.resolveL2TokenAddress(l1Token)]: events.map((event) => processEvent(event, "_amount")), 63 | }; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/adapter/bridges/OpStackUSDCBridge.ts: -------------------------------------------------------------------------------- 1 | import { Contract, BigNumber, paginatedEventQuery, EventSearchConfig, Signer, Provider, EvmAddress } from "../../utils"; 2 | import { CONTRACT_ADDRESSES } from "../../common"; 3 | import { BaseBridgeAdapter, BridgeTransactionDetails, BridgeEvents } from "./BaseBridgeAdapter"; 4 | import { processEvent } from "../utils"; 5 | 6 | export class OpStackUSDCBridge extends BaseBridgeAdapter { 7 | private readonly l2Gas = 200000; 8 | 9 | constructor(l2chainId: number, hubChainId: number, l1Signer: Signer, l2SignerOrProvider: Signer | Provider) { 10 | const { address: l1Address, abi: l1Abi } = CONTRACT_ADDRESSES[hubChainId][`opUSDCBridge_${l2chainId}`]; 11 | const { address: l2Address, abi: l2Abi } = CONTRACT_ADDRESSES[l2chainId].opUSDCBridge; 12 | super(l2chainId, hubChainId, l1Signer, [EvmAddress.from(l1Address)]); 13 | 14 | this.l1Bridge = new Contract(l1Address, l1Abi, l1Signer); 15 | this.l2Bridge = new Contract(l2Address, l2Abi, l2SignerOrProvider); 16 | } 17 | 18 | async constructL1ToL2Txn( 19 | toAddress: EvmAddress, 20 | _l1Token: EvmAddress, 21 | _l2Token: EvmAddress, 22 | amount: BigNumber 23 | ): Promise { 24 | return Promise.resolve({ 25 | contract: this.getL1Bridge(), 26 | method: "sendMessage", 27 | args: [toAddress.toAddress(), amount, this.l2Gas], 28 | }); 29 | } 30 | 31 | async queryL1BridgeInitiationEvents( 32 | l1Token: EvmAddress, 33 | _fromAddress: EvmAddress, 34 | toAddress: EvmAddress, 35 | eventConfig: EventSearchConfig 36 | ): Promise { 37 | const l1Bridge = this.getL1Bridge(); 38 | const events = await paginatedEventQuery( 39 | l1Bridge, 40 | l1Bridge.filters.MessageSent(undefined, toAddress.toAddress()), 41 | eventConfig 42 | ); 43 | return { 44 | [this.resolveL2TokenAddress(l1Token)]: events.map((event) => processEvent(event, "_amount")), 45 | }; 46 | } 47 | 48 | async queryL2BridgeFinalizationEvents( 49 | l1Token: EvmAddress, 50 | _fromAddress: EvmAddress, 51 | toAddress: EvmAddress, 52 | eventConfig: EventSearchConfig 53 | ): Promise { 54 | const l2Bridge = this.getL2Bridge(); 55 | const events = await paginatedEventQuery( 56 | l2Bridge, 57 | l2Bridge.filters.MessageReceived(undefined, toAddress.toAddress()), 58 | eventConfig 59 | ); 60 | return { 61 | [this.resolveL2TokenAddress(l1Token)]: events.map((event) => processEvent(event, "_amount")), 62 | }; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/adapter/bridges/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./DaiOptimismBridge"; 2 | export * from "./SnxOptimismBridge"; 3 | export * from "./BaseBridgeAdapter"; 4 | export * from "./BinanceCEXBridge"; 5 | export * from "./BinanceCEXNativeBridge"; 6 | export * from "./UsdcTokenSplitterBridge"; 7 | export * from "./OpStackWethBridge"; 8 | export * from "./ArbitrumOrbitBridge"; 9 | export * from "./LineaBridge"; 10 | export * from "./PolygonERC20Bridge"; 11 | export * from "./PolygonWethBridge"; 12 | export * from "./OpStackDefaultErc20Bridge"; 13 | export * from "./LineaWethBridge"; 14 | export * from "./BlastBridge"; 15 | export * from "./ScrollERC20Bridge"; 16 | export * from "./OpStackUSDCBridge"; 17 | export * from "./UsdcCCTPBridge"; 18 | export * from "./ZKStackBridge"; 19 | export * from "./ZKStackUSDCBridge"; 20 | export * from "./ZKStackWethBridge"; 21 | export * from "./SolanaUsdcCCTPBridge"; 22 | export * from "./OFTBridge"; 23 | -------------------------------------------------------------------------------- /src/adapter/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./BaseChainAdapter"; 2 | export { aboveAllowanceThreshold } from "./utils"; 3 | -------------------------------------------------------------------------------- /src/adapter/l2Bridges/BaseL2BridgeAdapter.ts: -------------------------------------------------------------------------------- 1 | import { AugmentedTransaction } from "../../clients/TransactionClient"; 2 | import { BigNumber, Contract, EventSearchConfig, Provider, Signer, EvmAddress, Address } from "../../utils"; 3 | 4 | export abstract class BaseL2BridgeAdapter { 5 | protected l2Bridge: Contract; 6 | protected l1Bridge: Contract; 7 | 8 | constructor( 9 | protected l2chainId: number, 10 | protected hubChainId: number, 11 | protected l2Signer: Signer, 12 | protected l1Provider: Provider | Signer, 13 | protected l1Token: EvmAddress 14 | ) {} 15 | 16 | abstract constructWithdrawToL1Txns( 17 | toAddress: Address, 18 | l2Token: Address, 19 | l1Token: EvmAddress, 20 | amount: BigNumber 21 | ): Promise; 22 | 23 | abstract getL2PendingWithdrawalAmount( 24 | l2EventSearchConfig: EventSearchConfig, 25 | l1EventSearchConfig: EventSearchConfig, 26 | fromAddress: Address, 27 | l2Token: Address 28 | ): Promise; 29 | 30 | // Note: Returns `EvmAddress`es since upstream BaseChainAdapter impl. performs evm-style approvals 31 | // Bridges that require specific approvals should override this method. 32 | public requiredTokenApprovals(): { token: EvmAddress; bridge: EvmAddress }[] { 33 | return []; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/adapter/l2Bridges/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ArbitrumOrbitBridge"; 2 | export * from "./BaseL2BridgeAdapter"; 3 | export * from "./BinanceCEXBridge"; 4 | export * from "./OpStackBridge"; 5 | export * from "./OpStackUSDCBridge"; 6 | export * from "./OpStackWethBridge"; 7 | -------------------------------------------------------------------------------- /src/adapter/utils.ts: -------------------------------------------------------------------------------- 1 | import { TOKEN_APPROVALS_TO_FIRST_ZERO } from "../common"; 2 | import { 3 | BigNumber, 4 | spreadEventWithBlockNumber, 5 | toBN, 6 | MAX_SAFE_ALLOWANCE, 7 | runTransaction, 8 | bnZero, 9 | getNetworkName, 10 | blockExplorerLink, 11 | mapAsync, 12 | winston, 13 | Address, 14 | } from "../utils"; 15 | import { BridgeEvent } from "./bridges/BaseBridgeAdapter"; 16 | import { Log, SortableEvent } from "../interfaces"; 17 | import { ExpandedERC20 } from "@across-protocol/contracts"; 18 | 19 | export { 20 | matchL2EthDepositAndWrapEvents, 21 | getAllowanceCacheKey, 22 | getTokenAllowanceFromCache, 23 | setTokenAllowanceInCache, 24 | } from "../clients/bridges/utils"; 25 | 26 | export { getL2TokenAllowanceFromCache, setL2TokenAllowanceInCache } from "../clients/bridges/utils"; 27 | 28 | export function aboveAllowanceThreshold(allowance: BigNumber): boolean { 29 | return allowance.gte(toBN(MAX_SAFE_ALLOWANCE).div(2)); 30 | } 31 | 32 | export async function approveTokens( 33 | tokens: { token: ExpandedERC20; bridges: Address[] }[], 34 | approvalChainId: number, 35 | hubChainId: number, 36 | logger: winston.Logger 37 | ): Promise { 38 | const bridges = tokens.flatMap(({ token, bridges }) => bridges.map((bridge) => ({ token, bridge }))); 39 | const approvalMarkdwn = await mapAsync(bridges, async ({ token, bridge }) => { 40 | const txs = []; 41 | if (approvalChainId == hubChainId) { 42 | if (TOKEN_APPROVALS_TO_FIRST_ZERO[hubChainId]?.includes(token.address)) { 43 | txs.push(await runTransaction(logger, token, "approve", [bridge.toAddress(), bnZero])); 44 | } 45 | } 46 | txs.push(await runTransaction(logger, token, "approve", [bridge.toAddress(), MAX_SAFE_ALLOWANCE])); 47 | const receipts = await Promise.all(txs.map((tx) => tx.wait())); 48 | const networkName = getNetworkName(approvalChainId); 49 | 50 | let internalMrkdwn = 51 | ` - Approved token bridge ${blockExplorerLink(bridge.toAddress(), approvalChainId)} ` + 52 | `to spend ${await token.symbol()} ${blockExplorerLink(token.address, approvalChainId)} on ${networkName}.` + 53 | `tx: ${blockExplorerLink(receipts.at(-1).transactionHash, approvalChainId)}`; 54 | 55 | if (receipts.length > 1) { 56 | internalMrkdwn += ` tx (to zero approval first): ${blockExplorerLink(receipts[0].transactionHash, hubChainId)}`; 57 | } 58 | return internalMrkdwn; 59 | }); 60 | return ["*Approval transactions:*", ...approvalMarkdwn].join("\n"); 61 | } 62 | 63 | export function processEvent(event: Log, amountField: string): BridgeEvent { 64 | const eventSpread = spreadEventWithBlockNumber(event) as SortableEvent; 65 | return { 66 | ...eventSpread, 67 | amount: eventSpread[amountField], 68 | }; 69 | } 70 | -------------------------------------------------------------------------------- /src/caching/RedisCache.ts: -------------------------------------------------------------------------------- 1 | import { interfaces, constants } from "@across-protocol/sdk"; 2 | import { RedisClient, setRedisKey } from "../utils"; 3 | 4 | /** 5 | * RedisCache is a caching mechanism that uses Redis as the backing store. It is used by the 6 | * Across SDK to cache data that is expensive to compute or retrieve from the blockchain. It 7 | * is designed to use the `CachingMechanismInterface` interface so that it can be used as a 8 | * drop-in in the SDK without the SDK needing to reason about the implementation details. 9 | */ 10 | export class RedisCache implements interfaces.CachingMechanismInterface { 11 | /** 12 | * The redisClient is the redis client that is used to communicate with the redis server. 13 | * It is instantiated lazily when the `instantiate` method is called. 14 | */ 15 | private redisClient: RedisClient | undefined; 16 | 17 | /** 18 | * The constructor takes in the redisUrl and an optional logger. 19 | * @param redisUrl The URL of the redis server to connect to. 20 | * @param logger The logger to use to log debug messages. 21 | */ 22 | constructor(redisClient: RedisClient) { 23 | this.redisClient = redisClient; 24 | } 25 | 26 | public async get(key: string): Promise { 27 | // Get the value from redis. 28 | return this.redisClient.get(key) as T; 29 | } 30 | 31 | public async set(key: string, value: T, ttl: number = constants.DEFAULT_CACHING_TTL): Promise { 32 | // Call the setRedisKey function to set the value in redis. 33 | await setRedisKey(key, String(value), this.redisClient, ttl); 34 | // Return key to indicate that the value was set successfully. 35 | return key; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/clients/bridges/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./AdapterManager"; 2 | export * from "./CrossChainTransferClient"; 3 | -------------------------------------------------------------------------------- /src/clients/index.ts: -------------------------------------------------------------------------------- 1 | import { clients } from "@across-protocol/sdk"; 2 | 3 | export type SpokePoolClient = clients.SpokePoolClient; 4 | export type EVMSpokePoolClient = clients.EVMSpokePoolClient; 5 | export type SpokePoolUpdate = clients.SpokePoolUpdate; 6 | export const { EVMSpokePoolClient, SpokePoolClient, SVMSpokePoolClient } = clients; 7 | 8 | export { IndexedSpokePoolClient, SpokePoolClientMessage } from "./SpokePoolClient"; 9 | export class BundleDataClient extends clients.BundleDataClient.BundleDataClient {} 10 | 11 | export * from "./BalanceAllocator"; 12 | export * from "./HubPoolClient"; 13 | export * from "./ConfigStoreClient"; 14 | export * from "./MultiCallerClient"; 15 | export * from "./ProfitClient"; 16 | export * from "./TokenClient"; 17 | export * from "./TokenTransferClient"; 18 | export * from "./TransactionClient"; 19 | export * from "./InventoryClient"; 20 | export * from "./AcrossAPIClient"; 21 | -------------------------------------------------------------------------------- /src/common/abi/ArbSysL2.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "anonymous": false, 4 | "inputs": [ 5 | { 6 | "indexed": false, 7 | "internalType": "address", 8 | "name": "caller", 9 | "type": "address" 10 | }, 11 | { 12 | "indexed": true, 13 | "internalType": "address", 14 | "name": "destination", 15 | "type": "address" 16 | }, 17 | { 18 | "indexed": true, 19 | "internalType": "uint256", 20 | "name": "hash", 21 | "type": "uint256" 22 | }, 23 | { 24 | "indexed": true, 25 | "internalType": "uint256", 26 | "name": "position", 27 | "type": "uint256" 28 | }, 29 | { 30 | "indexed": false, 31 | "internalType": "uint256", 32 | "name": "arbBlockNum", 33 | "type": "uint256" 34 | }, 35 | { 36 | "indexed": false, 37 | "internalType": "uint256", 38 | "name": "ethBlockNum", 39 | "type": "uint256" 40 | }, 41 | { 42 | "indexed": false, 43 | "internalType": "uint256", 44 | "name": "timestamp", 45 | "type": "uint256" 46 | }, 47 | { 48 | "indexed": false, 49 | "internalType": "uint256", 50 | "name": "callvalue", 51 | "type": "uint256" 52 | }, 53 | { 54 | "indexed": false, 55 | "internalType": "bytes", 56 | "name": "data", 57 | "type": "bytes" 58 | } 59 | ], 60 | "name": "L2ToL1Tx", 61 | "type": "event" 62 | }, 63 | { 64 | "inputs": [ 65 | { 66 | "internalType": "address", 67 | "name": "destination", 68 | "type": "address" 69 | } 70 | ], 71 | "name": "withdrawEth", 72 | "outputs": [ 73 | { 74 | "internalType": "uint256", 75 | "name": "", 76 | "type": "uint256" 77 | } 78 | ], 79 | "stateMutability": "payable", 80 | "type": "function" 81 | } 82 | ] 83 | -------------------------------------------------------------------------------- /src/common/abi/ArbitrumErc20GatewayL1.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "anonymous": false, 4 | "inputs": [ 5 | { "indexed": false, "internalType": "address", "name": "l1Token", "type": "address" }, 6 | { "indexed": true, "internalType": "address", "name": "_from", "type": "address" }, 7 | { "indexed": true, "internalType": "address", "name": "_to", "type": "address" }, 8 | { "indexed": true, "internalType": "uint256", "name": "_sequenceNumber", "type": "uint256" }, 9 | { "indexed": false, "internalType": "uint256", "name": "_amount", "type": "uint256" } 10 | ], 11 | "name": "DepositInitiated", 12 | "type": "event" 13 | }, 14 | { 15 | "anonymous": false, 16 | "inputs": [ 17 | { "indexed": false, "internalType": "address", "name": "l1Token", "type": "address" }, 18 | { "indexed": true, "internalType": "address", "name": "_from", "type": "address" }, 19 | { "indexed": true, "internalType": "address", "name": "_to", "type": "address" }, 20 | { "indexed": true, "internalType": "uint256", "name": "_exitNum", "type": "uint256" }, 21 | { "indexed": false, "internalType": "uint256", "name": "_amount", "type": "uint256" } 22 | ], 23 | "name": "WithdrawalFinalized", 24 | "type": "event" 25 | } 26 | ] 27 | -------------------------------------------------------------------------------- /src/common/abi/ArbitrumErc20GatewayL2.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "anonymous": false, 4 | "inputs": [ 5 | { "indexed": true, "internalType": "address", "name": "l1Token", "type": "address" }, 6 | { "indexed": true, "internalType": "address", "name": "from", "type": "address" }, 7 | { "indexed": true, "internalType": "address", "name": "to", "type": "address" }, 8 | { "indexed": false, "internalType": "uint256", "name": "amount", "type": "uint256" } 9 | ], 10 | "name": "DepositFinalized", 11 | "type": "event" 12 | }, 13 | { 14 | "anonymous": false, 15 | "inputs": [ 16 | { 17 | "indexed": false, 18 | "internalType": "address", 19 | "name": "l1Token", 20 | "type": "address" 21 | }, 22 | { 23 | "indexed": true, 24 | "internalType": "address", 25 | "name": "_from", 26 | "type": "address" 27 | }, 28 | { 29 | "indexed": true, 30 | "internalType": "address", 31 | "name": "_to", 32 | "type": "address" 33 | }, 34 | { 35 | "indexed": true, 36 | "internalType": "uint256", 37 | "name": "_l2ToL1Id", 38 | "type": "uint256" 39 | }, 40 | { 41 | "indexed": false, 42 | "internalType": "uint256", 43 | "name": "_exitNum", 44 | "type": "uint256" 45 | }, 46 | { 47 | "indexed": false, 48 | "internalType": "uint256", 49 | "name": "_amount", 50 | "type": "uint256" 51 | } 52 | ], 53 | "name": "WithdrawalInitiated", 54 | "type": "event" 55 | }, 56 | { 57 | "inputs": [ 58 | { "internalType": "address", "name": "_token", "type": "address" }, 59 | { "internalType": "address", "name": "_to", "type": "address" }, 60 | { "internalType": "uint256", "name": "_amount", "type": "uint256" }, 61 | { "internalType": "bytes", "name": "_data", "type": "bytes" } 62 | ], 63 | "name": "outboundTransfer", 64 | "outputs": [{ "internalType": "bytes", "name": "", "type": "bytes" }], 65 | "stateMutability": "payable", 66 | "type": "function" 67 | } 68 | ] 69 | -------------------------------------------------------------------------------- /src/common/abi/ArbitrumErc20GatewayRouterL1.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { "internalType": "address", "name": "_token", "type": "address" }, 5 | { "internalType": "address", "name": "_to", "type": "address" }, 6 | { "internalType": "uint256", "name": "_amount", "type": "uint256" }, 7 | { "internalType": "uint256", "name": "_maxGas", "type": "uint256" }, 8 | { "internalType": "uint256", "name": "_gasPriceBid", "type": "uint256" }, 9 | { "internalType": "bytes", "name": "_data", "type": "bytes" } 10 | ], 11 | "name": "outboundTransfer", 12 | "outputs": [{ "internalType": "bytes", "name": "", "type": "bytes" }], 13 | "stateMutability": "payable", 14 | "type": "function" 15 | } 16 | ] 17 | -------------------------------------------------------------------------------- /src/common/abi/ArbitrumErc20GatewayRouterL2.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "anonymous": false, 4 | "inputs": [ 5 | { 6 | "indexed": true, 7 | "internalType": "address", 8 | "name": "token", 9 | "type": "address" 10 | }, 11 | { 12 | "indexed": true, 13 | "internalType": "address", 14 | "name": "_userFrom", 15 | "type": "address" 16 | }, 17 | { 18 | "indexed": true, 19 | "internalType": "address", 20 | "name": "_userTo", 21 | "type": "address" 22 | }, 23 | { 24 | "indexed": false, 25 | "internalType": "address", 26 | "name": "gateway", 27 | "type": "address" 28 | } 29 | ], 30 | "name": "TransferRouted", 31 | "type": "event" 32 | }, 33 | { 34 | "inputs": [ 35 | { "internalType": "address", "name": "_token", "type": "address" }, 36 | { "internalType": "address", "name": "_to", "type": "address" }, 37 | { "internalType": "uint256", "name": "_amount", "type": "uint256" }, 38 | { "internalType": "bytes", "name": "_data", "type": "bytes" } 39 | ], 40 | "name": "outboundTransfer", 41 | "outputs": [{ "internalType": "bytes", "name": "", "type": "bytes" }], 42 | "stateMutability": "payable", 43 | "type": "function" 44 | }, 45 | { 46 | "inputs": [ 47 | { 48 | "internalType": "address", 49 | "name": "_token", 50 | "type": "address" 51 | } 52 | ], 53 | "name": "getGateway", 54 | "outputs": [ 55 | { 56 | "internalType": "address", 57 | "name": "gateway", 58 | "type": "address" 59 | } 60 | ], 61 | "stateMutability": "view", 62 | "type": "function" 63 | } 64 | ] 65 | -------------------------------------------------------------------------------- /src/common/abi/ArbitrumOutbox.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { "internalType": "bytes32[]", "name": "proof", "type": "bytes32[]" }, 5 | { "internalType": "uint256", "name": "index", "type": "uint256" }, 6 | { "internalType": "address", "name": "l2Sender", "type": "address" }, 7 | { "internalType": "address", "name": "to", "type": "address" }, 8 | { "internalType": "uint256", "name": "l2Block", "type": "uint256" }, 9 | { "internalType": "uint256", "name": "l1Block", "type": "uint256" }, 10 | { "internalType": "uint256", "name": "l2Timestamp", "type": "uint256" }, 11 | { "internalType": "uint256", "name": "value", "type": "uint256" }, 12 | { "internalType": "bytes", "name": "data", "type": "bytes" } 13 | ], 14 | "name": "executeTransaction", 15 | "outputs": [], 16 | "stateMutability": "nonpayable", 17 | "type": "function" 18 | } 19 | ] 20 | -------------------------------------------------------------------------------- /src/common/abi/AtomicDepositorTransferProxy.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "address", 6 | "name": "to", 7 | "type": "address" 8 | } 9 | ], 10 | "name": "transfer", 11 | "outputs": [], 12 | "stateMutability": "payable", 13 | "type": "function" 14 | } 15 | ] 16 | -------------------------------------------------------------------------------- /src/common/abi/BlastDaiRetriever.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "uint256", 6 | "name": "_requestId", 7 | "type": "uint256" 8 | }, 9 | { 10 | "internalType": "uint256", 11 | "name": "_hintId", 12 | "type": "uint256" 13 | } 14 | ], 15 | "name": "retrieve", 16 | "outputs": [], 17 | "stateMutability": "nonpayable", 18 | "type": "function" 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /src/common/abi/BlastOptimismPortal.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "anonymous": false, 4 | "inputs": [ 5 | { "indexed": true, "internalType": "bytes32", "name": "withdrawalHash", "type": "bytes32" }, 6 | { "indexed": true, "internalType": "address", "name": "from", "type": "address" }, 7 | { "indexed": true, "internalType": "address", "name": "to", "type": "address" }, 8 | { "indexed": false, "internalType": "uint256", "name": "requestId", "type": "uint256" } 9 | ], 10 | "name": "WithdrawalProven", 11 | "type": "event" 12 | }, 13 | { 14 | "inputs": [ 15 | { "internalType": "uint256", "name": "hintId", "type": "uint256" }, 16 | { 17 | "components": [ 18 | { "internalType": "uint256", "name": "nonce", "type": "uint256" }, 19 | { "internalType": "address", "name": "sender", "type": "address" }, 20 | { "internalType": "address", "name": "target", "type": "address" }, 21 | { "internalType": "uint256", "name": "value", "type": "uint256" }, 22 | { "internalType": "uint256", "name": "gasLimit", "type": "uint256" }, 23 | { "internalType": "bytes", "name": "data", "type": "bytes" } 24 | ], 25 | "internalType": "struct Types.WithdrawalTransaction", 26 | "name": "_tx", 27 | "type": "tuple" 28 | } 29 | ], 30 | "name": "finalizeWithdrawalTransaction", 31 | "outputs": [], 32 | "stateMutability": "nonpayable", 33 | "type": "function" 34 | } 35 | ] 36 | -------------------------------------------------------------------------------- /src/common/abi/BlastYieldManager.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { "internalType": "uint256", "name": "_requestId", "type": "uint256" }, 5 | { "internalType": "uint256", "name": "_start", "type": "uint256" }, 6 | { "internalType": "uint256", "name": "_end", "type": "uint256" } 7 | ], 8 | "name": "findCheckpointHint", 9 | "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], 10 | "stateMutability": "view", 11 | "type": "function" 12 | }, 13 | { 14 | "inputs": [], 15 | "name": "getLastCheckpointId", 16 | "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], 17 | "stateMutability": "view", 18 | "type": "function" 19 | }, 20 | { 21 | "inputs": [], 22 | "name": "getLastFinalizedRequestId", 23 | "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], 24 | "stateMutability": "view", 25 | "type": "function" 26 | }, 27 | { 28 | "inputs": [ 29 | { "internalType": "uint256", "name": "_requestId", "type": "uint256" }, 30 | { "internalType": "uint256", "name": "_hintId", "type": "uint256" } 31 | ], 32 | "name": "claimWithdrawal", 33 | "outputs": [{ "internalType": "bool", "name": "success", "type": "bool" }], 34 | "stateMutability": "nonpayable", 35 | "type": "function" 36 | }, 37 | { 38 | "anonymous": false, 39 | "inputs": [ 40 | { "indexed": true, "internalType": "uint256", "name": "requestId", "type": "uint256" }, 41 | { "indexed": true, "internalType": "address", "name": "requestor", "type": "address" }, 42 | { "indexed": true, "internalType": "address", "name": "recipient", "type": "address" }, 43 | { "indexed": false, "internalType": "uint256", "name": "amount", "type": "uint256" } 44 | ], 45 | "name": "WithdrawalRequested", 46 | "type": "event" 47 | }, 48 | { 49 | "anonymous": false, 50 | "inputs": [ 51 | { "indexed": true, "internalType": "uint256", "name": "requestId", "type": "uint256" }, 52 | { "indexed": true, "internalType": "address", "name": "recipient", "type": "address" }, 53 | { "indexed": false, "internalType": "uint256", "name": "amountOfETH", "type": "uint256" } 54 | ], 55 | "name": "WithdrawalClaimed", 56 | "type": "event" 57 | } 58 | ] 59 | -------------------------------------------------------------------------------- /src/common/abi/CctpMessageTransmitter.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "anonymous": "false", 4 | "inputs": [ 5 | { 6 | "indexed": false, 7 | "internalType": "bytes", 8 | "name": "message", 9 | "type": "bytes" 10 | } 11 | ], 12 | "name": "MessageSent", 13 | "type": "event" 14 | }, 15 | { 16 | "inputs": [ 17 | { 18 | "internalType": "bytes", 19 | "name": "message", 20 | "type": "bytes" 21 | }, 22 | { 23 | "internalType": "bytes", 24 | "name": "attestation", 25 | "type": "bytes" 26 | } 27 | ], 28 | "name": "receiveMessage", 29 | "outputs": [ 30 | { 31 | "internalType": "bool", 32 | "name": "success", 33 | "type": "bool" 34 | } 35 | ], 36 | "stateMutability": "nonpayable", 37 | "type": "function" 38 | }, 39 | { 40 | "inputs": [ 41 | { 42 | "internalType": "bytes32", 43 | "name": "", 44 | "type": "bytes32" 45 | } 46 | ], 47 | "name": "usedNonces", 48 | "outputs": [ 49 | { 50 | "internalType": "uint256", 51 | "name": "", 52 | "type": "uint256" 53 | } 54 | ], 55 | "stateMutability": "view", 56 | "type": "function" 57 | } 58 | ] 59 | -------------------------------------------------------------------------------- /src/common/abi/CctpTokenMessenger.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "anonymous": false, 4 | "inputs": [ 5 | { 6 | "indexed": true, 7 | "internalType": "uint64", 8 | "name": "nonce", 9 | "type": "uint64" 10 | }, 11 | { 12 | "indexed": true, 13 | "internalType": "address", 14 | "name": "burnToken", 15 | "type": "address" 16 | }, 17 | { 18 | "indexed": false, 19 | "internalType": "uint256", 20 | "name": "amount", 21 | "type": "uint256" 22 | }, 23 | { 24 | "indexed": true, 25 | "internalType": "address", 26 | "name": "depositor", 27 | "type": "address" 28 | }, 29 | { 30 | "indexed": false, 31 | "internalType": "bytes32", 32 | "name": "mintRecipient", 33 | "type": "bytes32" 34 | }, 35 | { 36 | "indexed": false, 37 | "internalType": "uint32", 38 | "name": "destinationDomain", 39 | "type": "uint32" 40 | }, 41 | { 42 | "indexed": false, 43 | "internalType": "bytes32", 44 | "name": "destinationTokenMessenger", 45 | "type": "bytes32" 46 | }, 47 | { 48 | "indexed": false, 49 | "internalType": "bytes32", 50 | "name": "destinationCaller", 51 | "type": "bytes32" 52 | } 53 | ], 54 | "name": "DepositForBurn", 55 | "type": "event" 56 | }, 57 | { 58 | "anonymous": false, 59 | "inputs": [ 60 | { 61 | "indexed": true, 62 | "internalType": "address", 63 | "name": "mintRecipient", 64 | "type": "address" 65 | }, 66 | { 67 | "indexed": false, 68 | "internalType": "uint256", 69 | "name": "amount", 70 | "type": "uint256" 71 | }, 72 | { 73 | "indexed": true, 74 | "internalType": "address", 75 | "name": "mintToken", 76 | "type": "address" 77 | } 78 | ], 79 | "name": "MintAndWithdraw", 80 | "type": "event" 81 | }, 82 | { 83 | "inputs": [ 84 | { 85 | "internalType": "uint256", 86 | "name": "amount", 87 | "type": "uint256" 88 | }, 89 | { 90 | "internalType": "uint32", 91 | "name": "destinationDomain", 92 | "type": "uint32" 93 | }, 94 | { 95 | "internalType": "bytes32", 96 | "name": "mintRecipient", 97 | "type": "bytes32" 98 | }, 99 | { 100 | "internalType": "address", 101 | "name": "burnToken", 102 | "type": "address" 103 | } 104 | ], 105 | "name": "depositForBurn", 106 | "outputs": [ 107 | { 108 | "internalType": "uint64", 109 | "name": "_nonce", 110 | "type": "uint64" 111 | } 112 | ], 113 | "stateMutability": "nonpayable", 114 | "type": "function" 115 | } 116 | ] 117 | -------------------------------------------------------------------------------- /src/common/abi/DaiOptimismBridgeL1.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "anonymous": false, 4 | "inputs": [ 5 | { "indexed": true, "internalType": "address", "name": "_l1Token", "type": "address" }, 6 | { "indexed": true, "internalType": "address", "name": "_l2Token", "type": "address" }, 7 | { "indexed": true, "internalType": "address", "name": "_from", "type": "address" }, 8 | { "indexed": false, "internalType": "address", "name": "_to", "type": "address" }, 9 | { "indexed": false, "internalType": "uint256", "name": "_amount", "type": "uint256" }, 10 | { "indexed": false, "internalType": "bytes", "name": "_data", "type": "bytes" } 11 | ], 12 | "name": "ERC20DepositInitiated", 13 | "type": "event" 14 | }, 15 | { 16 | "inputs": [ 17 | { "internalType": "address", "name": "_l1Token", "type": "address" }, 18 | { "internalType": "address", "name": "_l2Token", "type": "address" }, 19 | { "internalType": "uint256", "name": "_amount", "type": "uint256" }, 20 | { "internalType": "uint32", "name": "_l2Gas", "type": "uint32" }, 21 | { "internalType": "bytes", "name": "_data", "type": "bytes" } 22 | ], 23 | "name": "depositERC20", 24 | "outputs": [], 25 | "stateMutability": "nonpayable", 26 | "type": "function" 27 | } 28 | ] 29 | -------------------------------------------------------------------------------- /src/common/abi/DaiOptimismBridgeL2.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "anonymous": false, 4 | "inputs": [ 5 | { "indexed": true, "internalType": "address", "name": "_l1Token", "type": "address" }, 6 | { "indexed": true, "internalType": "address", "name": "_l2Token", "type": "address" }, 7 | { "indexed": true, "internalType": "address", "name": "_from", "type": "address" }, 8 | { "indexed": false, "internalType": "address", "name": "_to", "type": "address" }, 9 | { "indexed": false, "internalType": "uint256", "name": "_amount", "type": "uint256" }, 10 | { "indexed": false, "internalType": "bytes", "name": "_data", "type": "bytes" } 11 | ], 12 | "name": "DepositFinalized", 13 | "type": "event" 14 | } 15 | ] 16 | -------------------------------------------------------------------------------- /src/common/abi/HubPool.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "anonymous": false, 4 | "inputs": [ 5 | { "indexed": false, "internalType": "address", "name": "l1Token", "type": "address" }, 6 | { "indexed": false, "internalType": "address", "name": "l2Token", "type": "address" }, 7 | { "indexed": false, "internalType": "uint256", "name": "amount", "type": "uint256" }, 8 | { "indexed": false, "internalType": "address", "name": "to", "type": "address" } 9 | ], 10 | "name": "TokensRelayed", 11 | "type": "event" 12 | } 13 | ] 14 | -------------------------------------------------------------------------------- /src/common/abi/HubPoolStore.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "anonymous": false, 4 | "inputs": [ 5 | { "indexed": true, "internalType": "address", "name": "target", "type": "address" }, 6 | { "indexed": false, "internalType": "bytes", "name": "data", "type": "bytes" }, 7 | { "indexed": true, "internalType": "uint256", "name": "nonce", "type": "uint256" } 8 | ], 9 | "name": "StoredCallData", 10 | "type": "event" 11 | } 12 | ] 13 | -------------------------------------------------------------------------------- /src/common/abi/IHypXERC20Router.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [], 4 | "name": "wrappedToken", 5 | "outputs": [ 6 | { 7 | "internalType": "contract IERC20", 8 | "name": "", 9 | "type": "address" 10 | } 11 | ], 12 | "stateMutability": "view", 13 | "type": "function" 14 | }, 15 | { 16 | "inputs": [ 17 | { 18 | "internalType": "uint32", 19 | "name": "_destinationDomain", 20 | "type": "uint32" 21 | } 22 | ], 23 | "name": "quoteGasPayment", 24 | "outputs": [ 25 | { 26 | "internalType": "uint256", 27 | "name": "", 28 | "type": "uint256" 29 | } 30 | ], 31 | "stateMutability": "view", 32 | "type": "function" 33 | }, 34 | { 35 | "anonymous": false, 36 | "inputs": [ 37 | { 38 | "indexed": true, 39 | "internalType": "uint32", 40 | "name": "origin", 41 | "type": "uint32" 42 | }, 43 | { 44 | "indexed": true, 45 | "internalType": "bytes32", 46 | "name": "recipient", 47 | "type": "bytes32" 48 | }, 49 | { 50 | "indexed": false, 51 | "internalType": "uint256", 52 | "name": "amount", 53 | "type": "uint256" 54 | } 55 | ], 56 | "name": "ReceivedTransferRemote", 57 | "type": "event" 58 | }, 59 | { 60 | "anonymous": false, 61 | "inputs": [ 62 | { 63 | "indexed": true, 64 | "internalType": "uint32", 65 | "name": "destination", 66 | "type": "uint32" 67 | }, 68 | { 69 | "indexed": true, 70 | "internalType": "bytes32", 71 | "name": "recipient", 72 | "type": "bytes32" 73 | }, 74 | { 75 | "indexed": false, 76 | "internalType": "uint256", 77 | "name": "amount", 78 | "type": "uint256" 79 | } 80 | ], 81 | "name": "SentTransferRemote", 82 | "type": "event" 83 | }, 84 | { 85 | "inputs": [ 86 | { 87 | "internalType": "uint32", 88 | "name": "_destination", 89 | "type": "uint32" 90 | }, 91 | { 92 | "internalType": "bytes32", 93 | "name": "_recipient", 94 | "type": "bytes32" 95 | }, 96 | { 97 | "internalType": "uint256", 98 | "name": "_amountOrId", 99 | "type": "uint256" 100 | } 101 | ], 102 | "name": "transferRemote", 103 | "outputs": [ 104 | { 105 | "internalType": "bytes32", 106 | "name": "messageId", 107 | "type": "bytes32" 108 | } 109 | ], 110 | "stateMutability": "payable", 111 | "type": "function" 112 | } 113 | ] 114 | -------------------------------------------------------------------------------- /src/common/abi/LineaMessageService.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [], 4 | "name": "minimumFeeInWei", 5 | "outputs": [ 6 | { 7 | "internalType": "uint256", 8 | "name": "", 9 | "type": "uint256" 10 | } 11 | ], 12 | "stateMutability": "view", 13 | "type": "function" 14 | }, 15 | { 16 | "anonymous": false, 17 | "inputs": [{ "indexed": true, "internalType": "bytes32", "name": "_messageHash", "type": "bytes32" }], 18 | "name": "MessageClaimed", 19 | "type": "event" 20 | }, 21 | { 22 | "inputs": [ 23 | { 24 | "internalType": "address", 25 | "name": "_to", 26 | "type": "address" 27 | }, 28 | { 29 | "internalType": "uint256", 30 | "name": "_fee", 31 | "type": "uint256" 32 | }, 33 | { 34 | "internalType": "bytes", 35 | "name": "_calldata", 36 | "type": "bytes" 37 | } 38 | ], 39 | "name": "sendMessage", 40 | "outputs": [], 41 | "stateMutability": "payable", 42 | "type": "function" 43 | }, 44 | { 45 | "anonymous": false, 46 | "inputs": [ 47 | { 48 | "indexed": true, 49 | "name": "_from", 50 | "type": "address" 51 | }, 52 | { 53 | "indexed": true, 54 | "name": "_to", 55 | "type": "address" 56 | }, 57 | { 58 | "indexed": false, 59 | "name": "_fee", 60 | "type": "uint256" 61 | }, 62 | { 63 | "indexed": false, 64 | "name": "_value", 65 | "type": "uint256" 66 | }, 67 | { 68 | "indexed": false, 69 | "name": "_nonce", 70 | "type": "uint256" 71 | }, 72 | { 73 | "indexed": false, 74 | "name": "_calldata", 75 | "type": "bytes" 76 | }, 77 | { 78 | "indexed": true, 79 | "name": "_messageHash", 80 | "type": "bytes32" 81 | } 82 | ], 83 | "name": "MessageSent", 84 | "type": "event" 85 | } 86 | ] 87 | -------------------------------------------------------------------------------- /src/common/abi/LineaTokenBridge.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { "internalType": "address", "name": "_token", "type": "address" }, 5 | { "internalType": "uint256", "name": "_amount", "type": "uint256" }, 6 | { "internalType": "address", "name": "_recipient", "type": "address" } 7 | ], 8 | "name": "bridgeToken", 9 | "outputs": [], 10 | "stateMutability": "payable", 11 | "type": "function" 12 | }, 13 | { 14 | "anonymous": false, 15 | "inputs": [ 16 | { 17 | "indexed": true, 18 | "internalType": "address", 19 | "name": "sender", 20 | "type": "address" 21 | }, 22 | { 23 | "indexed": true, 24 | "internalType": "address", 25 | "name": "recipient", 26 | "type": "address" 27 | }, 28 | { 29 | "indexed": true, 30 | "internalType": "address", 31 | "name": "token", 32 | "type": "address" 33 | }, 34 | { 35 | "indexed": false, 36 | "internalType": "uint256", 37 | "name": "amount", 38 | "type": "uint256" 39 | } 40 | ], 41 | "name": "BridgingInitiatedV2", 42 | "type": "event" 43 | }, 44 | { 45 | "anonymous": false, 46 | "inputs": [ 47 | { 48 | "indexed": true, 49 | "internalType": "address", 50 | "name": "nativeToken", 51 | "type": "address" 52 | }, 53 | { 54 | "indexed": true, 55 | "internalType": "address", 56 | "name": "bridgedToken", 57 | "type": "address" 58 | }, 59 | { 60 | "indexed": false, 61 | "internalType": "uint256", 62 | "name": "amount", 63 | "type": "uint256" 64 | }, 65 | { 66 | "indexed": true, 67 | "internalType": "address", 68 | "name": "recipient", 69 | "type": "address" 70 | } 71 | ], 72 | "name": "BridgingFinalizedV2", 73 | "type": "event" 74 | }, 75 | { 76 | "inputs": [ 77 | { 78 | "internalType": "address", 79 | "name": "_nativeToken", 80 | "type": "address" 81 | }, 82 | { 83 | "internalType": "uint256", 84 | "name": "_amount", 85 | "type": "uint256" 86 | }, 87 | { 88 | "internalType": "address", 89 | "name": "_recipient", 90 | "type": "address" 91 | }, 92 | { 93 | "internalType": "uint256", 94 | "name": "_chainId", 95 | "type": "uint256" 96 | }, 97 | { 98 | "internalType": "bytes", 99 | "name": "_tokenMetadata", 100 | "type": "bytes" 101 | } 102 | ], 103 | "stateMutability": "nonpayable", 104 | "type": "function", 105 | "name": "completeBridging" 106 | } 107 | ] 108 | -------------------------------------------------------------------------------- /src/common/abi/LineaUsdcBridge.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { "internalType": "uint256", "name": "amount", "type": "uint256" }, 5 | { "internalType": "address", "name": "to", "type": "address" } 6 | ], 7 | "name": "depositTo", 8 | "outputs": [], 9 | "stateMutability": "payable", 10 | "type": "function" 11 | }, 12 | { 13 | "anonymous": false, 14 | "inputs": [ 15 | { 16 | "indexed": true, 17 | "internalType": "address", 18 | "name": "depositor", 19 | "type": "address" 20 | }, 21 | { 22 | "indexed": false, 23 | "internalType": "uint256", 24 | "name": "amount", 25 | "type": "uint256" 26 | }, 27 | { 28 | "indexed": true, 29 | "internalType": "address", 30 | "name": "to", 31 | "type": "address" 32 | } 33 | ], 34 | "name": "Deposited", 35 | "type": "event" 36 | }, 37 | { 38 | "anonymous": false, 39 | "inputs": [ 40 | { 41 | "indexed": true, 42 | "internalType": "address", 43 | "name": "recipient", 44 | "type": "address" 45 | }, 46 | { 47 | "indexed": true, 48 | "internalType": "uint256", 49 | "name": "amount", 50 | "type": "uint256" 51 | } 52 | ], 53 | "name": "ReceivedFromOtherLayer", 54 | "type": "event" 55 | }, 56 | { 57 | "inputs": [ 58 | { "internalType": "address", "name": "recipient", "type": "address" }, 59 | { "internalType": "uint256", "name": "amount", "type": "uint256" } 60 | ], 61 | "name": "receiveFromOtherLayer", 62 | "outputs": [], 63 | "stateMutability": "nonpayable", 64 | "type": "function" 65 | } 66 | ] 67 | -------------------------------------------------------------------------------- /src/common/abi/MinimalERC20.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "anonymous": false, 4 | "inputs": [ 5 | { 6 | "indexed": true, 7 | "internalType": "address", 8 | "name": "from", 9 | "type": "address" 10 | }, 11 | { 12 | "indexed": true, 13 | "internalType": "address", 14 | "name": "to", 15 | "type": "address" 16 | }, 17 | { 18 | "indexed": false, 19 | "internalType": "uint256", 20 | "name": "value", 21 | "type": "uint256" 22 | } 23 | ], 24 | "name": "Transfer", 25 | "type": "event" 26 | }, 27 | { 28 | "inputs": [ 29 | { 30 | "internalType": "address", 31 | "name": "to", 32 | "type": "address" 33 | }, 34 | { 35 | "internalType": "uint256", 36 | "name": "value", 37 | "type": "uint256" 38 | } 39 | ], 40 | "name": "transfer", 41 | "outputs": [ 42 | { 43 | "internalType": "bool", 44 | "name": "", 45 | "type": "bool" 46 | } 47 | ], 48 | "stateMutability": "nonpayable", 49 | "type": "function" 50 | }, 51 | { 52 | "inputs": [ 53 | { 54 | "internalType": "address", 55 | "name": "spender", 56 | "type": "address" 57 | }, 58 | { 59 | "internalType": "uint256", 60 | "name": "amount", 61 | "type": "uint256" 62 | } 63 | ], 64 | "name": "approve", 65 | "outputs": [ 66 | { 67 | "internalType": "bool", 68 | "name": "", 69 | "type": "bool" 70 | } 71 | ], 72 | "stateMutability": "nonpayable", 73 | "type": "function" 74 | } 75 | ] 76 | -------------------------------------------------------------------------------- /src/common/abi/PolygonBridge.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "anonymous": false, 4 | "inputs": [ 5 | { "indexed": true, "internalType": "address", "name": "depositor", "type": "address" }, 6 | { "indexed": true, "internalType": "address", "name": "depositReceiver", "type": "address" }, 7 | { "indexed": true, "internalType": "address", "name": "rootToken", "type": "address" }, 8 | { "indexed": false, "internalType": "uint256", "name": "amount", "type": "uint256" } 9 | ], 10 | "name": "LockedERC20", 11 | "type": "event" 12 | }, 13 | { 14 | "anonymous": false, 15 | "inputs": [ 16 | { "indexed": true, "internalType": "address", "name": "depositor", "type": "address" }, 17 | { "indexed": true, "internalType": "address", "name": "depositReceiver", "type": "address" }, 18 | { "indexed": false, "internalType": "uint256", "name": "amount", "type": "uint256" } 19 | ], 20 | "name": "LockedEther", 21 | "type": "event" 22 | }, 23 | { 24 | "anonymous": false, 25 | "inputs": [ 26 | { "indexed": true, "internalType": "address", "name": "owner", "type": "address" }, 27 | { "indexed": true, "internalType": "address", "name": "token", "type": "address" }, 28 | { "indexed": false, "internalType": "uint256", "name": "amountOrNFTId", "type": "uint256" }, 29 | { "indexed": false, "internalType": "uint256", "name": "depositBlockId", "type": "uint256" } 30 | ], 31 | "name": "NewDepositBlock", 32 | "type": "event" 33 | } 34 | ] 35 | -------------------------------------------------------------------------------- /src/common/abi/PolygonRootChainManager.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { "internalType": "address", "name": "user", "type": "address" }, 5 | { "internalType": "address", "name": "rootToken", "type": "address" }, 6 | { "internalType": "bytes", "name": "depositData", "type": "bytes" } 7 | ], 8 | "name": "depositFor", 9 | "outputs": [], 10 | "stateMutability": "nonpayable", 11 | "type": "function" 12 | }, 13 | { 14 | "inputs": [{ "internalType": "address", "name": "user", "type": "address" }], 15 | "name": "depositEtherFor", 16 | "outputs": [], 17 | "stateMutability": "payable", 18 | "type": "function" 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /src/common/abi/PolygonWithdrawableErc20.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "anonymous": false, 4 | "inputs": [ 5 | { "indexed": true, "internalType": "address", "name": "from", "type": "address" }, 6 | { "indexed": true, "internalType": "address", "name": "to", "type": "address" }, 7 | { "indexed": false, "internalType": "uint256", "name": "value", "type": "uint256" } 8 | ], 9 | "name": "Transfer", 10 | "type": "event" 11 | }, 12 | { 13 | "anonymous": false, 14 | "inputs": [ 15 | { "indexed": true, "internalType": "address", "name": "rootToken", "type": "address" }, 16 | { "indexed": true, "internalType": "address", "name": "childToken", "type": "address" }, 17 | { "indexed": true, "internalType": "address", "name": "user", "type": "address" }, 18 | { "indexed": false, "internalType": "uint256", "name": "amount", "type": "uint256" }, 19 | { "indexed": false, "internalType": "uint256", "name": "depositCount", "type": "uint256" } 20 | ], 21 | "name": "TokenDeposited", 22 | "type": "event" 23 | } 24 | ] 25 | -------------------------------------------------------------------------------- /src/common/abi/SP1Helios.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "anonymous": false, 4 | "inputs": [ 5 | { "indexed": true, "internalType": "uint256", "name": "head", "type": "uint256" }, 6 | { "indexed": true, "internalType": "bytes32", "name": "key", "type": "bytes32" }, 7 | { "indexed": false, "internalType": "bytes32", "name": "value", "type": "bytes32" }, 8 | { "indexed": false, "internalType": "address", "name": "contractAddress", "type": "address" } 9 | ], 10 | "name": "StorageSlotVerified", 11 | "type": "event" 12 | }, 13 | { 14 | "anonymous": false, 15 | "inputs": [ 16 | { 17 | "indexed": true, 18 | "internalType": "uint256", 19 | "name": "slot", 20 | "type": "uint256" 21 | }, 22 | { 23 | "indexed": true, 24 | "internalType": "bytes32", 25 | "name": "root", 26 | "type": "bytes32" 27 | } 28 | ], 29 | "name": "HeadUpdate", 30 | "type": "event" 31 | }, 32 | { 33 | "inputs": [], 34 | "name": "head", 35 | "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], 36 | "stateMutability": "view", 37 | "type": "function" 38 | }, 39 | { 40 | "inputs": [{ "internalType": "uint256", "name": "beaconSlot", "type": "uint256" }], 41 | "name": "headers", 42 | "outputs": [{ "internalType": "bytes32", "name": "beaconHeaderRoot", "type": "bytes32" }], 43 | "stateMutability": "view", 44 | "type": "function" 45 | }, 46 | { 47 | "inputs": [ 48 | { "internalType": "bytes", "name": "proof", "type": "bytes" }, 49 | { "internalType": "bytes", "name": "publicValues", "type": "bytes" } 50 | ], 51 | "name": "update", 52 | "outputs": [], 53 | "stateMutability": "nonpayable", 54 | "type": "function" 55 | } 56 | ] 57 | -------------------------------------------------------------------------------- /src/common/abi/ScrollGasPriceOracle.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "uint256", 6 | "name": "_gasLimit", 7 | "type": "uint256" 8 | } 9 | ], 10 | "name": "estimateCrossDomainMessageFee", 11 | "outputs": [ 12 | { 13 | "internalType": "uint256", 14 | "name": "", 15 | "type": "uint256" 16 | } 17 | ], 18 | "stateMutability": "view", 19 | "type": "function" 20 | } 21 | ] 22 | -------------------------------------------------------------------------------- /src/common/abi/ScrollRelayMessenger.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "address", 6 | "name": "_from", 7 | "type": "address" 8 | }, 9 | { 10 | "internalType": "address", 11 | "name": "_to", 12 | "type": "address" 13 | }, 14 | { 15 | "internalType": "uint256", 16 | "name": "_value", 17 | "type": "uint256" 18 | }, 19 | { 20 | "internalType": "uint256", 21 | "name": "_nonce", 22 | "type": "uint256" 23 | }, 24 | { 25 | "internalType": "bytes", 26 | "name": "_message", 27 | "type": "bytes" 28 | }, 29 | { 30 | "components": [ 31 | { 32 | "internalType": "uint256", 33 | "name": "batchIndex", 34 | "type": "uint256" 35 | }, 36 | { 37 | "internalType": "bytes", 38 | "name": "merkleProof", 39 | "type": "bytes" 40 | } 41 | ], 42 | "internalType": "struct IL1ScrollMessenger.L2MessageProof", 43 | "name": "_proof", 44 | "type": "tuple" 45 | } 46 | ], 47 | "name": "relayMessageWithProof", 48 | "outputs": [], 49 | "stateMutability": "nonpayable", 50 | "type": "function" 51 | } 52 | ] 53 | -------------------------------------------------------------------------------- /src/common/abi/SnxOptimismBridgeL1.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "anonymous": false, 4 | "inputs": [ 5 | { "indexed": true, "internalType": "address", "name": "_from", "type": "address" }, 6 | { "indexed": false, "internalType": "address", "name": "_to", "type": "address" }, 7 | { "indexed": false, "internalType": "uint256", "name": "_amount", "type": "uint256" } 8 | ], 9 | "name": "DepositInitiated", 10 | "type": "event" 11 | }, 12 | { 13 | "inputs": [ 14 | { "internalType": "address", "name": "to", "type": "address" }, 15 | { "internalType": "uint256", "name": "amount", "type": "uint256" } 16 | ], 17 | "name": "depositTo", 18 | "outputs": [], 19 | "stateMutability": "nonpayable", 20 | "type": "function" 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /src/common/abi/SnxOptimismBridgeL2.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "anonymous": false, 4 | "inputs": [ 5 | { "indexed": true, "internalType": "address", "name": "_to", "type": "address" }, 6 | { "indexed": false, "internalType": "uint256", "name": "_amount", "type": "uint256" } 7 | ], 8 | "name": "DepositFinalized", 9 | "type": "event" 10 | } 11 | ] 12 | -------------------------------------------------------------------------------- /src/common/abi/Universal_SpokePool.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "anonymous": false, 4 | "inputs": [ 5 | { "indexed": true, "internalType": "uint256", "name": "nonce", "type": "uint256" }, 6 | { "indexed": false, "internalType": "address", "name": "caller", "type": "address" } 7 | ], 8 | "name": "RelayedCallData", 9 | "type": "event" 10 | }, 11 | { 12 | "inputs": [ 13 | { "internalType": "uint256", "name": "_messageNonce", "type": "uint256" }, 14 | { "internalType": "bytes", "name": "_message", "type": "bytes" }, 15 | { "internalType": "uint256", "name": "_blockNumber", "type": "uint256" } 16 | ], 17 | "name": "executeMessage", 18 | "outputs": [], 19 | "stateMutability": "nonpayable", 20 | "type": "function" 21 | }, 22 | { 23 | "inputs": [], 24 | "name": "helios", 25 | "outputs": [ 26 | { 27 | "internalType": "address", 28 | "name": "", 29 | "type": "address" 30 | } 31 | ], 32 | "stateMutability": "view", 33 | "type": "function" 34 | } 35 | ] 36 | -------------------------------------------------------------------------------- /src/common/abi/VotingV2.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "anonymous": false, 4 | "inputs": [ 5 | { "indexed": true, "internalType": "address", "name": "requester", "type": "address" }, 6 | { "indexed": true, "internalType": "uint32", "name": "roundId", "type": "uint32" }, 7 | { "indexed": true, "internalType": "bytes32", "name": "identifier", "type": "bytes32" }, 8 | { "indexed": false, "internalType": "uint256", "name": "time", "type": "uint256" }, 9 | { "indexed": false, "internalType": "bytes", "name": "ancillaryData", "type": "bytes" }, 10 | { "indexed": false, "internalType": "bool", "name": "isGovernance", "type": "bool" } 11 | ], 12 | "name": "RequestAdded", 13 | "type": "event" 14 | } 15 | ] 16 | -------------------------------------------------------------------------------- /src/common/abi/Weth.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "constant": false, 4 | "inputs": [{ "name": "wad", "type": "uint256" }], 5 | "name": "withdraw", 6 | "outputs": [], 7 | "payable": false, 8 | "stateMutability": "nonpayable", 9 | "type": "function" 10 | }, 11 | { 12 | "constant": false, 13 | "inputs": [], 14 | "name": "deposit", 15 | "outputs": [], 16 | "payable": true, 17 | "stateMutability": "payable", 18 | "type": "function" 19 | }, 20 | { 21 | "constant": true, 22 | "inputs": [{ "name": "", "type": "address" }], 23 | "name": "balanceOf", 24 | "outputs": [{ "name": "", "type": "uint256" }], 25 | "payable": false, 26 | "stateMutability": "view", 27 | "type": "function" 28 | }, 29 | { 30 | "anonymous": false, 31 | "inputs": [ 32 | { 33 | "indexed": true, 34 | "internalType": "address", 35 | "name": "dst", 36 | "type": "address" 37 | }, 38 | { 39 | "indexed": false, 40 | "internalType": "uint256", 41 | "name": "wad", 42 | "type": "uint256" 43 | } 44 | ], 45 | "name": "Deposit", 46 | "type": "event" 47 | }, 48 | { 49 | "anonymous": false, 50 | "inputs": [ 51 | { "indexed": true, "internalType": "address", "name": "from", "type": "address" }, 52 | { "indexed": true, "internalType": "address", "name": "_to", "type": "address" }, 53 | { "indexed": false, "internalType": "uint256", "name": "_amount", "type": "uint256" } 54 | ], 55 | "name": "Transfer", 56 | "type": "event" 57 | }, 58 | { 59 | "constant": true, 60 | "inputs": [], 61 | "name": "symbol", 62 | "outputs": [ 63 | { 64 | "name": "", 65 | "type": "string" 66 | } 67 | ], 68 | "payable": false, 69 | "stateMutability": "view", 70 | "type": "function" 71 | } 72 | ] 73 | -------------------------------------------------------------------------------- /src/common/abi/ZkStackNativeTokenVault.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "address", 6 | "name": "tokenAddress", 7 | "type": "address" 8 | } 9 | ], 10 | "name": "assetId", 11 | "outputs": [ 12 | { 13 | "internalType": "bytes32", 14 | "name": "assetId", 15 | "type": "bytes32" 16 | } 17 | ], 18 | "stateMutability": "view", 19 | "type": "function" 20 | }, 21 | { 22 | "anonymous": false, 23 | "inputs": [ 24 | { 25 | "indexed": true, 26 | "internalType": "uint256", 27 | "name": "chainId", 28 | "type": "uint256" 29 | }, 30 | { 31 | "indexed": true, 32 | "internalType": "bytes32", 33 | "name": "assetId", 34 | "type": "bytes32" 35 | }, 36 | { 37 | "indexed": false, 38 | "internalType": "address", 39 | "name": "receiver", 40 | "type": "address" 41 | }, 42 | { 43 | "indexed": false, 44 | "internalType": "uint256", 45 | "name": "amount", 46 | "type": "uint256" 47 | } 48 | ], 49 | "name": "BridgeMint", 50 | "type": "event" 51 | }, 52 | { 53 | "anonymous": false, 54 | "inputs": [ 55 | { 56 | "indexed": true, 57 | "internalType": "uint256", 58 | "name": "chainId", 59 | "type": "uint256" 60 | }, 61 | { 62 | "indexed": true, 63 | "internalType": "bytes32", 64 | "name": "assetId", 65 | "type": "bytes32" 66 | }, 67 | { 68 | "indexed": true, 69 | "internalType": "address", 70 | "name": "sender", 71 | "type": "address" 72 | }, 73 | { 74 | "indexed": false, 75 | "internalType": "address", 76 | "name": "receiver", 77 | "type": "address" 78 | }, 79 | { 80 | "indexed": false, 81 | "internalType": "uint256", 82 | "name": "amount", 83 | "type": "uint256" 84 | } 85 | ], 86 | "name": "BridgeBurn", 87 | "type": "event" 88 | } 89 | ] 90 | -------------------------------------------------------------------------------- /src/common/abi/ZkStackUSDCBridge.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "anonymous": false, 4 | "inputs": [ 5 | { 6 | "indexed": true, 7 | "internalType": "address", 8 | "name": "from", 9 | "type": "address" 10 | }, 11 | { 12 | "indexed": true, 13 | "internalType": "address", 14 | "name": "to", 15 | "type": "address" 16 | }, 17 | { 18 | "indexed": true, 19 | "internalType": "address", 20 | "name": "l2Token", 21 | "type": "address" 22 | }, 23 | { 24 | "indexed": false, 25 | "internalType": "uint256", 26 | "name": "amount", 27 | "type": "uint256" 28 | } 29 | ], 30 | "name": "FinalizeDeposit", 31 | "type": "event" 32 | }, 33 | { 34 | "anonymous": false, 35 | "inputs": [ 36 | { 37 | "indexed": true, 38 | "internalType": "uint256", 39 | "name": "chainId", 40 | "type": "uint256" 41 | }, 42 | { 43 | "indexed": true, 44 | "internalType": "bytes32", 45 | "name": "txDataHash", 46 | "type": "bytes32" 47 | }, 48 | { 49 | "indexed": true, 50 | "internalType": "address", 51 | "name": "from", 52 | "type": "address" 53 | }, 54 | { 55 | "indexed": false, 56 | "internalType": "address", 57 | "name": "to", 58 | "type": "address" 59 | }, 60 | { 61 | "indexed": false, 62 | "internalType": "address", 63 | "name": "l1Token", 64 | "type": "address" 65 | }, 66 | { 67 | "indexed": false, 68 | "internalType": "uint256", 69 | "name": "amount", 70 | "type": "uint256" 71 | } 72 | ], 73 | "name": "BridgehubDepositInitiated", 74 | "type": "event" 75 | } 76 | ] 77 | -------------------------------------------------------------------------------- /src/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Config"; 2 | export * from "./ClientHelper"; 3 | export * from "./Constants"; 4 | export * from "./ContractAddresses"; 5 | -------------------------------------------------------------------------------- /src/dataworker/RelayerRefundUtils.ts: -------------------------------------------------------------------------------- 1 | import { Refund, RelayerRefundLeaf, RelayerRefundLeafWithGroup, SpokePoolTargetBalance } from "../interfaces"; 2 | import { BigNumber, bnZero, compareAddresses, getNetSendAmountForL1Token } from "../utils"; 3 | 4 | export function getAmountToReturnForRelayerRefundLeaf( 5 | spokePoolTargetBalance: SpokePoolTargetBalance, 6 | runningBalanceForLeaf: BigNumber 7 | ): BigNumber { 8 | const netSendAmountForLeaf = getNetSendAmountForL1Token(spokePoolTargetBalance, runningBalanceForLeaf); 9 | return netSendAmountForLeaf.lt(bnZero) ? netSendAmountForLeaf.mul(-1) : bnZero; 10 | } 11 | 12 | export function sortRefundAddresses(refunds: Refund): string[] { 13 | const deepCopy = { ...refunds }; 14 | return [...Object.keys(deepCopy)].sort((addressA, addressB) => { 15 | if (deepCopy[addressA].gt(deepCopy[addressB])) { 16 | return -1; 17 | } 18 | if (deepCopy[addressA].lt(deepCopy[addressB])) { 19 | return 1; 20 | } 21 | const sortOutput = compareAddresses(addressA, addressB); 22 | if (sortOutput !== 0) { 23 | return sortOutput; 24 | } else { 25 | throw new Error("Unexpected matching address"); 26 | } 27 | }); 28 | } 29 | 30 | // Sort leaves by chain ID and then L2 token address in ascending order. Assign leaves unique, ascending ID's 31 | // beginning from 0. 32 | export function sortRelayerRefundLeaves(relayerRefundLeaves: RelayerRefundLeafWithGroup[]): RelayerRefundLeaf[] { 33 | return [...relayerRefundLeaves] 34 | .sort((leafA, leafB) => { 35 | if (leafA.chainId !== leafB.chainId) { 36 | return leafA.chainId - leafB.chainId; 37 | } else if (compareAddresses(leafA.l2TokenAddress, leafB.l2TokenAddress) !== 0) { 38 | return compareAddresses(leafA.l2TokenAddress, leafB.l2TokenAddress); 39 | } else if (leafA.groupIndex !== leafB.groupIndex) { 40 | return leafA.groupIndex - leafB.groupIndex; 41 | } else { 42 | throw new Error("Unexpected leaf group indices match"); 43 | } 44 | }) 45 | .map((leaf: RelayerRefundLeafWithGroup, i: number): RelayerRefundLeaf => { 46 | delete leaf.groupIndex; // Delete group index now that we've used it to sort leaves for the same 47 | // { repaymentChain, l2TokenAddress } since it doesn't exist in RelayerRefundLeaf 48 | return { ...leaf, leafId: i }; 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /src/finalizer/types.ts: -------------------------------------------------------------------------------- 1 | import { Signer } from "ethers"; 2 | import { AugmentedTransaction, HubPoolClient, SpokePoolClient } from "../clients"; 3 | import { Multicall2Call, winston } from "../utils"; 4 | 5 | /** 6 | * A cross-chain message is a message sent from one chain to another. This can be a token withdrawal from L2 to L1, 7 | * a token deposit from L1 to L2, or a miscellaneous transaction to facilitate the two. 8 | * 9 | * Note: This is a union type. All cross-chain transfers will have the properties `originationChainId` and `destinationChainId`. 10 | * The other properties will only be present if the transfer is of that type. 11 | */ 12 | export type CrossChainMessage = { 13 | originationChainId: number; 14 | destinationChainId: number; 15 | } & ( 16 | | { 17 | type: "withdrawal" | "deposit"; 18 | l1TokenSymbol: string; 19 | amount: string; 20 | } 21 | | { 22 | type: "misc"; 23 | miscReason: string; 24 | // Note: Properties `l1TokenSymbol` and `amount` are optional for type "misc" b/c not all misc transactions will have 25 | // these properties, e.g. a call to `relayMessage` of an Adapter contract. 26 | l1TokenSymbol?: string; 27 | amount?: string; 28 | } 29 | ); 30 | 31 | export type FinalizerPromise = { 32 | callData: (Multicall2Call | AugmentedTransaction)[]; 33 | crossChainMessages: CrossChainMessage[]; 34 | }; 35 | 36 | export interface ChainFinalizer { 37 | ( 38 | logger: winston.Logger, 39 | signer: Signer, 40 | hubPoolClient: HubPoolClient, 41 | l2SpokePoolClient: SpokePoolClient, 42 | l1SpokePoolClient: SpokePoolClient, 43 | l1ToL2AddressesToFinalize: string[] 44 | ): Promise; 45 | } 46 | 47 | /** 48 | * Type guard to check if a transaction object is an AugmentedTransaction. 49 | * @param txn The transaction object to check. 50 | * @returns True if the object is an AugmentedTransaction, false otherwise. 51 | */ 52 | export function isAugmentedTransaction(txn: Multicall2Call | AugmentedTransaction): txn is AugmentedTransaction { 53 | // Check for the presence of the 'contract' property, unique to AugmentedTransaction 54 | return txn != null && typeof txn === "object" && "contract" in txn; 55 | } 56 | -------------------------------------------------------------------------------- /src/finalizer/utils/cctp/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./l1ToL2"; 2 | export * from "./l2ToL1"; 3 | -------------------------------------------------------------------------------- /src/finalizer/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./polygon"; 2 | export * from "./arbStack"; 3 | export * from "./opStack"; 4 | export * from "./zkSync"; 5 | export * from "./scroll"; 6 | export * from "./cctp"; 7 | export * from "./binance"; 8 | export * from "./linea"; 9 | export * from "./helios"; 10 | -------------------------------------------------------------------------------- /src/finalizer/utils/linea/imports.ts: -------------------------------------------------------------------------------- 1 | // Normally we avoid importing directly from a node_modules' /dist package but we need access to some 2 | // of the internal classes and functions in order to replicate SDK logic so that we can by pass hardcoded 3 | // ethers.Provider instances and use our own custom provider instead. 4 | import { L1MessageServiceContract, L2MessageServiceContract } from "@consensys/linea-sdk/dist/lib/contracts"; 5 | import { L1ClaimingService } from "@consensys/linea-sdk/dist/lib/sdk/claiming/L1ClaimingService"; 6 | import { MessageSentEvent } from "@consensys/linea-sdk/dist/typechain/L2MessageService"; 7 | import { SparseMerkleTreeFactory } from "@consensys/linea-sdk/dist/lib/sdk/merkleTree/MerkleTreeFactory"; 8 | import { 9 | DEFAULT_L2_MESSAGE_TREE_DEPTH, 10 | L2_MERKLE_TREE_ADDED_EVENT_SIGNATURE, 11 | L2_MESSAGING_BLOCK_ANCHORED_EVENT_SIGNATURE, 12 | } from "@consensys/linea-sdk/dist/lib/utils/constants"; 13 | 14 | export { 15 | L1ClaimingService, 16 | L1MessageServiceContract, 17 | L2MessageServiceContract, 18 | MessageSentEvent, 19 | SparseMerkleTreeFactory, 20 | DEFAULT_L2_MESSAGE_TREE_DEPTH, 21 | L2_MERKLE_TREE_ADDED_EVENT_SIGNATURE, 22 | L2_MESSAGING_BLOCK_ANCHORED_EVENT_SIGNATURE, 23 | }; 24 | -------------------------------------------------------------------------------- /src/finalizer/utils/linea/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./l2ToL1"; 2 | export * from "./l1ToL2"; 3 | -------------------------------------------------------------------------------- /src/interfaces/Arweave.ts: -------------------------------------------------------------------------------- 1 | import { number, object, optional, string } from "superstruct"; 2 | 3 | export type ArweaveWalletJWKInterface = { 4 | kty: string; 5 | e: string; 6 | n: string; 7 | d?: string; 8 | p?: string; 9 | q?: string; 10 | dp?: string; 11 | dq?: string; 12 | qi?: string; 13 | }; 14 | 15 | export type ArweaveGatewayInterface = { 16 | url: string; 17 | protocol: string; 18 | port: number; 19 | }; 20 | 21 | export const ArweaveWalletJWKInterfaceSS = object({ 22 | kty: string(), 23 | e: string(), 24 | n: string(), 25 | d: optional(string()), 26 | p: optional(string()), 27 | q: optional(string()), 28 | dp: optional(string()), 29 | dq: optional(string()), 30 | qi: optional(string()), 31 | }); 32 | 33 | export const ArweaveGatewayInterfaceSS = object({ 34 | url: string(), 35 | protocol: string(), 36 | port: number(), 37 | }); 38 | -------------------------------------------------------------------------------- /src/interfaces/BundleData.ts: -------------------------------------------------------------------------------- 1 | import { interfaces } from "@across-protocol/sdk"; 2 | import { BigNumber } from "../utils"; 3 | export type ExpiredDepositsToRefundV3 = { 4 | [originChainId: number]: { 5 | [originToken: string]: interfaces.DepositWithBlock[]; 6 | }; 7 | }; 8 | 9 | export type BundleDepositsV3 = { 10 | [originChainId: number]: { 11 | [originToken: string]: interfaces.DepositWithBlock[]; 12 | }; 13 | }; 14 | 15 | export interface BundleFillV3 extends interfaces.FillWithBlock { 16 | lpFeePct: BigNumber; 17 | } 18 | 19 | export type BundleFillsV3 = { 20 | [repaymentChainId: number]: { 21 | [repaymentToken: string]: { 22 | fills: BundleFillV3[]; 23 | refunds: interfaces.Refund; 24 | totalRefundAmount: BigNumber; 25 | realizedLpFees: BigNumber; 26 | }; 27 | }; 28 | }; 29 | 30 | export type BundleExcessSlowFills = { 31 | [destinationChainId: number]: { 32 | [destinationToken: string]: (interfaces.DepositWithBlock & { lpFeePct: BigNumber })[]; 33 | }; 34 | }; 35 | export type BundleSlowFills = { 36 | [destinationChainId: number]: { 37 | [destinationToken: string]: (interfaces.DepositWithBlock & { lpFeePct: BigNumber })[]; 38 | }; 39 | }; 40 | 41 | export type LoadDataReturnValue = { 42 | bundleDepositsV3: BundleDepositsV3; 43 | expiredDepositsToRefundV3: ExpiredDepositsToRefundV3; 44 | bundleFillsV3: BundleFillsV3; 45 | unexecutableSlowFills: BundleExcessSlowFills; 46 | bundleSlowFillsV3: BundleSlowFills; 47 | }; 48 | 49 | export type BundleData = LoadDataReturnValue & { 50 | bundleBlockRanges: number[][]; 51 | }; 52 | -------------------------------------------------------------------------------- /src/interfaces/Error.ts: -------------------------------------------------------------------------------- 1 | export type EthersError = Error & { 2 | code: string; 3 | reason: string; 4 | error?: EthersError; 5 | }; 6 | -------------------------------------------------------------------------------- /src/interfaces/Helios.ts: -------------------------------------------------------------------------------- 1 | import { SortableEvent } from "."; 2 | import { BigNumber } from "../utils"; 3 | 4 | // Event type for Sp1Helios used in v4 messaging (Flattened) 5 | export interface StorageSlotVerifiedEvent extends SortableEvent { 6 | key: string; // bytes32 indexed (storage slot key) 7 | value: string; // bytes32 (value at storage slot key) 8 | head: BigNumber; // uint256 (beacon chain slot number) 9 | contractAddress: string; // address 10 | } 11 | 12 | // Flattened SP1Helios Event for HeadUpdate 13 | export interface HeadUpdateEvent extends SortableEvent { 14 | slot: BigNumber; // uint256 15 | root: string; // bytes32 16 | } 17 | -------------------------------------------------------------------------------- /src/interfaces/InventoryManagement.ts: -------------------------------------------------------------------------------- 1 | import { utils as ethersUtils } from "ethers"; 2 | import { BigNumber, TOKEN_SYMBOLS_MAP } from "../utils"; 3 | 4 | export type TokenBalanceConfig = { 5 | targetOverageBuffer: BigNumber; // Max multiplier for targetPct, to give flexibility in repayment chain selection. 6 | targetPct: BigNumber; // The desired amount of the given token on the L2 chainId. 7 | thresholdPct: BigNumber; // Threshold, below which, we will execute a rebalance. 8 | unwrapWethThreshold?: BigNumber; // Threshold for ETH to trigger WETH unwrapping to maintain ETH balance. 9 | unwrapWethTarget?: BigNumber; // Amount of WETH to unwrap to refill ETH. Unused if unwrapWethThreshold is undefined. 10 | withdrawExcessPeriod?: number; // Period in seconds over which to withdraw any excess balance over the 11 | // (thresholdPct * targetOverageBuffer) down to the targetPct. IM will only withdraw excess if this value is set. 12 | }; 13 | 14 | export type ChainTokenConfig = { 15 | [chainId: string]: TokenBalanceConfig; 16 | }; 17 | 18 | // AliasConfig permits a single HubPool token to map onto multiple tokens on a remote chain. 19 | export type ChainTokenInventory = { 20 | [symbol: string]: ChainTokenConfig; 21 | }; 22 | 23 | /** 24 | * Example configuration: 25 | * - DAI on chains 10 & 42161. 26 | * - Bridged USDC (USDC.e, USDbC) on chains 10, 137, 324, 8453, 42161 & 59144. 27 | * - Native USDC on Polygon. 28 | * 29 | * All token allocations are "global", so Polygon will be allocated a total of 8% of all USDC: 30 | * - 4% of global USDC as Native USDC, and 31 | * - 4% as Bridged USDC. 32 | * 33 | * "tokenConfig": { 34 | * "DAI": { 35 | * "10": { "targetPct": 8, "thresholdPct": 4 }, 36 | * "42161": { "targetPct": 8, "thresholdPct": 4 }, 37 | * }, 38 | * "USDC": { 39 | * "USDC.e": { 40 | * "10": { "targetPct": 8, "thresholdPct": 4 }, 41 | * "137": { "targetPct": 4, "thresholdPct": 2 }, 42 | * "324": { "targetPct": 8, "thresholdPct": 4 }, 43 | * "42161": { "targetPct": 8, "thresholdPct": 4 }, 44 | * "59144": { "targetPct": 5, "thresholdPct": 2 } 45 | * }, 46 | * "USDbC": { 47 | * "8453": { "targetPct": 5, "thresholdPct": 2 } 48 | * }, 49 | * "USDC": { 50 | * "137": { "targetPct": 4, "thresholdPct": 2 } 51 | * } 52 | * } 53 | * } 54 | */ 55 | export interface InventoryConfig { 56 | // tokenConfig can map to a single token allocation, or a set of allocations that all map to the same HubPool token. 57 | tokenConfig: { [l1Token: string]: ChainTokenConfig } | { [l1Token: string]: ChainTokenInventory }; 58 | 59 | // If ETH balance on chain is above threshold, wrap the excess over the target to WETH. 60 | wrapEtherTargetPerChain: { 61 | [chainId: number]: BigNumber; 62 | }; 63 | wrapEtherTarget: BigNumber; 64 | wrapEtherThresholdPerChain: { 65 | [chainId: number]: BigNumber; 66 | }; 67 | wrapEtherThreshold: BigNumber; 68 | } 69 | 70 | export function isAliasConfig(config: ChainTokenConfig | ChainTokenInventory): config is ChainTokenInventory { 71 | return ( 72 | Object.keys(config).every((k) => ethersUtils.isAddress(k)) || Object.keys(config).every((k) => TOKEN_SYMBOLS_MAP[k]) 73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /src/interfaces/Report.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from "../utils"; 2 | 3 | export enum BundleAction { 4 | PROPOSED = "proposed", 5 | DISPUTED = "disputed", 6 | CANCELED = "canceled", 7 | } 8 | 9 | export enum BalanceType { 10 | // Current balance. 11 | CURRENT = "current", 12 | // Balance from any pending bundle's refunds. 13 | PENDING = "pending", 14 | // Balance from next bundle's refunds. 15 | NEXT = "next", 16 | // Balance from pending cross chain transfers. 17 | PENDING_TRANSFERS = "pending transfers", 18 | // Total balance across current, pending, next. 19 | TOTAL = "total", 20 | } 21 | 22 | export interface RelayerBalanceCell { 23 | // This should match BalanceType values. We can't use BalanceType directly because interface only accepts static keys, 24 | // not those, such as enums, that are determined at runtime. 25 | [balanceType: string]: BigNumber; 26 | } 27 | 28 | export interface RelayerBalanceColumns { 29 | // Including a column for "Total". 30 | [chainName: string]: RelayerBalanceCell; 31 | } 32 | 33 | // The relayer balance table is organized as below: 34 | // 1. Rows are token symbols, e.g. WETH, USDC 35 | // 2. Columns are chains, e.g. Optimism. 36 | // 3. Each column is further broken into subcolumns: current (live balance), pending (balance from pending bundle's refunds) 37 | // and next bundle (balance from next bundle's refunds) 38 | export interface RelayerBalanceTable { 39 | [tokenSymbol: string]: RelayerBalanceColumns; 40 | } 41 | 42 | export interface RelayerBalanceReport { 43 | [relayer: string]: RelayerBalanceTable; 44 | } 45 | -------------------------------------------------------------------------------- /src/interfaces/SpokePool.ts: -------------------------------------------------------------------------------- 1 | import { SpokePoolClient } from "../clients"; 2 | 3 | export interface SpokePoolClientsByChain { 4 | [chainId: number]: SpokePoolClient; 5 | } 6 | -------------------------------------------------------------------------------- /src/interfaces/Token.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from "../utils"; 2 | import { SortableEvent } from "."; 3 | 4 | export interface TokenTransfer extends SortableEvent { 5 | value: BigNumber; 6 | from: string; 7 | to: string; 8 | } 9 | 10 | export interface TransfersByTokens { 11 | [token: string]: { 12 | incoming: TokenTransfer[]; 13 | outgoing: TokenTransfer[]; 14 | }; 15 | } 16 | 17 | export interface TransfersByChain { 18 | [chainId: number]: TransfersByTokens; 19 | } 20 | -------------------------------------------------------------------------------- /src/interfaces/Universal.ts: -------------------------------------------------------------------------------- 1 | import { SortableEvent } from "."; 2 | import { BigNumber } from "../utils"; 3 | 4 | // Flattened HubPoolStore Event 5 | export interface StoredCallDataEvent extends SortableEvent { 6 | target: string; 7 | data: string; 8 | nonce: BigNumber; 9 | } 10 | 11 | // Flattened Universal SpokePool Event 12 | export interface RelayedCallDataEvent extends SortableEvent { 13 | nonce: BigNumber; 14 | caller: string; 15 | } 16 | -------------------------------------------------------------------------------- /src/interfaces/ZkApi.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from "../utils"; 2 | 3 | // --- API Interaction Types --- 4 | export interface ApiProofRequest { 5 | src_chain_contract_address: string; 6 | src_chain_storage_slots: string[]; 7 | src_chain_block_number: number; // u64 on Rust API side 8 | dst_chain_contract_from_head: number; // u64 on Rust API side 9 | dst_chain_contract_from_header: string; 10 | } 11 | 12 | export type ProofStatus = "pending" | "success" | "errored"; 13 | 14 | export interface SP1HeliosProofData { 15 | proof: string; 16 | public_values: string; 17 | } 18 | 19 | export interface ProofStateResponse { 20 | proof_id: string; 21 | status: ProofStatus; 22 | update_calldata?: SP1HeliosProofData; // Present only if status is "success" 23 | error_message?: string; // Present only if status is "errored" 24 | } 25 | 26 | // ABI for `public_values` returned from ZK API as part of `SP1HeliosProofData` 27 | export const PROOF_OUTPUTS_ABI_TUPLE = `tuple( 28 | bytes32 executionStateRoot, 29 | bytes32 newHeader, 30 | bytes32 nextSyncCommitteeHash, 31 | uint256 newHead, 32 | bytes32 prevHeader, 33 | uint256 prevHead, 34 | bytes32 syncCommitteeHash, 35 | bytes32 startSyncCommitteeHash, 36 | tuple(bytes32 key, bytes32 value, address contractAddress)[] slots 37 | )`; 38 | 39 | export type ProofOutputs = { 40 | executionStateRoot: string; 41 | newHeader: string; 42 | nextSyncCommitteeHash: string; 43 | newHead: BigNumber; 44 | prevHeader: string; 45 | prevHead: BigNumber; 46 | syncCommitteeHash: string; 47 | startSyncCommitteeHash: string; 48 | slots: { key: string; value: string; contractAddress: string }[]; 49 | }; 50 | -------------------------------------------------------------------------------- /src/libexec/types.ts: -------------------------------------------------------------------------------- 1 | export { Log } from "../interfaces"; 2 | export { SpokePoolClientMessage } from "../clients"; 3 | 4 | export type ScraperOpts = { 5 | lookback?: number; // Event lookback (in seconds). 6 | deploymentBlock: number; // SpokePool deployment block 7 | maxBlockRange?: number; // Maximum block range for paginated getLogs queries. 8 | filterArgs?: { [event: string]: string[] }; // Event-specific filter criteria to apply. 9 | }; 10 | -------------------------------------------------------------------------------- /src/libexec/util/evm/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./util"; 2 | -------------------------------------------------------------------------------- /src/libexec/util/evm/util.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import { Contract, EventFilter } from "ethers"; 3 | import { getNetworkName, isDefined, paginatedEventQuery, Profiler, winston } from "../../../utils"; 4 | import { Log, ScraperOpts } from "../../types"; 5 | 6 | /** 7 | * Given an event name and contract, return the corresponding Ethers EventFilter object. 8 | * @param contract Ethers Contract instance. 9 | * @param eventName The name of the event to be filtered. 10 | * @param filterArgs Optional filter arguments to be applied. 11 | * @returns An Ethers EventFilter instance. 12 | */ 13 | export function getEventFilter(contract: Contract, eventName: string, filterArgs?: string[]): EventFilter { 14 | const filter = contract.filters[eventName]; 15 | if (!isDefined(filter)) { 16 | throw new Error(`Event ${eventName} not defined for contract`); 17 | } 18 | 19 | return isDefined(filterArgs) ? filter(...filterArgs) : filter(); 20 | } 21 | 22 | /** 23 | * Get a general event filter mapping to be used for filtering SpokePool contract events. 24 | * This is currently only useful for filtering the relayer address on FilledRelay events. 25 | * @param relayer Optional relayer address to filter on. 26 | * @returns An argument array for input to an Ethers EventFilter. 27 | */ 28 | export function getEventFilterArgs(relayer?: string): { [event: string]: (null | string)[] } { 29 | const FilledRelay = !isDefined(relayer) 30 | ? undefined 31 | : [null, null, null, null, null, null, null, null, null, null, relayer]; 32 | 33 | return { FilledRelay }; 34 | } 35 | 36 | /** 37 | * Given a SpokePool contract instance and an event name, scrape all corresponding events and submit them to the 38 | * parent process (if defined). 39 | * @param spokePool Ethers Contract instance. 40 | * @param eventName The name of the event to be filtered. 41 | * @param opts Options to configure event scraping behaviour. 42 | * @returns void 43 | */ 44 | export async function scrapeEvents( 45 | spokePool: Contract, 46 | eventName: string, 47 | opts: ScraperOpts & { toBlock: number }, 48 | logger?: winston.Logger 49 | ): Promise { 50 | const profiler = new Profiler({ 51 | logger, 52 | at: "scrapeEvents", 53 | }); 54 | const { lookback, deploymentBlock, filterArgs, maxBlockRange, toBlock } = opts; 55 | const { chainId } = await spokePool.provider.getNetwork(); 56 | const chain = getNetworkName(chainId); 57 | 58 | const fromBlock = Math.max(toBlock - (lookback ?? deploymentBlock), deploymentBlock); 59 | assert(toBlock > fromBlock, `${toBlock} > ${fromBlock}`); 60 | const searchConfig = { from: fromBlock, to: toBlock, maxLookBack: maxBlockRange }; 61 | 62 | const mark = profiler.start("paginatedEventQuery"); 63 | const filter = getEventFilter(spokePool, eventName, filterArgs[eventName]); 64 | const events = await paginatedEventQuery(spokePool, filter, searchConfig); 65 | mark.stop({ 66 | message: `Scraped ${events.length} ${chain} ${eventName} events.`, 67 | numEvents: events.length, 68 | chain, 69 | eventName, 70 | searchConfig, 71 | }); 72 | 73 | return events; 74 | } 75 | -------------------------------------------------------------------------------- /src/libexec/util/ipc.ts: -------------------------------------------------------------------------------- 1 | import { utils as sdkUtils } from "@across-protocol/sdk"; 2 | import { isDefined, sortEventsAscending } from "../../utils"; 3 | import { Log, SpokePoolClientMessage } from "./../types"; 4 | 5 | /** 6 | * Given the inputs for a SpokePoolClient update, consolidate the inputs into a message and submit it to the parent 7 | * process (if defined). 8 | * @param blockNumber Block number up to which the update applies. 9 | * @param currentTime The SpokePool timestamp at blockNumber. 10 | * @param events An array of Log objects to be submitted. 11 | * @returns void 12 | */ 13 | export function postEvents(blockNumber: number, currentTime: number, events: Log[]): boolean { 14 | if (!isDefined(process.send)) { 15 | // Process was probably started standalone. 16 | // https://nodejs.org/api/process.html#processsendmessage-sendhandle-options-callback 17 | return true; 18 | } 19 | 20 | events = sortEventsAscending(events); 21 | const message: SpokePoolClientMessage = { 22 | blockNumber, 23 | currentTime, 24 | nEvents: events.length, 25 | data: JSON.stringify(events, sdkUtils.jsonReplacerWithBigNumbers), 26 | }; 27 | 28 | const msg = JSON.stringify(message); 29 | try { 30 | process.send(msg); 31 | } catch { 32 | return false; 33 | } 34 | 35 | return true; 36 | } 37 | 38 | /** 39 | * Given an event removal notification, post the message to the parent process. 40 | * @param event Log instance. 41 | * @returns void 42 | */ 43 | export function removeEvent(event: Log): void { 44 | if (!isDefined(process.send)) { 45 | return; 46 | } 47 | 48 | const message: SpokePoolClientMessage = { 49 | event: JSON.stringify(event, sdkUtils.jsonReplacerWithBigNumbers), 50 | }; 51 | process.send(JSON.stringify(message)); 52 | } 53 | -------------------------------------------------------------------------------- /src/monitor/.env.sample: -------------------------------------------------------------------------------- 1 | POLLING_DELAY= 2 | HUBPOOL_STARTING_BLOCK_NUMBER= 3 | HUBPOOL_ENDING_BLOCK_NUMBER= 4 | UTILIZATION_ENABLED= 5 | UNKNOWN_ROOT_BUNDLE_CALLERS_ENABLED= 6 | HUB_CHAIN_ID= 7 | UTILIZATION_THRESHOLD= 8 | WHITELISTED_DATA_WORKERS= 9 | WHITELISTED_RELAYERS= 10 | SPOKE_POOL_CHAIN_IDS= 11 | SPOKE_POOLS_BLOCKS={ 4: { startingBlock: 0, endingBlock: 0 }, 10: { startingBlock: 0, endingBlock: 0 } } 12 | MONITOR_REPORT_ENABLED= 13 | MONITORED_RELAYERS=["0xAddress"] 14 | -------------------------------------------------------------------------------- /src/utils/AddressUtils.ts: -------------------------------------------------------------------------------- 1 | import { TOKEN_SYMBOLS_MAP } from "@across-protocol/constants"; 2 | import { compareAddressesSimple, ethers, getRemoteTokenForL1Token, isDefined } from "."; 3 | 4 | export function includesAddressSimple(address: string | undefined, list: string[]): boolean { 5 | if (!isDefined(address)) { 6 | return false; 7 | } 8 | return list.filter((listAddress) => compareAddressesSimple(address, listAddress)).length > 0; 9 | } 10 | 11 | /** 12 | * Match the token symbol for the given token address and chain ID. 13 | * @param tokenAddress The token address to resolve the symbol for. 14 | * @param chainId The chain ID to resolve the symbol for. 15 | * @returns The token symbols for the given token address and chain ID, or undefined if the token address is not 16 | * recognized. 17 | */ 18 | export function matchTokenSymbol(tokenAddress: string, chainId: number): string[] { 19 | // We can match one l1 token address on multiple symbols in some special cases, like ETH/WETH. 20 | return Object.values(TOKEN_SYMBOLS_MAP) 21 | .filter(({ addresses }) => addresses[chainId]?.toLowerCase() === tokenAddress.toLowerCase()) 22 | .map(({ symbol }) => symbol); 23 | } 24 | 25 | /** 26 | * Match the token decimals for a given token symbol. 27 | * @param tokenSymbol Symbol of the token to query. 28 | * @returns The number of ERC20 decimals configured for the requested token. 29 | */ 30 | export function resolveTokenDecimals(tokenSymbol: string): number { 31 | const decimals = TOKEN_SYMBOLS_MAP[tokenSymbol]?.decimals; 32 | if (decimals === undefined) { 33 | throw new Error(`Unrecognized token symbol: ${tokenSymbol}`); 34 | } 35 | return decimals; 36 | } 37 | 38 | /** 39 | * @notice Returns the token address for a given token address on a given chain ID and lets caller specify 40 | * whether they want the native or bridged USDC for an L2 chain, if the l1 token is USDC. 41 | * @param l1Token L1 token address 42 | * @param hubChainId L1 token chain 43 | * @param l2ChainId Chain on which the token address is requested 44 | * @param isNativeUsdc True if caller wants native USDC on L2, false if caller wants bridged USDC on L2 45 | * @returns L2 token address 46 | */ 47 | export function getTranslatedTokenAddress( 48 | l1Token: string, 49 | hubChainId: number, 50 | l2ChainId: number, 51 | isNativeUsdc = false 52 | ): string { 53 | // Base Case 54 | if (hubChainId === l2ChainId) { 55 | return l1Token; 56 | } 57 | // Native USDC or not USDC, we can just look up in the token map directly. 58 | if (isNativeUsdc || !compareAddressesSimple(l1Token, TOKEN_SYMBOLS_MAP.USDC.addresses[hubChainId])) { 59 | return getRemoteTokenForL1Token(l1Token, l2ChainId, hubChainId); 60 | } 61 | // Handle USDC special case where there could be multiple versions of USDC on an L2: Bridged or Native 62 | const bridgedUsdcMapping = Object.values(TOKEN_SYMBOLS_MAP).find( 63 | ({ symbol, addresses }) => 64 | symbol !== "USDC" && compareAddressesSimple(addresses[hubChainId], l1Token) && addresses[l2ChainId] 65 | ); 66 | return bridgedUsdcMapping?.addresses[l2ChainId]; 67 | } 68 | 69 | export function checkAddressChecksum(tokenAddress: string): boolean { 70 | return ethers.utils.getAddress(tokenAddress) === tokenAddress; 71 | } 72 | -------------------------------------------------------------------------------- /src/utils/AnchorUtils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getNodeUrlList, 3 | CHAIN_IDs, 4 | DEFAULT_SIMULATED_RELAYER_ADDRESS_SVM, 5 | isDefined, 6 | getSvmSignerFromEvmSigner, 7 | Signer, 8 | } from "./"; 9 | import { AnchorProvider, Program, Idl, web3, Wallet as SolanaWallet } from "@coral-xyz/anchor"; 10 | 11 | export async function getAnchorProgram(idl: Idl, signer?: Signer): Promise { 12 | const wallet = isDefined(signer) 13 | ? new SolanaWallet(getSvmSignerFromEvmSigner(signer)) 14 | : (AnchorVoidSigner(new web3.PublicKey(DEFAULT_SIMULATED_RELAYER_ADDRESS_SVM)) as SolanaWallet); 15 | const provider = getAnchorProvider(wallet); 16 | return new Program(idl, provider); 17 | } 18 | 19 | export function getAnchorProvider(wallet: SolanaWallet): AnchorProvider { 20 | const nodeUrlList = getNodeUrlList(CHAIN_IDs.SOLANA); 21 | return new AnchorProvider(new web3.Connection(Object.values(nodeUrlList)[0]), wallet); 22 | } 23 | 24 | export function toPublicKey(pubkey: string): web3.PublicKey { 25 | return new web3.PublicKey(pubkey); 26 | } 27 | 28 | const AnchorVoidSigner = (publicKey: web3.PublicKey) => { 29 | return { 30 | publicKey, 31 | signTransaction: async (_tx: web3.Transaction): Promise => { 32 | _tx; 33 | throw new Error("Cannot sign transaction with a void signer"); 34 | }, 35 | signAllTransactions: async (_txs: web3.Transaction[]): Promise => { 36 | _txs; 37 | throw new Error("Cannot sign transactions with a void signer"); 38 | }, 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /src/utils/BNUtils.ts: -------------------------------------------------------------------------------- 1 | import { ConvertDecimals } from "./"; 2 | // eslint-disable-next-line no-restricted-imports 3 | import { BigNumber } from "@ethersproject/bignumber"; 4 | 5 | // eslint-disable-next-line no-restricted-imports 6 | export * from "@ethersproject/bignumber"; 7 | 8 | export function bnComparatorDescending(a: BigNumber, b: BigNumber): -1 | 0 | 1 { 9 | if (b.gt(a)) { 10 | return 1; 11 | } else if (a.gt(b)) { 12 | return -1; 13 | } else { 14 | return 0; 15 | } 16 | } 17 | 18 | export function bnComparatorAscending(a: BigNumber, b: BigNumber): -1 | 0 | 1 { 19 | if (a.gt(b)) { 20 | return 1; 21 | } else if (b.gt(a)) { 22 | return -1; 23 | } else { 24 | return 0; 25 | } 26 | } 27 | 28 | export function floatToBN(float: string | number, precision: number): BigNumber { 29 | const strFloat = String(float); 30 | const adjustment = strFloat.length - strFloat.indexOf(".") - 1; 31 | const scaledAmount = Number(float) * 10 ** adjustment; 32 | const bnAmount = BigNumber.from(Math.round(scaledAmount)); 33 | return ConvertDecimals(adjustment, precision)(bnAmount); 34 | } 35 | -------------------------------------------------------------------------------- /src/utils/BinanceUtils.ts: -------------------------------------------------------------------------------- 1 | import Binance, { HttpMethod, type Binance as BinanceApi } from "binance-api-node"; 2 | import minimist from "minimist"; 3 | import { getGckmsConfig, retrieveGckmsKeys, isDefined, assert } from "./"; 4 | 5 | // Store global promises on Gckms key retrievel actions so that we don't retrieve the same key multiple times. 6 | let binanceSecretKeyPromise = undefined; 7 | 8 | type WithdrawalQuota = { 9 | wdQuota: number; 10 | usedWdQuota: number; 11 | }; 12 | 13 | /** 14 | * Returns an API client to interface with Binance 15 | * @param url The base HTTP url to use to connect to Binance. 16 | * @returns A Binance client from `binance-api-node`. 17 | */ 18 | export async function getBinanceApiClient(url = "https://api.binance.com") { 19 | const apiKey = process.env["BINANCE_API_KEY"]; 20 | const secretKey = (await getBinanceSecretKey()) ?? process.env["BINANCE_HMAC_KEY"]; 21 | assert(isDefined(apiKey) && isDefined(secretKey), "Binance client cannot be constructed due to missing keys."); 22 | return Binance({ 23 | apiKey, 24 | apiSecret: secretKey, 25 | httpBase: url, 26 | }); 27 | } 28 | 29 | /** 30 | * Retrieves a Binance API secret key from GCKMS if the key is stored in GCKMS. 31 | * @returns A base64 encoded secret key, or undefined if the key is not present in GCKMS. 32 | */ 33 | async function getBinanceSecretKey(): Promise { 34 | binanceSecretKeyPromise ??= retrieveBinanceSecretKeyFromCLIArgs(); 35 | return binanceSecretKeyPromise; 36 | } 37 | 38 | /** 39 | * Retrieves a Binance HMAC secret key based on CLI args. 40 | * @returns A Binance API secret key if present in the arguments, or otherwise `undefined`. 41 | */ 42 | async function retrieveBinanceSecretKeyFromCLIArgs(): Promise { 43 | const opts = { 44 | string: ["binanceSecretKey"], 45 | }; 46 | const args = minimist(process.argv.slice(2), opts); 47 | if (!isDefined(args.binanceSecretKey)) { 48 | return undefined; 49 | } 50 | const binanceKeys = await retrieveGckmsKeys(getGckmsConfig([args.binanceSecretKey])); 51 | if (binanceKeys.length === 0) { 52 | return undefined; 53 | } 54 | return binanceKeys[0].slice(2); 55 | } 56 | 57 | /** 58 | * Retrieves the input client account's withdrawal quota. 59 | * @dev This is in a utility function since the Binance API does not natively support calling this endpoint. 60 | * @returns an object with two fields: `wdQuota` and `usedWdQuota`, corresponding to the total amount 61 | * available to rebalance per day and the amount already used. 62 | */ 63 | export async function getBinanceWithdrawalLimits(binanceApi: BinanceApi): Promise { 64 | const unparsedQuota = await binanceApi.privateRequest("GET" as HttpMethod, "/sapi/v1/capital/withdraw/quota", {}); 65 | return { 66 | wdQuota: unparsedQuota["wdQuota"], 67 | usedWdQuota: unparsedQuota["usedWdQuota"], 68 | }; 69 | } 70 | -------------------------------------------------------------------------------- /src/utils/CLIUtils.ts: -------------------------------------------------------------------------------- 1 | import minimist from "minimist"; 2 | import { Signer } from "ethers"; 3 | import { constants as sdkConsts } from "@across-protocol/sdk"; 4 | import { SignerOptions, getSigner } from "./SignerUtils"; 5 | import { isDefined } from "./TypeGuards"; 6 | 7 | const keyTypes = ["secret", "mnemonic", "privateKey", "gckms", "void"]; 8 | 9 | /** 10 | * Retrieves a signer based on both the CLI args and the env. 11 | * @returns A signer based on the CLI args. 12 | */ 13 | export function retrieveSignerFromCLIArgs(): Promise { 14 | const opts = { 15 | string: ["wallet", "keys", "address"], 16 | default: { 17 | wallet: "secret", 18 | address: sdkConsts.DEFAULT_SIMULATED_RELAYER_ADDRESS, 19 | }, 20 | }; 21 | 22 | // Call into the process' argv to retrieve the CLI args. 23 | const args = minimist(process.argv.slice(2), opts); 24 | 25 | // Resolve the wallet type & verify that it is valid. 26 | const keyType = args.wallet ?? "secret"; 27 | if (!isValidKeyType(keyType)) { 28 | throw new Error(`Unsupported key type (${keyType}); expected one of: ${keyTypes.join(", ")}.`); 29 | } 30 | 31 | // Build out the signer options to pass to the signer utils. 32 | const signerOptions: SignerOptions = { 33 | keyType, 34 | gckmsKeys: isDefined(args.keys) ? [args.keys] : [], 35 | roAddress: args.address, 36 | cleanEnv: false, // TODO: We don't want to clean the env for now. This will be changed in the future. 37 | }; 38 | 39 | // Return the signer. 40 | return getSigner(signerOptions); 41 | } 42 | 43 | /** 44 | * Checks if the key type is valid for being passed as input to the CLI 45 | * @param keyType The key type to check. 46 | * @returns True if the key type is valid, false otherwise. 47 | */ 48 | function isValidKeyType(keyType: string): keyType is "secret" | "mnemonic" | "privateKey" | "gckms" | "void" { 49 | return keyTypes.includes(keyType); 50 | } 51 | -------------------------------------------------------------------------------- /src/utils/ContractUtils.ts: -------------------------------------------------------------------------------- 1 | import * as typechain from "@across-protocol/contracts"; // TODO: refactor once we've fixed export from contract repo 2 | import { CHAIN_IDs, getNetworkName, Contract, Signer, getDeployedAddress, getDeployedBlockNumber } from "."; 3 | 4 | // Return an ethers contract instance for a deployed contract, imported from the Across-protocol contracts repo. 5 | export function getDeployedContract(contractName: string, networkId: number, signer?: Signer): Contract { 6 | try { 7 | const address = getDeployedAddress(contractName, networkId); 8 | // If the contractName is SpokePool then we need to modify it to find the correct contract factory artifact. 9 | const factoryName = `${contractName === "SpokePool" ? castSpokePoolName(networkId) : contractName}__factory`; 10 | const artifact = typechain[factoryName]; 11 | return new Contract(address, artifact.abi, signer); 12 | } catch (error) { 13 | throw new Error(`Could not find address for contract ${contractName} on ${networkId} (${error})`); 14 | } 15 | } 16 | 17 | // If the name of the contract is SpokePool then we need to apply a transformation on the name to get the correct 18 | // contract factory name. For example, if the network is "mainnet" then the contract is called Ethereum_SpokePool. 19 | export function castSpokePoolName(networkId: number): string { 20 | let networkName: string; 21 | switch (networkId) { 22 | case CHAIN_IDs.MAINNET: 23 | case CHAIN_IDs.SEPOLIA: 24 | return "Ethereum_SpokePool"; 25 | case CHAIN_IDs.ARBITRUM: 26 | return "Arbitrum_SpokePool"; 27 | case CHAIN_IDs.BSC: 28 | return "Universal_SpokePool"; 29 | case CHAIN_IDs.ZK_SYNC: 30 | return "ZkSync_SpokePool"; 31 | case CHAIN_IDs.SOLANA: 32 | case CHAIN_IDs.SOLANA_DEVNET: 33 | return "SvmSpoke"; 34 | case CHAIN_IDs.SONEIUM: 35 | return "Cher_SpokePool"; 36 | case CHAIN_IDs.UNICHAIN || CHAIN_IDs.UNICHAIN_SEPOLIA: 37 | return "DoctorWho_SpokePool"; 38 | default: 39 | networkName = getNetworkName(networkId); 40 | } 41 | 42 | return `${networkName.replace(" ", "")}_SpokePool`; 43 | } 44 | 45 | // For a chain ID and optional SpokePool address, return a Contract instance with the corresponding ABI. 46 | export function getSpokePool(chainId: number, address?: string): Contract { 47 | const factoryName = castSpokePoolName(chainId); 48 | const artifact = typechain[`${factoryName}__factory`]; 49 | return new Contract(address ?? getDeployedAddress("SpokePool", chainId), artifact.abi); 50 | } 51 | 52 | export function getParamType(contractName: string, functionName: string, paramName: string): string { 53 | const artifact = typechain[`${[contractName]}__factory`]; 54 | const fragment = artifact.abi.find((fragment: { name: string }) => fragment.name === functionName); 55 | return fragment.inputs.find((input: { name: string }) => input.name === paramName) || ""; 56 | } 57 | 58 | export function getDeploymentBlockNumber(contractName: string, networkId: number): number { 59 | try { 60 | return Number(getDeployedBlockNumber(contractName, networkId)); 61 | } catch (error) { 62 | throw new Error(`Could not find deployment block for contract ${contractName} on ${networkId}`); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/utils/DepositUtils.ts: -------------------------------------------------------------------------------- 1 | import { RelayData } from "../interfaces"; 2 | import { utils } from "@across-protocol/sdk"; 3 | const { toBytes32 } = utils; 4 | 5 | export function convertRelayDataParamsToBytes32(relayData: RelayData): RelayData { 6 | return { 7 | ...relayData, 8 | depositor: toBytes32(relayData.depositor), 9 | recipient: toBytes32(relayData.recipient), 10 | inputToken: toBytes32(relayData.inputToken), 11 | outputToken: toBytes32(relayData.outputToken), 12 | exclusiveRelayer: toBytes32(relayData.exclusiveRelayer), 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/ExecutionUtils.ts: -------------------------------------------------------------------------------- 1 | import { delay, winston } from "./"; 2 | 3 | export function exit(code: number): void { 4 | // eslint-disable-next-line no-process-exit 5 | process.exit(code); 6 | } 7 | 8 | export async function processEndPollingLoop( 9 | logger: winston.Logger, 10 | fileName: string, 11 | pollingDelay: number 12 | ): Promise { 13 | if (pollingDelay === 0) { 14 | logger.debug({ at: `${fileName}#index`, message: "End of serverless execution loop - terminating process" }); 15 | return true; 16 | } 17 | 18 | logger.debug({ at: `${fileName}#index`, message: `End of execution loop - waiting polling delay ${pollingDelay}s` }); 19 | await delay(pollingDelay); 20 | return false; 21 | } 22 | 23 | export function startupLogLevel(config: { pollingDelay: number }): string { 24 | return config.pollingDelay > 0 ? "info" : "debug"; 25 | } 26 | 27 | export function rejectAfterDelay(seconds: number, message = ""): Promise { 28 | return new Promise((_, reject) => { 29 | setTimeout(reject, seconds * 1000, { 30 | status: "timeout", 31 | message: `Execution took longer than ${seconds}s. ${message}`, 32 | }); 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /src/utils/GckmsUtils.ts: -------------------------------------------------------------------------------- 1 | // TODO: this can be replaced by an import from @uma/common when exported from the protocol package. The code herein is 2 | // a reduced version of that found in common. 3 | import kms from "@google-cloud/kms"; 4 | import { Storage } from "@google-cloud/storage"; 5 | import fs from "fs"; 6 | export interface KeyConfig { 7 | projectId: string; 8 | locationId: string; 9 | keyRingId: string; 10 | cryptoKeyId: string; 11 | ciphertextBucket: string; 12 | ciphertextFilename: string; 13 | } 14 | export interface GckmsConfig { 15 | [network: string]: { 16 | [keyName: string]: KeyConfig; 17 | }; 18 | } 19 | 20 | const { GCP_STORAGE_CONFIG } = process.env; 21 | 22 | // Allows the environment to customize the config that's used to interact with google cloud storage. 23 | // Relevant options can be found here: https://googleapis.dev/nodejs/storage/latest/global.html#StorageOptions. 24 | // Specific fields of interest: 25 | // - timeout: allows the env to set the timeout for all http requests. 26 | // - retryOptions: object that allows the caller to specify how the library retries. 27 | const storageConfig = GCP_STORAGE_CONFIG ? JSON.parse(GCP_STORAGE_CONFIG) : undefined; 28 | 29 | export function getGckmsConfig(keys: string[]): KeyConfig[] { 30 | let configOverride: GckmsConfig = {}; 31 | if (process.env.GCKMS_CONFIG) { 32 | configOverride = JSON.parse(process.env.GCKMS_CONFIG); 33 | } else { 34 | const overrideFname = ".GckmsOverride.js"; 35 | try { 36 | if (fs.existsSync(`${__dirname}/${overrideFname}`)) { 37 | configOverride = require(`./${overrideFname}`); 38 | } 39 | } catch (err) { 40 | // eslint-disable-next-line no-console 41 | console.error(err); 42 | } 43 | } 44 | 45 | const keyConfigs = keys.map((keyName: string): KeyConfig => { 46 | return (configOverride["mainnet"][keyName] || {}) as KeyConfig; // Hardcode to "mainnet" network. This makes no impact key retrieval. 47 | }); 48 | 49 | return keyConfigs; 50 | } 51 | 52 | export async function retrieveGckmsKeys(gckmsConfigs: KeyConfig[]): Promise { 53 | return await Promise.all( 54 | gckmsConfigs.map(async (config) => { 55 | const storage = new Storage(storageConfig); 56 | const keyMaterialBucket = storage.bucket(config.ciphertextBucket); 57 | const ciphertextFile = keyMaterialBucket.file(config.ciphertextFilename); 58 | const contentsBuffer = (await ciphertextFile.download())[0]; 59 | const ciphertext = contentsBuffer.toString("base64"); 60 | const client = new kms.KeyManagementServiceClient(); // Send the request to decrypt the downloaded file. 61 | const name = client.cryptoKeyPath(config.projectId, config.locationId, config.keyRingId, config.cryptoKeyId); 62 | const [result] = await client.decrypt({ name, ciphertext }); 63 | if (!(result.plaintext instanceof Uint8Array)) { 64 | throw new Error("result.plaintext wrong type"); 65 | } 66 | return "0x" + Buffer.from(result.plaintext).toString().trim(); 67 | }) 68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /src/utils/HeliosUtils.ts: -------------------------------------------------------------------------------- 1 | import { ethers, Provider, Signer } from "."; 2 | import SP1_HELIOS_ABI from "../common/abi/SP1Helios.json"; 3 | import UNIVERSAL_SPOKE_ABI from "../common/abi/Universal_SpokePool.json"; 4 | 5 | /** 6 | * Retrieves an ethers.Contract instance for the SP1Helios contract. 7 | * The SP1Helios contract address is fetched from the `helios()` view function of the `evmSpokePool`. 8 | * 9 | * @param evmSpokePool SpokePool contract instance with a `helios()` view function. 10 | * @param signerOrProvider Signer or Provider for the SP1Helios contract. 11 | * @returns Promise resolving to an SP1Helios ethers.Contract instance. 12 | * @throws If `helios()` call fails or `evmSpokePool` is invalid. 13 | */ 14 | export async function getSp1HeliosContractEVM( 15 | evmSpokePool: ethers.Contract, 16 | signerOrProvider: Signer | Provider 17 | ): Promise { 18 | const universalSpokePoolContract = new ethers.Contract( 19 | evmSpokePool.address, 20 | UNIVERSAL_SPOKE_ABI, 21 | evmSpokePool.provider 22 | ); 23 | const heliosAddress = await universalSpokePoolContract.helios(); 24 | return new ethers.Contract(heliosAddress, SP1_HELIOS_ABI as any, signerOrProvider); 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/Help.ts: -------------------------------------------------------------------------------- 1 | export function usage(badInput: string | undefined = undefined): boolean { 2 | let usageStr = badInput ? `\nUnrecognized input: "${badInput}".\n\n` : ""; 3 | const walletTypes = "secret|mnemonic|privateKey|gckms|void"; 4 | const walletUsage = `--wallet <${walletTypes}> | --wallet void [--address ]`; 5 | 6 | usageStr += ` 7 | Usage: 8 | \tnode ./dist/index.js --help 9 | \tnode ./dist/index.js [-h] <--monitor|--relayer> [${walletUsage}] 10 | \tnode ./dist/index.js [-h] <--dataworker|--finalizer> [${walletUsage}] 11 | `.slice(1); // Skip leading newline 12 | 13 | // eslint-disable-next-line no-console 14 | console.log(usageStr); 15 | 16 | // eslint-disable-next-line no-process-exit 17 | process.exit(badInput === undefined ? 0 : 9); 18 | } 19 | 20 | export function help(): void { 21 | const botRepoUrl = "https://github.com/across-protocol/relayer"; 22 | const relayerDocsUrl = "https://docs.across.to/relayers/running-a-relayer"; 23 | const helpStr = ` 24 | Across v2 Bot 25 | 26 | Description: 27 | \tThis application performs various duties in support of the Across v2 28 | \tecosystem. The application implements four key functionalities, divided 29 | \tcoarsely between *Basic* and *Advanced* use cases. 30 | 31 | \tBasic functionalities are designed for widespread use. These include: 32 | 33 | \t Monitor: Monitor and report on all relay and bundle events. 34 | \t Relayer: Perform transaction relays for eligible SpokePool deposits. 35 | 36 | \tOperating a relayer can be a profitable activity and helps to improve the 37 | \tspeed and reliability of Across for its users. Note that as with any 38 | \tautomated operations involving funds, operating a relayer implies some 39 | \tunavoidable level of operational risk. Loss of funds is a possibility. 40 | \tBefore operating a relayer, please research and understand the implicit 41 | \trisks associated, and ensure this is compatible with your risk profile. 42 | 43 | \tAdvanced functionalities implement the heavy lifting that is required to 44 | \tkeep Across operating. These include: 45 | 46 | \t Dataworker: Monitor and produce relay bundles for the Hub Pool. 47 | \t Finalizer: Finalize and collect canonical bridge transfers. 48 | 49 | \tRunning a dataworker or finalizer is predominantly a benevolent activity 50 | \tthat will typically not generate a direct profit. Advanced functionalities 51 | \tare intended for use for key Across ecosystem stakeholders, rather than 52 | \tindividuals. 53 | 54 | Links: 55 | \tRepository: ${botRepoUrl} 56 | \tRelayer Instructions: ${relayerDocsUrl} 57 | `.slice(0, -1); // Skip trailing newline 58 | 59 | // eslint-disable-next-line no-console 60 | console.log(helpStr); 61 | usage(); // no return 62 | } 63 | -------------------------------------------------------------------------------- /src/utils/LogUtils.ts: -------------------------------------------------------------------------------- 1 | export type DefaultLogLevels = "debug" | "info" | "warn" | "error"; 2 | 3 | export function stringifyThrownValue(value: unknown): string { 4 | if (value instanceof Error) { 5 | const errToString = value.toString(); 6 | return value.stack 7 | ? value.stack 8 | : value.message || errToString !== "[object Object]" 9 | ? errToString 10 | : "could not extract error from 'Error' instance"; 11 | } else if (value instanceof Object) { 12 | const objStringified = JSON.stringify(value); 13 | return objStringified !== "{}" ? objStringified : "could not extract error from 'Object' instance"; 14 | } else { 15 | return `ThrownValue: ${value.toString()}`; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/utils/MerkleTreeUtils.ts: -------------------------------------------------------------------------------- 1 | import { MerkleTree, EMPTY_MERKLE_ROOT } from "@across-protocol/contracts"; 2 | import { RelayerRefundLeaf, RelayerRefundLeafWithGroup, SlowFillLeaf } from "../interfaces"; 3 | import { getParamType, utils } from "."; 4 | import _ from "lodash"; 5 | import { convertRelayDataParamsToBytes32 } from "./DepositUtils"; 6 | 7 | export function buildSlowRelayTree(relays: SlowFillLeaf[]): MerkleTree { 8 | const hashFn = (_input: SlowFillLeaf) => { 9 | // Clone the input so we can mutate it. 10 | const input = _.cloneDeep(_input); 11 | input.relayData = convertRelayDataParamsToBytes32(input.relayData); 12 | const verifyFn = "verifyV3SlowRelayFulfillment"; 13 | const paramType = getParamType("MerkleLibTest", verifyFn, "slowFill"); 14 | return utils.keccak256(utils.defaultAbiCoder.encode([paramType], [input])); 15 | }; 16 | return new MerkleTree(relays, hashFn); 17 | } 18 | 19 | export function buildRelayerRefundTree(relayerRefundLeaves: RelayerRefundLeaf[]): MerkleTree { 20 | for (let i = 0; i < relayerRefundLeaves.length; i++) { 21 | // The 2 provided parallel arrays must be of equal length. 22 | if (relayerRefundLeaves[i].refundAddresses.length !== relayerRefundLeaves[i].refundAmounts.length) { 23 | throw new Error("Provided lef arrays are not of equal length"); 24 | } 25 | } 26 | 27 | const paramType = getParamType("MerkleLibTest", "verifyRelayerRefund", "refund"); 28 | const hashFn = (input: RelayerRefundLeaf) => utils.keccak256(utils.defaultAbiCoder.encode([paramType], [input])); 29 | return new MerkleTree(relayerRefundLeaves, hashFn); 30 | } 31 | 32 | export { MerkleTree, RelayerRefundLeaf, RelayerRefundLeafWithGroup, EMPTY_MERKLE_ROOT }; 33 | -------------------------------------------------------------------------------- /src/utils/NetworkUtils.ts: -------------------------------------------------------------------------------- 1 | import { utils as sdkUtils } from "@across-protocol/sdk"; 2 | 3 | export const { getNetworkName, getNativeTokenSymbol } = sdkUtils; 4 | 5 | /** 6 | * Returns the origin of a URL. 7 | * @param url A URL. 8 | * @returns The origin of the URL, or "UNKNOWN" if the URL is invalid. 9 | */ 10 | export function getOriginFromURL(url: string): string { 11 | try { 12 | return new URL(url).origin; 13 | } catch (e) { 14 | return "UNKNOWN"; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/ObjectUtils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ 2 | /* eslint-disable @typescript-eslint/no-explicit-any */ 3 | 4 | import lodash from "lodash"; 5 | import { isDefined } from "./TypeGuards"; 6 | 7 | export function count2DDictionaryValues(dictionary: { [key: string]: { [key2: string]: any[] } }): { 8 | [key: string]: { [key2: string]: number }; 9 | } { 10 | return Object.entries(dictionary).reduce((output, [key, innerDict]) => { 11 | const innerDictOutput = Object.entries(innerDict).reduce((innerOutput, [key2, vals]) => { 12 | return { ...innerOutput, [key2]: vals.length }; 13 | }, {}); 14 | return { ...output, [key]: innerDictOutput }; 15 | }, {}); 16 | } 17 | 18 | export function count3DDictionaryValues( 19 | dictionary: { [key: string]: { [key2: string]: any } }, 20 | innerPropName: string 21 | ): { [key: string]: { [key2: string]: number } } { 22 | return Object.entries(dictionary).reduce((output, [key, innerDict]) => { 23 | const innerDictOutput = Object.entries(innerDict).reduce((innerOutput, [key2, vals]) => { 24 | return { ...innerOutput, [key2]: vals[innerPropName].length }; 25 | }, {}); 26 | return { ...output, [key]: innerDictOutput }; 27 | }, {}); 28 | } 29 | 30 | /** 31 | * Deletes keys from an object and returns new copy of object without ignored keys 32 | * @param ignoredKeys 33 | * @param obj 34 | * @returns Objects with ignored keys removed 35 | */ 36 | function deleteIgnoredKeys(ignoredKeys: string[], obj: Record) { 37 | if (!isDefined(obj)) { 38 | return; 39 | } 40 | const newObj = { ...obj }; 41 | for (const key of ignoredKeys) { 42 | delete newObj[key]; 43 | } 44 | return newObj; 45 | } 46 | 47 | export function compareResultsAndFilterIgnoredKeys( 48 | ignoredKeys: string[], 49 | _objA: Record, 50 | _objB: Record 51 | ): boolean { 52 | // Remove ignored keys from copied objects. 53 | const filteredA = deleteIgnoredKeys(ignoredKeys, _objA); 54 | const filteredB = deleteIgnoredKeys(ignoredKeys, _objB); 55 | 56 | // Compare objects without the ignored keys. 57 | return lodash.isEqual(filteredA, filteredB); 58 | } 59 | 60 | export function compareArrayResultsWithIgnoredKeys(ignoredKeys: string[], objA: unknown[], objB: unknown[]): boolean { 61 | // Remove ignored keys from each element of copied arrays. 62 | const filteredA = objA?.map((obj) => deleteIgnoredKeys(ignoredKeys, obj as Record)); 63 | const filteredB = objB?.map((obj) => deleteIgnoredKeys(ignoredKeys, obj as Record)); 64 | 65 | // Compare objects without the ignored keys. 66 | return isDefined(filteredA) && isDefined(filteredB) && lodash.isEqual(filteredA, filteredB); 67 | } 68 | -------------------------------------------------------------------------------- /src/utils/RetryUtils.ts: -------------------------------------------------------------------------------- 1 | import { delay } from "./SDKUtils"; 2 | 3 | export function retryAsync( 4 | fn: (...args: U) => Promise, 5 | numRetries: number, 6 | delayS: number, 7 | ...args: U 8 | ): Promise { 9 | let ret = fn(...args); 10 | for (let i = 0; i < numRetries; i++) { 11 | ret = ret.catch(async () => { 12 | await delay(delayS); 13 | return fn(...args); 14 | }); 15 | } 16 | return ret; 17 | } 18 | -------------------------------------------------------------------------------- /src/utils/SDKUtils.ts: -------------------------------------------------------------------------------- 1 | import * as sdk from "@across-protocol/sdk"; 2 | 3 | // EVMBlockFinder returns _only_ EVMBlock types. 4 | export class EVMBlockFinder extends sdk.arch.evm.EVMBlockFinder {} 5 | export class SVMBlockFinder extends sdk.arch.svm.SVMBlockFinder {} 6 | export type BlockFinderHints = sdk.utils.BlockFinderHints; 7 | 8 | export class AddressAggregator extends sdk.addressAggregator.AddressAggregator {} 9 | export const addressAdapters = sdk.addressAggregator.adapters; 10 | 11 | export class SvmCpiEventsClient extends sdk.arch.svm.SvmCpiEventsClient {} 12 | 13 | export class PriceClient extends sdk.priceClient.PriceClient {} 14 | export const { acrossApi, coingecko, defiLlama } = sdk.priceClient.adapters; 15 | export const { isEVMSpokePoolClient, isSVMSpokePoolClient } = sdk.clients; 16 | 17 | export class Address extends sdk.utils.Address {} 18 | export class EvmAddress extends sdk.utils.EvmAddress {} 19 | export class SvmAddress extends sdk.utils.SvmAddress {} 20 | 21 | export type EvmGasPriceEstimate = sdk.gasPriceOracle.EvmGasPriceEstimate; 22 | 23 | export const { fillStatusArray, populateV3Relay, relayFillStatus, getTimestampForBlock } = sdk.arch.evm; 24 | export const { getAssociatedTokenAddress } = sdk.arch.svm; 25 | export type SVMProvider = sdk.arch.svm.SVMProvider; 26 | 27 | export const { 28 | assign, 29 | groupObjectCountsByProp, 30 | groupObjectCountsByTwoProps, 31 | groupObjectCountsByThreeProps, 32 | delay, 33 | getCurrentTime, 34 | bnZero, 35 | bnOne, 36 | bnUint32Max, 37 | bnUint256Max, 38 | chainIsOPStack, 39 | chainIsOrbit, 40 | chainIsArbitrum, 41 | chainIsProd, 42 | chainIsMatic, 43 | chainIsLinea, 44 | dedupArray, 45 | fixedPointAdjustment, 46 | forEachAsync, 47 | formatEther, 48 | formatUnits, 49 | mapAsync, 50 | parseUnits, 51 | filterAsync, 52 | toBN, 53 | bnToHex, 54 | toWei, 55 | toGWei, 56 | toBNWei, 57 | formatFeePct, 58 | shortenHexStrings, 59 | convertFromWei, 60 | formatGwei, 61 | max, 62 | min, 63 | utf8ToHex, 64 | createFormatFunction, 65 | fromWei, 66 | blockExplorerLink, 67 | isContractDeployedToAddress, 68 | blockExplorerLinks, 69 | createShortHexString: shortenHexString, 70 | compareAddresses, 71 | compareAddressesSimple, 72 | getL1TokenAddress, 73 | getUsdcSymbol, 74 | Profiler, 75 | getMessageHash, 76 | getRelayEventKey, 77 | toBytes32, 78 | validateFillForDeposit, 79 | toAddressType, 80 | chainIsEvm, 81 | chainIsSvm, 82 | ConvertDecimals, 83 | getTokenInfo, 84 | } = sdk.utils; 85 | 86 | export const { 87 | getRefundsFromBundle, 88 | isChainDisabled, 89 | getWidestPossibleExpectedBlockRange, 90 | getEndBlockBuffers, 91 | buildPoolRebalanceLeafTree, 92 | getNetSendAmountForL1Token, 93 | _buildPoolRebalanceRoot, 94 | } = sdk.clients.BundleDataClient; 95 | -------------------------------------------------------------------------------- /src/utils/SuperstructUtils.ts: -------------------------------------------------------------------------------- 1 | import { object, min, string, integer } from "superstruct"; 2 | 3 | export const EventsAddedMessage = object({ 4 | blockNumber: min(integer(), 0), 5 | currentTime: min(integer(), 0), 6 | nEvents: min(integer(), 0), 7 | data: string(), 8 | }); 9 | 10 | export const EventRemovedMessage = object({ 11 | event: string(), 12 | }); 13 | -------------------------------------------------------------------------------- /src/utils/SvmSignerUtils.ts: -------------------------------------------------------------------------------- 1 | import { web3 } from "@coral-xyz/anchor"; 2 | import { isSignerWallet, Signer } from "./"; 3 | import assert from "assert"; 4 | 5 | export function getSvmSignerFromEvmSigner(evmSigner: Signer): web3.Keypair { 6 | assert(isSignerWallet(evmSigner), "Signer is not a Wallet"); 7 | 8 | // Extract the private key from the evm signer and use it to create a svm signer. 9 | const evmPrivateKey = evmSigner._signingKey().privateKey; 10 | return getSvmSignerFromPrivateKey(evmPrivateKey); 11 | } 12 | 13 | export function getSvmSignerFromPrivateKey(privateKey: string): web3.Keypair { 14 | const privateKeyAsBytes = Uint8Array.from(Buffer.from(privateKey.slice(2), "hex")); 15 | return web3.Keypair.fromSeed(privateKeyAsBytes); 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/TokenUtils.ts: -------------------------------------------------------------------------------- 1 | import { TOKEN_EQUIVALENCE_REMAPPING, TOKEN_SYMBOLS_MAP } from "@across-protocol/constants"; 2 | import { constants, utils } from "@across-protocol/sdk"; 3 | import { CONTRACT_ADDRESSES } from "../common"; 4 | import { BigNumberish } from "./BNUtils"; 5 | import { formatUnits } from "./SDKUtils"; 6 | import { isDefined } from "./TypeGuards"; 7 | 8 | const { ZERO_ADDRESS } = constants; 9 | 10 | export const { fetchTokenInfo, getL2TokenAddresses } = utils; 11 | 12 | export function getRemoteTokenForL1Token( 13 | l1Token: string, 14 | remoteChainId: number | string, 15 | hubChainId: number 16 | ): string | undefined { 17 | const tokenMapping = Object.values(TOKEN_SYMBOLS_MAP).find( 18 | ({ addresses }) => addresses[hubChainId] === l1Token && isDefined(addresses[remoteChainId]) 19 | ); 20 | if (!tokenMapping) { 21 | return undefined; 22 | } 23 | const l1TokenSymbol = TOKEN_EQUIVALENCE_REMAPPING[tokenMapping.symbol] ?? tokenMapping.symbol; 24 | return TOKEN_SYMBOLS_MAP[l1TokenSymbol]?.addresses[remoteChainId] ?? tokenMapping.addresses[remoteChainId]; 25 | } 26 | 27 | export function getNativeTokenAddressForChain(chainId: number): string { 28 | return CONTRACT_ADDRESSES[chainId]?.nativeToken?.address ?? ZERO_ADDRESS; 29 | } 30 | 31 | export function getWrappedNativeTokenAddress(chainId: number): string { 32 | const tokenSymbol = utils.getNativeTokenSymbol(chainId); 33 | // If the native token is ETH, then we know the wrapped native token is WETH. Otherwise, some ERC20 token is the native token. 34 | // In PUBLIC_NETWORKS, the native token symbol is the symbol corresponding to the L1 token contract, so "W" should not be prepended 35 | // to the symbol. 36 | const wrappedTokenSymbol = tokenSymbol === "ETH" ? "WETH" : tokenSymbol; 37 | 38 | // Undefined returns should be caught and handled by consumers of this function. 39 | return TOKEN_SYMBOLS_MAP[wrappedTokenSymbol]?.addresses[chainId]; 40 | } 41 | 42 | /** 43 | * Format the given amount of tokens to the correct number of decimals for the given token symbol. 44 | * @param symbol The token symbol to format the amount for. 45 | * @param amount The amount to format. 46 | * @returns The formatted amount as a decimal-inclusive string. 47 | */ 48 | export function formatUnitsForToken(symbol: string, amount: BigNumberish): string { 49 | const decimals = (TOKEN_SYMBOLS_MAP[symbol]?.decimals as number) ?? 18; 50 | return formatUnits(amount, decimals); 51 | } 52 | -------------------------------------------------------------------------------- /src/utils/TypeGuards.ts: -------------------------------------------------------------------------------- 1 | import { utils } from "@across-protocol/sdk"; 2 | 3 | export const { isDefined, isPromiseFulfilled, isPromiseRejected } = utils; 4 | 5 | // This function allows you to test for the key type in an object literal. 6 | // For instance, this would compile in typescript strict: 7 | // const myObj = { a: 1, b: 2, c: 3 } as const; 8 | // const d: string = "a"; 9 | // const myNumber = isKeyOf(d, myObj) ? myObj[d] : 4; 10 | export function isKeyOf( 11 | input: V, 12 | obj: Record 13 | ): input is T { 14 | return input in obj; 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/TypeUtils.ts: -------------------------------------------------------------------------------- 1 | export type MakeOptional = Omit & Partial>; 2 | 3 | export type AnyObject = Record; 4 | -------------------------------------------------------------------------------- /src/utils/UmaUtils.ts: -------------------------------------------------------------------------------- 1 | import { HubPoolClient } from "../clients"; 2 | import { CONTRACT_ADDRESSES } from "../common"; 3 | import { ProposedRootBundle, SortableEvent } from "../interfaces"; 4 | import { isEventOlder, paginatedEventQuery, sortEventsDescending, spreadEventWithBlockNumber } from "./EventUtils"; 5 | import { Contract, ethers, getBlockForTimestamp } from "."; 6 | 7 | export async function getDvmContract(provider: ethers.providers.Provider): Promise { 8 | const { chainId } = await provider.getNetwork(); 9 | const { address, abi } = CONTRACT_ADDRESSES[chainId].VotingV2; 10 | return new Contract(address, abi, provider); 11 | } 12 | 13 | export function getDisputedProposal( 14 | hubPoolClient: HubPoolClient, 15 | disputeEvent: SortableEvent 16 | ): ProposedRootBundle | undefined { 17 | return sortEventsDescending(hubPoolClient.getProposedRootBundles()).find((e) => 18 | isEventOlder(e as SortableEvent, disputeEvent) 19 | ); 20 | } 21 | 22 | export async function getDisputeForTimestamp( 23 | dvm: Contract, 24 | hubPoolClient: HubPoolClient, 25 | disputeRequestTimestamp: number, 26 | disputeRequestBlock?: number 27 | ): Promise { 28 | const filter = dvm.filters.RequestAdded(); 29 | const priceRequestBlock = 30 | disputeRequestBlock !== undefined 31 | ? disputeRequestBlock 32 | : await getBlockForTimestamp(hubPoolClient.chainId, disputeRequestTimestamp); 33 | 34 | const eventSearchConfig = { from: priceRequestBlock, to: priceRequestBlock }; 35 | const disputes = await paginatedEventQuery(dvm, filter, eventSearchConfig); 36 | const dispute = disputes.find(({ args }) => args.time.toString() === disputeRequestTimestamp.toString()); 37 | return dispute ? spreadEventWithBlockNumber(dispute) : undefined; 38 | } 39 | -------------------------------------------------------------------------------- /src/utils/UniversalUtils.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "ethers"; 2 | import { BigNumber, Signer, Provider } from "."; 3 | import { CONTRACT_ADDRESSES } from "../common"; 4 | 5 | /** 6 | * Calculates the storage slot in the HubPoolStore contract for a given nonce. 7 | * This assumes the data is stored in a mapping at slot 0, keyed by nonce. 8 | * storage_slot = keccak256(h(k) . h(p)) where k = nonce, p = mapping slot position (0) 9 | */ 10 | export function calculateHubPoolStoreStorageSlot(nonce: BigNumber): string { 11 | const mappingSlotPosition = 0; // The relayMessageCallData mapping is at slot 0 12 | 13 | // Ensure nonce and slot position are correctly padded to 32 bytes (64 hex chars + 0x prefix) 14 | const paddedNonce = ethers.utils.hexZeroPad(nonce.toHexString(), 32); 15 | const paddedSlot = ethers.utils.hexZeroPad(BigNumber.from(mappingSlotPosition).toHexString(), 32); 16 | 17 | // Concatenate the padded key (nonce) and slot position 18 | // ethers.utils.concat expects Uint8Array or hex string inputs 19 | const concatenated = ethers.utils.concat([paddedNonce, paddedSlot]); 20 | 21 | // Calculate the Keccak256 hash 22 | const storageSlot = ethers.utils.keccak256(concatenated); 23 | 24 | return storageSlot; 25 | } 26 | 27 | /** 28 | * Retrieves an ethers.Contract instance for the HubPoolStore contract on the specified chain. 29 | * @throws {Error} If the HubPoolStore contract address or ABI is not found for the given chainId in CONTRACT_ADDRESSES. 30 | */ 31 | export function getHubPoolStoreContract(chainId: number, signerOrProvider: Signer | Provider): ethers.Contract { 32 | const hubPoolStoreInfo = CONTRACT_ADDRESSES[chainId]?.hubPoolStore; 33 | if (!hubPoolStoreInfo?.address || !hubPoolStoreInfo.abi) { 34 | throw new Error(`HubPoolStore contract address or ABI not found for chain ${chainId}.`); 35 | } 36 | 37 | return new ethers.Contract(hubPoolStoreInfo.address, hubPoolStoreInfo.abi as any, signerOrProvider); 38 | } 39 | -------------------------------------------------------------------------------- /src/utils/ZkApiUtils.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, ethers } from "."; 2 | import { ApiProofRequest, PROOF_OUTPUTS_ABI_TUPLE, ProofOutputs } from "../interfaces/ZkApi"; 3 | 4 | /** 5 | * Calculates the deterministic Proof ID based on the request parameters. 6 | * Matches the Rust implementation using RLP encoding and Keccak256. 7 | */ 8 | export function calculateProofId(request: ApiProofRequest): string { 9 | // RLP spec: integer values should be minimal big-endian; zero is empty string 10 | const blockNumberHex = BigNumber.from(request.src_chain_block_number).isZero() 11 | ? "0x" 12 | : BigNumber.from(request.src_chain_block_number).toHexString(); 13 | const headHex = BigNumber.from(request.dst_chain_contract_from_head).isZero() 14 | ? "0x" 15 | : BigNumber.from(request.dst_chain_contract_from_head).toHexString(); 16 | const encoded = ethers.utils.RLP.encode([ 17 | request.src_chain_contract_address, 18 | request.src_chain_storage_slots, 19 | blockNumberHex, 20 | headHex, 21 | request.dst_chain_contract_from_header, 22 | ]); 23 | return ethers.utils.keccak256(encoded); 24 | } 25 | 26 | /** 27 | * Decodes the ABI-encoded public_values string from the ZK Proof API into a structured ProofOutputs object. 28 | * @param publicValuesBytes The ABI-encoded hex string (with or without 0x prefix) containing the proof outputs. 29 | * @returns The decoded ProofOutputs object. 30 | * @throws {Error} If the decoding fails (e.g., invalid format). 31 | */ 32 | export function decodeProofOutputs(publicValuesBytes: string): ProofOutputs { 33 | // Ensure 0x prefix for decoder 34 | const prefixedBytes = publicValuesBytes.startsWith("0x") ? publicValuesBytes : "0x" + publicValuesBytes; 35 | const decodedResult = ethers.utils.defaultAbiCoder.decode([PROOF_OUTPUTS_ABI_TUPLE], prefixedBytes)[0]; 36 | 37 | // Map the decoded array elements to the ProofOutputs type properties 38 | // @dev Notice, if `decodedResult` is not what we expect, this will implicitly throw an error. 39 | return { 40 | executionStateRoot: decodedResult[0], 41 | newHeader: decodedResult[1], 42 | nextSyncCommitteeHash: decodedResult[2], 43 | newHead: decodedResult[3], // Already a BigNumber from decoder 44 | prevHeader: decodedResult[4], 45 | prevHead: decodedResult[5], // Already a BigNumber from decoder 46 | syncCommitteeHash: decodedResult[6], 47 | startSyncCommitteeHash: decodedResult[7], 48 | slots: decodedResult[8].map((slot: any[]) => ({ 49 | key: slot[0], 50 | value: slot[1], 51 | contractAddress: slot[2], 52 | })), 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /src/utils/chains/index.ts: -------------------------------------------------------------------------------- 1 | export * as zkSync from "./zkSync"; 2 | -------------------------------------------------------------------------------- /src/utils/chains/zkSync.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import { providers as ethersProviders } from "ethers"; 3 | import { Provider as ZKSyncProvider } from "zksync-ethers"; 4 | import { RetryProvider } from "../ProviderUtils"; 5 | import { isDefined } from "../TypeGuards"; 6 | 7 | /** 8 | * Converts a valid Ethers Provider into a ZKSync Provider 9 | * @param ethersProvider The Ethers provider that we wish to convert 10 | * @returns A ZKSync Provider 11 | * @throws If the provider is not a valid JsonRpcProvider 12 | */ 13 | export function convertEthersRPCToZKSyncRPC(ethersProvider: ethersProviders.Provider): ZKSyncProvider { 14 | const url = (ethersProvider as RetryProvider).providers[0].connection.url; 15 | assert(isDefined(url), "Provider must be of type RetryProvider"); 16 | return new ZKSyncProvider(url); 17 | } 18 | -------------------------------------------------------------------------------- /src/utils/fsUtils.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs/promises"; 2 | import { readFileSync as _readFileSync } from "node:fs"; 3 | import { typeguards } from "@across-protocol/sdk"; 4 | 5 | export function readFileSync(fileName: string): string { 6 | try { 7 | return _readFileSync(fileName, { encoding: "utf8" }); 8 | } catch (err) { 9 | // @dev fs methods can return errors that are not Error objects (i.e. errno). 10 | const msg = typeguards.isError(err) ? err.message : (err as Record)?.code; 11 | throw new Error(`Unable to read ${fileName} (${msg ?? "unknown error"})`); 12 | } 13 | } 14 | 15 | export async function readFile(fileName: string): Promise { 16 | try { 17 | return await fs.readFile(fileName, { encoding: "utf8" }); 18 | } catch (err) { 19 | // @dev fs methods can return errors that are not Error objects (i.e. errno). 20 | const msg = typeguards.isError(err) ? err.message : (err as Record)?.code; 21 | throw new Error(`Unable to read ${fileName} (${msg ?? "unknown error"})`); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | // Utils from other packages. 2 | import { constants as sdkConstants } from "@across-protocol/sdk"; 3 | import { constants as ethersConstants } from "ethers"; 4 | 5 | import winston from "winston"; 6 | import assert from "assert"; 7 | export { winston, assert }; 8 | 9 | export const { MAX_SAFE_ALLOWANCE, ZERO_BYTES, DEFAULT_SIMULATED_RELAYER_ADDRESS_SVM } = sdkConstants; 10 | export const { AddressZero: ZERO_ADDRESS, MaxUint256: MAX_UINT_VAL } = ethersConstants; 11 | 12 | export { 13 | ethers, 14 | providers, 15 | utils, 16 | BaseContract, 17 | Contract, 18 | ContractFactory, 19 | EventFilter, 20 | Signer, 21 | Transaction, 22 | Wallet, 23 | } from "ethers"; 24 | export type { Block, TransactionResponse, TransactionReceipt, Provider } from "@ethersproject/abstract-provider"; 25 | 26 | export { config } from "dotenv"; 27 | 28 | export { replaceAddressCase } from "@uma/common"; 29 | export { Logger, waitForLogger } from "@uma/logger"; 30 | 31 | export { 32 | CHAIN_IDs, 33 | TESTNET_CHAIN_IDs, 34 | TOKEN_SYMBOLS_MAP, 35 | TOKEN_EQUIVALENCE_REMAPPING, 36 | } from "@across-protocol/constants"; 37 | 38 | // TypeChain exports used in the bot. 39 | export { 40 | getContractInfoFromAddress, 41 | getDeployedAddress, 42 | getDeployedBlockNumber, 43 | ExpandedERC20__factory as ERC20, 44 | HubPool__factory as HubPool, 45 | SpokePool__factory as SpokePool, 46 | AcrossConfigStore__factory as AcrossConfigStore, 47 | PolygonTokenBridger__factory as PolygonTokenBridger, 48 | WETH9__factory as WETH9, 49 | } from "@across-protocol/contracts"; 50 | 51 | // Utils specifically for this bot. 52 | export * from "./AnchorUtils"; 53 | export * from "./SDKUtils"; 54 | export * from "./chains"; 55 | export * from "./fsUtils"; 56 | export * from "./ProviderUtils"; 57 | export * from "./SignerUtils"; 58 | export * from "./SvmSignerUtils"; 59 | export * from "./BlockUtils"; 60 | export * from "./EventUtils"; 61 | export * from "./FillUtils"; 62 | export * from "./ObjectUtils"; 63 | export * from "./ContractUtils"; 64 | export * from "./ExecutionUtils"; 65 | export * from "./NetworkUtils"; 66 | export * from "./TransactionUtils"; 67 | export * from "./MerkleTreeUtils"; 68 | export * from "./AddressUtils"; 69 | export * from "./GckmsUtils"; 70 | export * from "./TypeGuards"; 71 | export * from "./Help"; 72 | export * from "./LogUtils"; 73 | export * from "./TypeUtils"; 74 | export * from "./RedisUtils"; 75 | export * from "./UmaUtils"; 76 | export * from "./TokenUtils"; 77 | export * from "./CLIUtils"; 78 | export * from "./BNUtils"; 79 | export * from "./CCTPUtils"; 80 | export * from "./RetryUtils"; 81 | export * from "./BinanceUtils"; 82 | -------------------------------------------------------------------------------- /tasks/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./integration-tests"; 2 | -------------------------------------------------------------------------------- /test/Dataworker.customSpokePoolClients.ts: -------------------------------------------------------------------------------- 1 | import { MultiCallerClient, SpokePoolClient } from "../src/clients"; 2 | import { MAX_UINT_VAL } from "../src/utils"; 3 | import { CHAIN_ID_TEST_LIST } from "./constants"; 4 | import { setupFastDataworker } from "./fixtures/Dataworker.Fixture"; 5 | import { 6 | Contract, 7 | ethers, 8 | expect, 9 | lastSpyLogIncludes, 10 | lastSpyLogLevel, 11 | sinon, 12 | spyLogIncludes, 13 | spyLogLevel, 14 | utf8ToHex, 15 | } from "./utils"; 16 | 17 | // Tested 18 | import { Dataworker } from "../src/dataworker/Dataworker"; 19 | 20 | let spy: sinon.SinonSpy; 21 | let l1Token_1: Contract, hubPool: Contract; 22 | 23 | let dataworkerInstance: Dataworker, multiCallerClient: MultiCallerClient; 24 | let spokePoolClients: { [chainId: number]: SpokePoolClient }; 25 | 26 | let updateAllClients: () => Promise; 27 | 28 | describe("Dataworker: Using SpokePool clients with short lookback windows", async function () { 29 | beforeEach(async function () { 30 | ({ hubPool, l1Token_1, dataworkerInstance, spokePoolClients, multiCallerClient, updateAllClients, spy } = 31 | await setupFastDataworker( 32 | ethers, 33 | 1 // Use very short lookback 34 | )); 35 | }); 36 | 37 | it("Cannot propose", async function () { 38 | await updateAllClients(); 39 | 40 | // Attempting to propose a root fails because bundle block range has a start blocks < spoke clients fromBlock's 41 | // (because lookback is set to low) 42 | await dataworkerInstance.proposeRootBundle( 43 | spokePoolClients, 44 | undefined, 45 | true, 46 | Object.fromEntries(Object.keys(spokePoolClients).map((chainId) => [chainId, Number.MAX_SAFE_INTEGER])) 47 | ); 48 | expect(lastSpyLogIncludes(spy, "Cannot propose bundle with insufficient event data")).to.be.true; 49 | expect(lastSpyLogLevel(spy)).to.equal("warn"); 50 | expect(multiCallerClient.transactionCount()).to.equal(0); 51 | }); 52 | it("Cannot validate", async function () { 53 | await updateAllClients(); 54 | 55 | // propose arbitrary bundle 56 | await l1Token_1.approve(hubPool.address, MAX_UINT_VAL); 57 | await hubPool.proposeRootBundle( 58 | Array(CHAIN_ID_TEST_LIST.length).fill(await hubPool.provider.getBlockNumber()), 59 | 1, 60 | utf8ToHex("BogusRoot"), 61 | utf8ToHex("BogusRoot"), 62 | utf8ToHex("BogusRoot") 63 | ); 64 | await updateAllClients(); 65 | await dataworkerInstance.validatePendingRootBundle( 66 | spokePoolClients, 67 | true, 68 | Object.fromEntries(Object.keys(spokePoolClients).map((chainId) => [chainId, Number.MAX_SAFE_INTEGER])) 69 | ); 70 | expect(lastSpyLogIncludes(spy, "Skipping dispute")).to.be.true; 71 | expect(spyLogLevel(spy, -1)).to.equal("error"); 72 | expect(spyLogIncludes(spy, -2, "Cannot validate bundle with insufficient event data")).to.be.true; 73 | expect(spyLogLevel(spy, -2)).to.equal("warn"); 74 | expect(multiCallerClient.transactionCount()).to.equal(0); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /test/EventUtils.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "./utils"; 2 | 3 | // Tested 4 | import { getPaginatedBlockRanges, getUniqueLogIndex } from "../src/utils/EventUtils"; 5 | 6 | describe("EventUtils", async function () { 7 | it("getPaginatedBlockRanges", async function () { 8 | // Undefined lookback returns full range 9 | expect(getPaginatedBlockRanges({ from: 0, to: 4, maxLookBack: undefined })).to.deep.equal([[0, 4]]); 10 | 11 | // zero lookback throws error 12 | expect(() => getPaginatedBlockRanges({ from: 0, to: 4, maxLookBack: 0 })).to.throw(); 13 | 14 | // to > from returns an empty array. 15 | expect(getPaginatedBlockRanges({ from: 5, to: 4, maxLookBack: 2 })).to.deep.equal([]); 16 | 17 | // Lookback larger than range returns full range 18 | expect(getPaginatedBlockRanges({ from: 0, to: 4, maxLookBack: 6 })).to.deep.equal([[0, 4]]); 19 | 20 | // Range of 0 returns a range that covers both the from and to as expected. 21 | expect(getPaginatedBlockRanges({ from: 1, to: 1, maxLookBack: 3 })).to.deep.equal([[0, 1]]); 22 | 23 | // Range of 0 returns a range that covers both the from and to as expected. 24 | expect(getPaginatedBlockRanges({ from: 3, to: 3, maxLookBack: 3 })).to.deep.equal([[3, 3]]); 25 | 26 | // Lookback of 1 27 | expect(getPaginatedBlockRanges({ from: 0, to: 4, maxLookBack: 2 })).to.deep.equal([ 28 | [0, 1], 29 | [2, 3], 30 | [4, 4], 31 | ]); 32 | 33 | // Lookback equal to range returns full range 34 | expect(getPaginatedBlockRanges({ from: 0, to: 4, maxLookBack: 5 })).to.deep.equal([[0, 4]]); 35 | 36 | // Range evenly divided by max block lookback: 37 | expect(getPaginatedBlockRanges({ from: 0, to: 100, maxLookBack: 50 })).to.deep.equal([ 38 | [0, 49], 39 | [50, 99], 40 | [100, 100], 41 | ]); 42 | 43 | // Range divided by max block lookback with remainder: 44 | expect(getPaginatedBlockRanges({ from: 0, to: 100, maxLookBack: 30 })).to.deep.equal([ 45 | [0, 29], 46 | [30, 59], 47 | [60, 89], 48 | [90, 100], 49 | ]); 50 | 51 | // Range divided by max block lookback with remainder: 52 | expect(getPaginatedBlockRanges({ from: 172, to: 200, maxLookBack: 12 })).to.deep.equal([ 53 | [168, 179], 54 | [180, 191], 55 | [192, 200], 56 | ]); 57 | 58 | // Consistent range (for caching purposes) for many sub-ranges 59 | for (let i = 0; i < 1000; i++) { 60 | const start = Math.floor(Math.random() * 100 + 100); 61 | expect(getPaginatedBlockRanges({ from: start, to: 199, maxLookBack: 100 })).to.deep.equal([[100, 199]]); 62 | } 63 | }); 64 | it("getUniqueLogIndex", async function () { 65 | const events = [ 66 | { 67 | txnRef: "0x1", 68 | }, 69 | { 70 | txnRef: "0x1", 71 | }, 72 | { 73 | txnRef: "0x2", 74 | }, 75 | { 76 | txnRef: "0x3", 77 | }, 78 | ]; 79 | expect(getUniqueLogIndex(events)).to.deep.equal([0, 1, 0, 0]); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /test/RetryUtils.ts: -------------------------------------------------------------------------------- 1 | import { assertPromiseError, expect } from "./utils"; 2 | 3 | // Tested 4 | import { delay, retryAsync } from "../src/utils"; 5 | 6 | const expectedErrorMsg = "async error"; 7 | const expectedReturnValue = 1; 8 | 9 | let ERROR_COUNTER = 0; 10 | 11 | // This function will throw based on the value of an external counter variable, this way 12 | // we can use it to test a function that fails intermittently. If `errorIndex` is > 13 | // to ERROR_COUNTER, then `expectedErrorMsg` will be thrown. 14 | async function incrementCounterThrowError(errorIndex: number, returnValue = expectedReturnValue): Promise { 15 | const oldCounter = ERROR_COUNTER; 16 | ERROR_COUNTER++; 17 | await delay(0); 18 | if (errorIndex > oldCounter) { 19 | throw new Error(expectedErrorMsg); 20 | } else { 21 | return Promise.resolve(returnValue); 22 | } 23 | } 24 | 25 | describe("RetryUtils", async function () { 26 | beforeEach(async function () { 27 | ERROR_COUNTER = 0; 28 | }); 29 | it("retryAsync", async function () { 30 | // Succeeds first time, runs one loop. 31 | expect(await retryAsync(() => incrementCounterThrowError(ERROR_COUNTER), 3, 1)).to.equal(expectedReturnValue); 32 | expect(ERROR_COUNTER).to.equal(1); 33 | 34 | // Fails every time, should throw error, runs four loops, one for first try, and then three retries. 35 | await assertPromiseError( 36 | retryAsync(() => incrementCounterThrowError(ERROR_COUNTER + 1), 3, 1), 37 | expectedErrorMsg 38 | ); 39 | expect(ERROR_COUNTER).to.equal(5); 40 | 41 | // Fails first time only, runs two loops, one for first failure, and then retries 42 | // successfully: 43 | const errorCounter = ERROR_COUNTER; 44 | expect(await retryAsync(() => incrementCounterThrowError(errorCounter + 1), 3, 1)).to.equal(expectedReturnValue); 45 | expect(ERROR_COUNTER).to.equal(7); 46 | 47 | // Delays 1s per retry, retries three times for four more iterations. 48 | const timerStart = performance.now(); 49 | await assertPromiseError( 50 | retryAsync(() => incrementCounterThrowError(ERROR_COUNTER + 1), 3, 1), 51 | expectedErrorMsg 52 | ); 53 | expect(ERROR_COUNTER).to.equal(11); 54 | expect(performance.now() - timerStart).to.be.greaterThan(3000); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /test/constants.ts: -------------------------------------------------------------------------------- 1 | import { 2 | amountToDeposit, 3 | amountToLp, 4 | amountToRelay, 5 | depositRelayerFeePct, 6 | originChainId, 7 | destinationChainId, 8 | mockTreeRoot, 9 | repaymentChainId, 10 | refundProposalLiveness, 11 | randomAddress, 12 | } from "@across-protocol/contracts/dist/test-utils"; 13 | import { bnOne, bnUint256Max, toWei, ZERO_ADDRESS } from "../src/utils"; 14 | 15 | export { 16 | amountToDeposit, 17 | amountToLp, 18 | amountToRelay, 19 | depositRelayerFeePct, 20 | originChainId, 21 | destinationChainId, 22 | mockTreeRoot, 23 | repaymentChainId, 24 | refundProposalLiveness, 25 | ZERO_ADDRESS, 26 | }; 27 | export { CONFIG_STORE_VERSION } from "../src/common"; 28 | 29 | export const randomL1Token = randomAddress(); 30 | export const randomOriginToken = randomAddress(); 31 | export const randomDestinationToken = randomAddress(); 32 | export const randomDestinationToken2 = randomAddress(); 33 | 34 | // This lookback of 24 hours should be enough to cover all Deposit events in the test cases. 35 | export const DEFAULT_UNFILLED_DEPOSIT_LOOKBACK = 1 * 24 * 60 * 60; 36 | 37 | // Max number of refunds in relayer refund leaf for a { repaymentChainId, L2TokenAddress }. 38 | export const MAX_REFUNDS_PER_RELAYER_REFUND_LEAF = 3; 39 | 40 | // Max number of L1 tokens for a chain ID in a pool rebalance leaf. 41 | export const MAX_L1_TOKENS_PER_POOL_REBALANCE_LEAF = 3; 42 | 43 | export const BUNDLE_END_BLOCK_BUFFER = 5; 44 | 45 | // DAI's Rate model. 46 | export const sampleRateModel = { 47 | UBar: toWei(0.8).toString(), 48 | R0: toWei(0.04).toString(), 49 | R1: toWei(0.07).toString(), 50 | R2: toWei(0.75).toString(), 51 | }; 52 | 53 | export const defaultTokenConfig = JSON.stringify({ 54 | rateModel: sampleRateModel, 55 | }); 56 | 57 | // Add Mainnet chain ID 1 to the chain ID list because the dataworker uses this chain to look up latest GlobalConfig 58 | // updates for config variables like MAX_REFUND_COUNT_FOR_RELAYER_REPAYMENT_LEAF. 59 | export const CHAIN_ID_TEST_LIST = [originChainId, destinationChainId, repaymentChainId, 1]; 60 | export const DEFAULT_BLOCK_RANGE_FOR_CHAIN = [ 61 | // For each chain ID in above list, default range is set super high so as to contain all events in a test 62 | // in the straightforward test cases. 63 | [0, 1_000_000], 64 | [0, 1_000_000], 65 | [0, 1_000_000], 66 | [0, 1_000_000], 67 | ]; 68 | 69 | export const IMPOSSIBLE_BLOCK_RANGE = DEFAULT_BLOCK_RANGE_FOR_CHAIN.map((range) => [range[1], range[1]]); 70 | 71 | export const baseSpeedUpString = "ACROSS-V2-FEE-1.0"; 72 | 73 | export const defaultMinDepositConfirmations = { 74 | [originChainId]: [ 75 | { usdThreshold: bnUint256Max.sub(bnOne), minConfirmations: 0 }, 76 | { usdThreshold: bnUint256Max, minConfirmations: Number.MAX_SAFE_INTEGER }, 77 | ], 78 | [destinationChainId]: [ 79 | { usdThreshold: bnUint256Max.sub(bnOne), minConfirmations: 0 }, 80 | { usdThreshold: bnUint256Max, minConfirmations: Number.MAX_SAFE_INTEGER }, 81 | ], 82 | }; 83 | -------------------------------------------------------------------------------- /test/fixtures/UmaEcosystemFixture.ts: -------------------------------------------------------------------------------- 1 | import * as utils from "@across-protocol/contracts/dist/test-utils"; 2 | import { Contract } from "ethers"; 3 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; 4 | import { utf8ToHex, identifier, refundProposalLiveness } from "@across-protocol/contracts/dist/test-utils"; 5 | 6 | export async function setupUmaEcosystem(owner: SignerWithAddress): Promise<{ 7 | timer: Contract; 8 | finder: Contract; 9 | collateralWhitelist: Contract; 10 | store: Contract; 11 | }> { 12 | // Setup minimum UMA ecosystem contracts. Note that we don't use the umaEcosystemFixture because Hardhat Fixture's 13 | // seem to produce non-deterministic behavior between tests. 14 | const timer = await (await utils.getContractFactory("Timer", owner)).deploy(); 15 | const finder = await (await utils.getContractFactory("Finder", owner)).deploy(); 16 | const identifierWhitelist = await (await utils.getContractFactory("IdentifierWhitelist", owner)).deploy(); 17 | const mockOracle = await ( 18 | await utils.getContractFactory("MockOracleAncillary", owner) 19 | ).deploy(finder.address, timer.address); 20 | const optimisticOracle = await ( 21 | await utils.getContractFactory("SkinnyOptimisticOracle", owner) 22 | ).deploy(refundProposalLiveness, finder.address, timer.address); 23 | const collateralWhitelist = await (await utils.getContractFactory("AddressWhitelist", owner)).deploy(); 24 | const store = await ( 25 | await utils.getContractFactory("Store", owner) 26 | ).deploy({ rawValue: "0" }, { rawValue: "0" }, timer.address); 27 | await finder.changeImplementationAddress(utf8ToHex("CollateralWhitelist"), collateralWhitelist.address); 28 | await finder.changeImplementationAddress(utf8ToHex("IdentifierWhitelist"), identifierWhitelist.address); 29 | await finder.changeImplementationAddress(utf8ToHex("SkinnyOptimisticOracle"), optimisticOracle.address); 30 | await finder.changeImplementationAddress(utf8ToHex("Store"), store.address); 31 | await finder.changeImplementationAddress(utf8ToHex("Oracle"), mockOracle.address); 32 | await identifierWhitelist.addSupportedIdentifier(identifier); 33 | return { 34 | timer, 35 | finder, 36 | collateralWhitelist, 37 | store, 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /test/mocks/MockArweaveClient.ts: -------------------------------------------------------------------------------- 1 | import { caching } from "@across-protocol/sdk"; 2 | 3 | export class MockArweaveClient extends caching.ArweaveClient { 4 | // Map from arweave key to JSON object stored as a string. 5 | protected cache: { [key: string]: string } = {}; 6 | 7 | constructor(arweaveJWT: string, logger: winston.logger, gatewayURL = "", protocol = "https", port = 443) { 8 | super(arweaveJWT, logger, gatewayURL, protocol, port); 9 | } 10 | 11 | async getByTopic( 12 | tag: string, 13 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 14 | _validator?: Struct, 15 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 16 | _originQueryAddress?: string 17 | ): Promise<{ data: T; hash: string }[]> { 18 | return Promise.resolve(JSON.parse(this.cache[tag])); 19 | } 20 | 21 | _setCache(key: string, object: T) { 22 | this.cache[key] = JSON.stringify(object); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/mocks/MockBaseChainAdapter.ts: -------------------------------------------------------------------------------- 1 | import { BaseChainAdapter } from "../../src/adapter"; 2 | import { EventSearchConfig, MakeOptional, winston } from "../../src/utils"; 3 | 4 | export class MockBaseChainAdapter extends BaseChainAdapter { 5 | constructor() { 6 | super( 7 | {}, 8 | 0, 9 | 0, 10 | [], 11 | winston.createLogger({ 12 | level: "debug", 13 | transports: [new winston.transports.Console()], 14 | }), 15 | [], 16 | {}, 17 | {}, 18 | 0 19 | ); 20 | } 21 | getSearchConfig(): MakeOptional { 22 | return { from: 0 }; 23 | } 24 | isSupportedL2Bridge(): boolean { 25 | return true; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/mocks/MockBundleDataClient.ts: -------------------------------------------------------------------------------- 1 | import { BundleDataClient, SpokePoolClient } from "../../src/clients"; 2 | import { CombinedRefunds } from "../../src/dataworker/DataworkerUtils"; 3 | import { DepositWithBlock, FillWithBlock } from "../../src/interfaces"; 4 | import { getRelayEventKey } from "../../src/utils"; 5 | 6 | export class MockBundleDataClient extends BundleDataClient { 7 | private pendingBundleRefunds: CombinedRefunds = {}; 8 | private nextBundleRefunds: CombinedRefunds = {}; 9 | private matchingFillEvents: Record = {}; 10 | 11 | async getPendingRefundsFromValidBundles(): Promise { 12 | return [this.pendingBundleRefunds]; 13 | } 14 | 15 | async getNextBundleRefunds(): Promise { 16 | return [this.nextBundleRefunds]; 17 | } 18 | 19 | setReturnedPendingBundleRefunds(refunds: CombinedRefunds): void { 20 | this.pendingBundleRefunds = refunds; 21 | } 22 | 23 | setReturnedNextBundleRefunds(refunds: CombinedRefunds): void { 24 | this.nextBundleRefunds = refunds; 25 | } 26 | 27 | getPersistedPendingRefundsFromLastValidBundle(): Promise { 28 | return Promise.resolve(undefined); 29 | } 30 | 31 | getPersistedNextBundleRefunds(): Promise { 32 | return Promise.resolve(undefined); 33 | } 34 | 35 | setMatchingFillEvent(deposit: DepositWithBlock, fill: FillWithBlock): void { 36 | const relayDataHash = getRelayEventKey(deposit); 37 | this.matchingFillEvents[relayDataHash] = fill; 38 | } 39 | 40 | findMatchingFillEvent( 41 | deposit: DepositWithBlock, 42 | spokePoolClient: SpokePoolClient 43 | ): Promise { 44 | const relayDataHash = getRelayEventKey(deposit); 45 | return this.matchingFillEvents[relayDataHash] 46 | ? Promise.resolve(this.matchingFillEvents[relayDataHash]) 47 | : super.findMatchingFillEvent(deposit, spokePoolClient); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/mocks/MockConfigStoreClient.ts: -------------------------------------------------------------------------------- 1 | import { clients } from "@across-protocol/sdk"; 2 | import { EventSearchConfig, MakeOptional, winston } from "../../src/utils"; 3 | import { Contract } from "../utils"; 4 | import { CHAIN_ID_TEST_LIST } from "../constants"; 5 | 6 | export const DEFAULT_CONFIG_STORE_VERSION = clients.DEFAULT_CONFIG_STORE_VERSION; 7 | 8 | // @dev This mocked class must re-implement any customisations in the local extended ConfigStoreClient. 9 | export class MockConfigStoreClient extends clients.mocks.MockConfigStoreClient { 10 | constructor( 11 | logger: winston.Logger, 12 | configStore: Contract, 13 | eventSearchConfig: MakeOptional = { from: 0, maxLookBack: 0 }, 14 | configStoreVersion = DEFAULT_CONFIG_STORE_VERSION, 15 | enabledChainIds = CHAIN_ID_TEST_LIST, 16 | chainId = 1, 17 | mockUpdate = false 18 | ) { 19 | super( 20 | logger, 21 | configStore, 22 | eventSearchConfig as EventSearchConfig, 23 | configStoreVersion, 24 | chainId, 25 | mockUpdate, 26 | enabledChainIds 27 | ); 28 | } 29 | 30 | _updateLiteChains(chainIds: number[], blockNumber = 0, blockTimestamp = 0) { 31 | this.liteChainIndicesUpdates = [ 32 | { 33 | ...this.liteChainIndicesUpdates, 34 | value: [...(this.liteChainIndicesUpdates.value ?? []), ...chainIds], 35 | blockNumber, 36 | timestamp: blockTimestamp, 37 | txnIndex: Math.floor(Math.random() * 10), 38 | logIndex: Math.floor(Math.random() * 10), 39 | txnRef: "", 40 | }, 41 | ]; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/mocks/MockCrossChainTransferClient.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, bnZero } from "../../src/utils"; 2 | import { CrossChainTransferClient } from "../../src/clients/bridges"; 3 | export class MockCrossChainTransferClient extends CrossChainTransferClient { 4 | crossChainTransferAmount = bnZero; 5 | constructor() { 6 | super(null, null, null); 7 | } 8 | 9 | setCrossChainTransferAmount(amount: BigNumber): void { 10 | this.crossChainTransferAmount = amount; 11 | } 12 | 13 | getOutstandingCrossChainTransferAmount(): BigNumber { 14 | return this.crossChainTransferAmount; 15 | } 16 | 17 | getOutstandingCrossChainTransferTxs(): string[] { 18 | return []; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/mocks/MockMultiCallerClient.ts: -------------------------------------------------------------------------------- 1 | import { MultiCallerClient, TransactionClient } from "../../src/clients"; 2 | import { Contract, winston } from "../utils"; 3 | 4 | export class MockedMultiCallerClient extends MultiCallerClient { 5 | constructor(logger: winston.Logger, chunkSize: { [chainId: number]: number } = {}, readonly multisend?: Contract) { 6 | super(logger, chunkSize); 7 | this.txnClient = new TransactionClient(logger); 8 | } 9 | 10 | // By default return undefined multisender so dataworker can just fallback to calling Multicaller instead 11 | // of having to deploy a Multisend2 on this network. 12 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 13 | async _getMultisender(_: number): Promise { 14 | return this.multisend; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/mocks/MockSpokePoolClient.ts: -------------------------------------------------------------------------------- 1 | import { clients, interfaces } from "@across-protocol/sdk"; 2 | import { Deposit } from "../../src/interfaces"; 3 | export class MockSpokePoolClient extends clients.mocks.MockSpokePoolClient { 4 | public maxFillDeadlineOverride?: number; 5 | public blockTimestampOverride: Record = {}; 6 | private relayFillStatuses: Record = {}; 7 | 8 | public setMaxFillDeadlineOverride(maxFillDeadlineOverride?: number): void { 9 | this.maxFillDeadlineOverride = maxFillDeadlineOverride; 10 | } 11 | 12 | public async getMaxFillDeadlineInRange(startBlock: number, endBlock: number): Promise { 13 | return this.maxFillDeadlineOverride ?? super.getMaxFillDeadlineInRange(startBlock, endBlock); 14 | } 15 | 16 | public setBlockTimestamp(block: number, timestamp: number): void { 17 | this.blockTimestampOverride[block] = timestamp; 18 | } 19 | 20 | public async getTimeAt(block: number): Promise { 21 | return Promise.resolve(this.blockTimestampOverride[block]) ?? super.getTimeAt(block); 22 | } 23 | 24 | public setRelayFillStatus(deposit: Deposit, fillStatus: interfaces.FillStatus): void { 25 | const relayDataHash = deposit.depositId.toString(); 26 | this.relayFillStatuses[relayDataHash] = fillStatus; 27 | } 28 | public relayFillStatus( 29 | relayData: interfaces.RelayData, 30 | blockTag?: number | "latest", 31 | destinationChainId?: number 32 | ): Promise { 33 | const relayDataHash = relayData.depositId.toString(); 34 | return this.relayFillStatuses[relayDataHash] 35 | ? Promise.resolve(this.relayFillStatuses[relayDataHash]) 36 | : super.relayFillStatus(relayData, blockTag, destinationChainId); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/mocks/MockTokenClient.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, Contract, toBN } from "../../src/utils"; 2 | import { TokenClient } from "../../src/clients"; 3 | import { L1Token } from "../../src/interfaces"; 4 | 5 | export class MockTokenClient extends TokenClient { 6 | public override tokenData: { [chainId: number]: { [token: string]: { balance: BigNumber; allowance: BigNumber } } } = 7 | {}; 8 | public tokenShortfall: { 9 | [chainId: number]: { [token: string]: { deposits: number[]; totalRequirement: BigNumber } }; 10 | } = {}; 11 | 12 | setTokenData(chainId: number, token: string, balance: BigNumber, allowance: BigNumber = toBN(0)): void { 13 | if (!this.tokenData[chainId]) { 14 | this.tokenData[chainId] = {}; 15 | } 16 | this.tokenData[chainId][token] = { balance, allowance }; 17 | } 18 | setTokenShortFallData(chainId: number, token: string, deposits: number[], totalRequirement: BigNumber): void { 19 | if (!this.tokenShortfall[chainId]) { 20 | this.tokenShortfall[chainId] = {}; 21 | } 22 | this.tokenShortfall[chainId][token] = { deposits, totalRequirement }; 23 | } 24 | 25 | getBalance(chainId: number, token: string): BigNumber { 26 | if (!this.tokenData[chainId]) { 27 | return toBN(0); 28 | } 29 | if (!this.tokenData[chainId][token]) { 30 | return toBN(0); 31 | } 32 | return this.tokenData[chainId][token].balance; 33 | } 34 | 35 | getTokensNeededToCoverShortfall(chainId: number, token: string): BigNumber { 36 | if (!this.tokenShortfall[chainId]) { 37 | return toBN(0); 38 | } 39 | if (!this.tokenShortfall[chainId][token]) { 40 | return toBN(0); 41 | } 42 | return this.tokenShortfall[chainId][token].totalRequirement; 43 | } 44 | 45 | decrementLocalBalance(chainId: number, token: string, amount: BigNumber): void { 46 | if (!this.tokenData[chainId]) { 47 | this.tokenData[chainId] = {}; 48 | } 49 | if (!this.tokenData[chainId][token]) { 50 | this.tokenData[chainId][token] = { balance: toBN(0), allowance: toBN(0) }; 51 | } 52 | this.tokenData[chainId][token].balance = this.tokenData[chainId][token].balance.sub(amount); 53 | } 54 | } 55 | 56 | export class SimpleMockTokenClient extends TokenClient { 57 | private tokenContracts: Contract[] | undefined = undefined; 58 | private remoteTokenContracts: { [chainId: number]: Contract[] } | undefined = {}; 59 | 60 | setRemoteTokens(tokens: Contract[], remoteTokenContracts?: { [chainId: number]: Contract[] }): void { 61 | this.tokenContracts = tokens; 62 | if (remoteTokenContracts) { 63 | this.remoteTokenContracts = remoteTokenContracts; 64 | } 65 | } 66 | 67 | resolveRemoteTokens(chainId: number, hubPoolTokens: L1Token[]): Contract[] { 68 | if (this.remoteTokenContracts?.[chainId]) { 69 | return this.remoteTokenContracts[chainId]; 70 | } 71 | if (this.tokenContracts) { 72 | return this.tokenContracts; 73 | } 74 | return super.resolveRemoteTokens(chainId, hubPoolTokens); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /test/mocks/MockTransactionClient.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "ethers"; 2 | import { random } from "lodash"; 3 | import { AugmentedTransaction, TransactionClient } from "../../src/clients"; 4 | import { BigNumber, TransactionResponse, TransactionSimulationResult } from "../../src/utils"; 5 | import { toBNWei, winston } from "../utils"; 6 | 7 | export const txnClientPassResult = "pass"; 8 | 9 | export class MockedTransactionClient extends TransactionClient { 10 | public gasLimit: BigNumber | undefined = undefined; 11 | 12 | constructor(logger: winston.Logger) { 13 | super(logger); 14 | } 15 | 16 | randomGasLimit(): BigNumber { 17 | return toBNWei(random(21_000, 30_000_000).toPrecision(9)); 18 | } 19 | 20 | // Forced failures are appended to any list of transaction arguments. 21 | txnFailureReason(txn: AugmentedTransaction): string { 22 | return txn.args.slice(-1)[0]?.result; 23 | } 24 | 25 | txnFailure(txn: AugmentedTransaction): boolean { 26 | const result = this.txnFailureReason(txn); 27 | return result !== undefined && result !== txnClientPassResult; 28 | } 29 | 30 | protected override async _simulate(txn: AugmentedTransaction): Promise { 31 | const fail = this.txnFailure(txn); 32 | 33 | this.logger.debug({ 34 | at: "MockMultiCallerClient#simulateTxn", 35 | message: `Forcing simulation ${fail ? "failure" : "success"}.`, 36 | txn, 37 | }); 38 | 39 | const gasLimit = this.gasLimit ?? this.randomGasLimit(); 40 | 41 | return { 42 | transaction: { ...txn, gasLimit }, 43 | succeed: !fail, 44 | reason: fail ? this.txnFailureReason(txn) : "", 45 | }; 46 | } 47 | 48 | protected override async _submit( 49 | txn: AugmentedTransaction, 50 | nonce: number | null = null 51 | ): Promise { 52 | if (this.txnFailure(txn)) { 53 | return Promise.reject(this.txnFailureReason(txn)); 54 | } 55 | 56 | const _nonce = nonce ?? 1; 57 | const txnResponse = { 58 | chainId: txn.chainId, 59 | nonce: _nonce, 60 | hash: ethers.utils.id(`Across-v2-${txn.contract.address}-${txn.method}-${_nonce}`), 61 | gasLimit: txn.gasLimit ?? this.randomGasLimit(), 62 | } as TransactionResponse; 63 | 64 | this.logger.debug({ 65 | at: "MockMultiCallerClient#submitTxns", 66 | message: "Transaction submission succeeded!", 67 | txn: txnResponse, 68 | }); 69 | 70 | return txnResponse; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /test/mocks/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./MockConfigStoreClient"; 2 | export * from "./MockHubPoolClient"; 3 | export * from "./MockBundleDataClient"; 4 | export * from "./MockProfitClient"; 5 | export * from "./MockAdapterManager"; 6 | export * from "./MockMultiCallerClient"; 7 | export * from "./MockTokenClient"; 8 | export * from "./MockTransactionClient"; 9 | export * from "./MockInventoryClient"; 10 | export * from "./MockSpokePoolClient"; 11 | export * from "./MockArweaveClient"; 12 | -------------------------------------------------------------------------------- /test/types/index.ts: -------------------------------------------------------------------------------- 1 | import { HardhatEthersHelpers } from "@nomiclabs/hardhat-ethers/types"; 2 | import type { ethers } from "ethers"; 3 | import winston from "winston"; 4 | import * as utils from "@across-protocol/contracts/dist/test-utils"; 5 | import { sinon } from "../utils"; 6 | 7 | export type EthersTestLibrary = typeof ethers & HardhatEthersHelpers; 8 | export type SpyLoggerResult = { 9 | spy: sinon.SinonSpy; 10 | spyLogger: winston.Logger; 11 | }; 12 | 13 | export type SpokePoolDeploymentResult = { 14 | weth: utils.Contract; 15 | erc20: utils.Contract; 16 | spokePool: utils.Contract; 17 | unwhitelistedErc20: utils.Contract; 18 | destErc20: utils.Contract; 19 | deploymentBlock: number; 20 | }; 21 | 22 | export type ContractsV2SlowFillRelayData = { 23 | depositor: string; 24 | recipient: string; 25 | destinationToken: string; 26 | amount: utils.BigNumber; 27 | realizedLpFeePct: utils.BigNumber; 28 | relayerFeePct: utils.BigNumber; 29 | depositId: string; 30 | originChainId: string; 31 | destinationChainId: string; 32 | message: string; 33 | }; 34 | 35 | export type ContractsV2SlowFill = { 36 | relayData: ContractsV2SlowFillRelayData; 37 | payoutAdjustmentPct: utils.BigNumber; 38 | }; 39 | -------------------------------------------------------------------------------- /test/utils/BlockchainUtils.ts: -------------------------------------------------------------------------------- 1 | import hre from "hardhat"; 2 | 3 | /** 4 | * Mines a random number of blocks. 5 | * @param amount Amount of blocks to mine. If not provided, a random number of blocks will be mined. 6 | */ 7 | export async function mineRandomBlocks(amount?: number): Promise { 8 | const randomBlocksToMine = amount ?? Math.ceil(Math.random() * 10); 9 | for (let i = 0; i < randomBlocksToMine; i++) { 10 | await hre.network.provider.send("evm_mine"); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/utils/SpokePoolUtils.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import { Contract, bnZero, spreadEvent, toBytes32 } from "../../src/utils"; 3 | import { 4 | Deposit, 5 | DepositWithBlock, 6 | Fill, 7 | FillType, 8 | SlowFillRequest, 9 | SlowFillRequestWithBlock, 10 | } from "../../src/interfaces"; 11 | import { SignerWithAddress } from "./utils"; 12 | 13 | export function V3FillFromDeposit( 14 | deposit: DepositWithBlock, 15 | relayer: string, 16 | repaymentChainId?: number, 17 | fillType = FillType.FastFill 18 | ): Fill { 19 | const { blockNumber, txnRef, logIndex, txnIndex, quoteTimestamp, ...relayData } = deposit; 20 | const fill: Fill = { 21 | ...relayData, 22 | relayer, 23 | realizedLpFeePct: deposit.realizedLpFeePct ?? bnZero, 24 | repaymentChainId: repaymentChainId ?? deposit.destinationChainId, 25 | relayExecutionInfo: { 26 | updatedRecipient: deposit.updatedRecipient, 27 | updatedMessage: deposit.updatedMessage, 28 | updatedOutputAmount: deposit.updatedOutputAmount, 29 | fillType, 30 | }, 31 | }; 32 | return fill; 33 | } 34 | 35 | export async function requestSlowFill( 36 | spokePool: Contract, 37 | relayer: SignerWithAddress, 38 | deposit: Deposit 39 | ): Promise { 40 | await spokePool 41 | .connect(relayer) 42 | .requestSlowFill([ 43 | toBytes32(deposit.depositor), 44 | toBytes32(deposit.recipient), 45 | toBytes32(deposit.exclusiveRelayer), 46 | toBytes32(deposit.inputToken), 47 | toBytes32(deposit.outputToken), 48 | deposit.inputAmount, 49 | deposit.outputAmount, 50 | deposit.originChainId, 51 | deposit.depositId, 52 | deposit.fillDeadline, 53 | deposit.exclusivityDeadline, 54 | deposit.message, 55 | ]); 56 | const [events, destinationChainId] = await Promise.all([ 57 | spokePool.queryFilter(spokePool.filters.RequestedSlowFill()), 58 | spokePool.chainId(), 59 | ]); 60 | const lastEvent = events.at(-1); 61 | assert(lastEvent); 62 | const requestObject: SlowFillRequestWithBlock = { 63 | ...(spreadEvent(lastEvent.args!) as SlowFillRequest), 64 | destinationChainId, 65 | blockNumber: lastEvent.blockNumber, 66 | txnRef: lastEvent.transactionHash, 67 | logIndex: lastEvent.logIndex, 68 | txnIndex: lastEvent.transactionIndex, 69 | }; 70 | return requestObject; 71 | } 72 | -------------------------------------------------------------------------------- /test/utils/UBAUtils.ts: -------------------------------------------------------------------------------- 1 | import { constants } from "@across-protocol/sdk"; 2 | import { SpokePoolClient } from "../../src/clients"; 3 | 4 | /** 5 | * This is a helper function to generate an array of empty objects that are typed as SpokePoolClients. 6 | * Several locations in our tests require an array of SpokePoolClients, but the actual values of the 7 | * SpokePoolClients are not important. This is because we need at least one spoke pool client for every 8 | * available chain and we don't usually need all of the available spoke clients. 9 | * @param desiredClients An optional object that contains the desired spoke pool clients. If set, this will override/append to the default clients. 10 | * @returns An array of empty objects that are typed as SpokePoolClients 11 | */ 12 | export function generateNoOpSpokePoolClientsForDefaultChainIndices( 13 | desiredClients?: Record 14 | ): Record { 15 | const defaultClients = Object.fromEntries( 16 | constants.PROTOCOL_DEFAULT_CHAIN_ID_INDICES.map((chainId) => [ 17 | chainId, 18 | { isUpdated: true } as unknown as SpokePoolClient, 19 | ]) 20 | ); 21 | return { 22 | ...defaultClients, 23 | ...(desiredClients ?? {}), 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /test/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * as contracts from "@across-protocol/contracts/dist/test-utils"; 2 | export * as uma from "@uma/logger"; 3 | 4 | export * from "./utils"; 5 | export * from "./BlockchainUtils"; 6 | export * from "./SpokePoolUtils"; 7 | 8 | export { smock } from "@defi-wonderland/smock"; 9 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": [".eslintrc.js", "index.ts", "./src", "./scripts", "./test", "./deploy", "./tasks"], 4 | "exclude": ["artifacts", "cache", "dist", "node_modules"], 5 | "compilerOptions": { 6 | "target": "es6", 7 | "module": "commonjs" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "outDir": "./dist", 7 | "declaration": true, 8 | "skipLibCheck": true, 9 | "resolveJsonModule": true, 10 | "strict": true, 11 | "strictNullChecks": false, 12 | "noImplicitAny": false 13 | }, 14 | "include": ["./src", "index.ts", "./scripts"], 15 | "files": ["./hardhat.config.ts"], 16 | "watchOptions": { 17 | "watchFile": "useFsEventsOnParentDirectory", 18 | "watchDirectory": "useFsEvents", 19 | "fallbackPolling": "dynamicPriority", 20 | "synchronousWatchDirectory": true, 21 | "excludeDirectories": ["**/node_modules", "artifacts", "cache", "contracts", "dist"] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": {}, 4 | "include": ["./test", "./src"] 5 | } 6 | --------------------------------------------------------------------------------