diff --git a/.github/workflows/solidity.yml b/.github/workflows/solidity.yml index 9e8c807f31..3d41291c8b 100644 --- a/.github/workflows/solidity.yml +++ b/.github/workflows/solidity.yml @@ -267,7 +267,9 @@ jobs: - name: Run tests and generate gas report working-directory: './packages/${{matrix.package}}' # Run separate set of tests (no fuzzing) to get accurate average gas cost estimates - run: forge test --mc GasBenchmark --gas-report > "../../gas-report-${{ matrix.package }}.ansi" + # Note: we use `npm run` with `--if-present` flag, allows not to define a gas:bench script in every package + # This is not natively supported by yarn yet, see: https://github.com/yarnpkg/yarn/pull/7159 + run: npm run gas:bench --if-present > "../../gas-report-${{ matrix.package }}.ansi" - name: Compare gas reports uses: Rubilmax/foundry-gas-diff@v3.18 diff --git a/contrib/promexporter/internal/gql/dfk/models.gen.go b/contrib/promexporter/internal/gql/dfk/models.gen.go index b632ece3ae..c44b4d63d0 100644 --- a/contrib/promexporter/internal/gql/dfk/models.gen.go +++ b/contrib/promexporter/internal/gql/dfk/models.gen.go @@ -1247,6 +1247,7 @@ type Hero struct { DarkSummoned *bool `json:"darkSummoned,omitempty"` DarkSummonLevels *int64 `json:"darkSummonLevels,omitempty"` HasValidCraftingGenes *bool `json:"hasValidCraftingGenes,omitempty"` + State *int64 `json:"state,omitempty"` MainClassStr string `json:"mainClassStr"` SubClassStr string `json:"subClassStr"` ProfessionStr string `json:"professionStr"` @@ -2333,6 +2334,14 @@ type HeroFilter struct { ProfessionStrNotStartsWith *string `json:"professionStr_not_starts_with,omitempty"` ProfessionStrEndsWith *string `json:"professionStr_ends_with,omitempty"` ProfessionStrNotEndsWith *string `json:"professionStr_not_ends_with,omitempty"` + State *int64 `json:"state,omitempty"` + StateNot *int64 `json:"state_not,omitempty"` + StateGt *int64 `json:"state_gt,omitempty"` + StateLt *int64 `json:"state_lt,omitempty"` + StateGte *int64 `json:"state_gte,omitempty"` + StateLte *int64 `json:"state_lte,omitempty"` + StateIn []*int64 `json:"state_in,omitempty"` + StateNotIn []*int64 `json:"state_not_in,omitempty"` } type Pet struct { @@ -3896,6 +3905,7 @@ const ( HeroOrderBySubClassStr HeroOrderBy = "subClassStr" HeroOrderByProfessionStr HeroOrderBy = "professionStr" HeroOrderByPet HeroOrderBy = "pet" + HeroOrderByState HeroOrderBy = "state" ) var AllHeroOrderBy = []HeroOrderBy{ @@ -4002,11 +4012,12 @@ var AllHeroOrderBy = []HeroOrderBy{ HeroOrderBySubClassStr, HeroOrderByProfessionStr, HeroOrderByPet, + HeroOrderByState, } func (e HeroOrderBy) IsValid() bool { switch e { - case HeroOrderByID, HeroOrderByNumberID, HeroOrderByOwner, HeroOrderByPreviousOwner, HeroOrderByCreator, HeroOrderByStatGenes, HeroOrderByVisualGenes, HeroOrderByRarity, HeroOrderByShiny, HeroOrderByGeneration, HeroOrderByFirstName, HeroOrderByLastName, HeroOrderByShinyStyle, HeroOrderByMainClass, HeroOrderBySubClass, HeroOrderBySummonedTime, HeroOrderByNextSummonTime, HeroOrderBySummonerID, HeroOrderByAssistantID, HeroOrderBySummons, HeroOrderByMaxSummons, HeroOrderByStaminaFullAt, HeroOrderByHpFullAt, HeroOrderByMpFullAt, HeroOrderByLevel, HeroOrderByXp, HeroOrderByCurrentQuest, HeroOrderBySp, HeroOrderByStatus, HeroOrderByStrength, HeroOrderByIntelligence, HeroOrderByWisdom, HeroOrderByLuck, HeroOrderByAgility, HeroOrderByVitality, HeroOrderByEndurance, HeroOrderByDexterity, HeroOrderByHp, HeroOrderByMp, HeroOrderByStamina, HeroOrderByStrengthGrowthP, HeroOrderByIntelligenceGrowthP, HeroOrderByWisdomGrowthP, HeroOrderByLuckGrowthP, HeroOrderByAgilityGrowthP, HeroOrderByVitalityGrowthP, HeroOrderByEnduranceGrowthP, HeroOrderByDexterityGrowthP, HeroOrderByStrengthGrowthS, HeroOrderByIntelligenceGrowthS, HeroOrderByWisdomGrowthS, HeroOrderByLuckGrowthS, HeroOrderByAgilityGrowthS, HeroOrderByVitalityGrowthS, HeroOrderByEnduranceGrowthS, HeroOrderByDexterityGrowthS, HeroOrderByHpSmGrowth, HeroOrderByHpRgGrowth, HeroOrderByHpLgGrowth, HeroOrderByMpSmGrowth, HeroOrderByMpRgGrowth, HeroOrderByMpLgGrowth, HeroOrderByMining, HeroOrderByGardening, HeroOrderByForaging, HeroOrderByFishing, HeroOrderByProfession, HeroOrderByPassive1, HeroOrderByPassive2, HeroOrderByActive1, HeroOrderByActive2, HeroOrderByStatBoost1, HeroOrderByStatBoost2, HeroOrderByStatsUnknown1, HeroOrderByElement, HeroOrderByStatsUnknown2, HeroOrderByGender, HeroOrderByHeadAppendage, HeroOrderByBackAppendage, HeroOrderByBackground, HeroOrderByHairStyle, HeroOrderByHairColor, HeroOrderByVisualUnknown1, HeroOrderByEyeColor, HeroOrderBySkinColor, HeroOrderByAppendageColor, HeroOrderByBackAppendageColor, HeroOrderByVisualUnknown2, HeroOrderByAssistingAuction, HeroOrderByAssistingPrice, HeroOrderBySaleAuction, HeroOrderBySalePrice, HeroOrderByPrivateAuctionProfile, HeroOrderBySummonsRemaining, HeroOrderByPjStatus, HeroOrderByPjLevel, HeroOrderByDarkSummoned, HeroOrderByDarkSummonLevels, HeroOrderByHasValidCraftingGenes, HeroOrderByMainClassStr, HeroOrderBySubClassStr, HeroOrderByProfessionStr, HeroOrderByPet: + case HeroOrderByID, HeroOrderByNumberID, HeroOrderByOwner, HeroOrderByPreviousOwner, HeroOrderByCreator, HeroOrderByStatGenes, HeroOrderByVisualGenes, HeroOrderByRarity, HeroOrderByShiny, HeroOrderByGeneration, HeroOrderByFirstName, HeroOrderByLastName, HeroOrderByShinyStyle, HeroOrderByMainClass, HeroOrderBySubClass, HeroOrderBySummonedTime, HeroOrderByNextSummonTime, HeroOrderBySummonerID, HeroOrderByAssistantID, HeroOrderBySummons, HeroOrderByMaxSummons, HeroOrderByStaminaFullAt, HeroOrderByHpFullAt, HeroOrderByMpFullAt, HeroOrderByLevel, HeroOrderByXp, HeroOrderByCurrentQuest, HeroOrderBySp, HeroOrderByStatus, HeroOrderByStrength, HeroOrderByIntelligence, HeroOrderByWisdom, HeroOrderByLuck, HeroOrderByAgility, HeroOrderByVitality, HeroOrderByEndurance, HeroOrderByDexterity, HeroOrderByHp, HeroOrderByMp, HeroOrderByStamina, HeroOrderByStrengthGrowthP, HeroOrderByIntelligenceGrowthP, HeroOrderByWisdomGrowthP, HeroOrderByLuckGrowthP, HeroOrderByAgilityGrowthP, HeroOrderByVitalityGrowthP, HeroOrderByEnduranceGrowthP, HeroOrderByDexterityGrowthP, HeroOrderByStrengthGrowthS, HeroOrderByIntelligenceGrowthS, HeroOrderByWisdomGrowthS, HeroOrderByLuckGrowthS, HeroOrderByAgilityGrowthS, HeroOrderByVitalityGrowthS, HeroOrderByEnduranceGrowthS, HeroOrderByDexterityGrowthS, HeroOrderByHpSmGrowth, HeroOrderByHpRgGrowth, HeroOrderByHpLgGrowth, HeroOrderByMpSmGrowth, HeroOrderByMpRgGrowth, HeroOrderByMpLgGrowth, HeroOrderByMining, HeroOrderByGardening, HeroOrderByForaging, HeroOrderByFishing, HeroOrderByProfession, HeroOrderByPassive1, HeroOrderByPassive2, HeroOrderByActive1, HeroOrderByActive2, HeroOrderByStatBoost1, HeroOrderByStatBoost2, HeroOrderByStatsUnknown1, HeroOrderByElement, HeroOrderByStatsUnknown2, HeroOrderByGender, HeroOrderByHeadAppendage, HeroOrderByBackAppendage, HeroOrderByBackground, HeroOrderByHairStyle, HeroOrderByHairColor, HeroOrderByVisualUnknown1, HeroOrderByEyeColor, HeroOrderBySkinColor, HeroOrderByAppendageColor, HeroOrderByBackAppendageColor, HeroOrderByVisualUnknown2, HeroOrderByAssistingAuction, HeroOrderByAssistingPrice, HeroOrderBySaleAuction, HeroOrderBySalePrice, HeroOrderByPrivateAuctionProfile, HeroOrderBySummonsRemaining, HeroOrderByPjStatus, HeroOrderByPjLevel, HeroOrderByDarkSummoned, HeroOrderByDarkSummonLevels, HeroOrderByHasValidCraftingGenes, HeroOrderByMainClassStr, HeroOrderBySubClassStr, HeroOrderByProfessionStr, HeroOrderByPet, HeroOrderByState: return true } return false diff --git a/docs/bridge/CHANGELOG.md b/docs/bridge/CHANGELOG.md index bcf598e478..c692d12f9d 100644 --- a/docs/bridge/CHANGELOG.md +++ b/docs/bridge/CHANGELOG.md @@ -3,6 +3,60 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.4.4](https://github.com/synapsecns/sanguine/compare/@synapsecns/bridge-docs@0.4.3...@synapsecns/bridge-docs@0.4.4) (2024-10-15) + +**Note:** Version bump only for package @synapsecns/bridge-docs + + + + + +## [0.4.3](https://github.com/synapsecns/sanguine/compare/@synapsecns/bridge-docs@0.4.2...@synapsecns/bridge-docs@0.4.3) (2024-10-15) + +**Note:** Version bump only for package @synapsecns/bridge-docs + + + + + +## [0.4.2](https://github.com/synapsecns/sanguine/compare/@synapsecns/bridge-docs@0.4.1...@synapsecns/bridge-docs@0.4.2) (2024-10-12) + + +### Bug Fixes + +* **docs:** Fixes gh link ([#3280](https://github.com/synapsecns/sanguine/issues/3280)) ([f1dfc82](https://github.com/synapsecns/sanguine/commit/f1dfc82bc26d60262a92feda671d44a6d54a3ce1)) + + + + + +## [0.4.1](https://github.com/synapsecns/sanguine/compare/@synapsecns/bridge-docs@0.4.0...@synapsecns/bridge-docs@0.4.1) (2024-10-12) + +**Note:** Version bump only for package @synapsecns/bridge-docs + + + + + +# [0.4.0](https://github.com/synapsecns/sanguine/compare/@synapsecns/bridge-docs@0.3.11...@synapsecns/bridge-docs@0.4.0) (2024-10-10) + + +### Features + +* **synapse-constants:** adds preinstall step ([#3269](https://github.com/synapsecns/sanguine/issues/3269)) ([acd61de](https://github.com/synapsecns/sanguine/commit/acd61de4846d9b23d7aa834b8f2eefcaae486c7d)) + + + + + +## [0.3.11](https://github.com/synapsecns/sanguine/compare/@synapsecns/bridge-docs@0.3.10...@synapsecns/bridge-docs@0.3.11) (2024-10-10) + +**Note:** Version bump only for package @synapsecns/bridge-docs + + + + + ## [0.3.10](https://github.com/synapsecns/sanguine/compare/@synapsecns/bridge-docs@0.3.9...@synapsecns/bridge-docs@0.3.10) (2024-10-08) **Note:** Version bump only for package @synapsecns/bridge-docs diff --git a/docs/bridge/README.md b/docs/bridge/README.md index afad5c80f5..73c43b2c0e 100644 --- a/docs/bridge/README.md +++ b/docs/bridge/README.md @@ -5,17 +5,18 @@ This website is built using [Docusaurus](https://docusaurus.io/), a modern stati ## Generating API Docs + `yarn docusaurus gen-api-docs all`. ### Installation -``` +```bash $ yarn ``` ### Local Development -``` +```bash $ yarn start ``` @@ -23,8 +24,16 @@ This command starts a local development server and opens up a browser window. Mo ### Build -``` +```bash $ yarn build ``` This command generates static content into the `build` directory and can be served using any static contents hosting service. + +### Serve + +This step is needed to create a searchable index. + +```bash +$ yarn serve +``` diff --git a/docs/bridge/docs/02-Bridge/01-SDK.md b/docs/bridge/docs/02-Bridge/01-SDK.md index c58d7e3f51..29687f414c 100644 --- a/docs/bridge/docs/02-Bridge/01-SDK.md +++ b/docs/bridge/docs/02-Bridge/01-SDK.md @@ -10,25 +10,25 @@ The Synapse Bridge SDK is built on top of the [Synapse Router](/docs/Routers/Syn ### Use cases -* Integrate your front-end application with the Synapse Bridge. -* Provide bridge liquidity. -* Perform cross-chain arbitrage. -* Integrate the Synapse Javascript SDK with your non-Javascript application. +- Integrate your front-end application with the Synapse Bridge. +- Provide bridge liquidity. +- Perform cross-chain arbitrage. +- Integrate the Synapse Javascript SDK with your non-Javascript application. ## Install :::note requires Node v16+ -The SDK has only been fully tested on Node 16+ or greater. Earlier versions are not guaranteed to work. +The SDK has only been fully tested on Node 16+ or greater. Earlier versions are not guaranteed to work. ::: -Requires either the `npx` or `yarn` package manager. +Requires either the `npm` or `yarn` package manager. | Options |- -| `npx install @synapsecns/sdk-router` -| `yarn install @synapsecns/sdk-router` +| `npm install @synapsecns/sdk-router` +| `yarn add @synapsecns/sdk-router` ## Configure Ethers @@ -60,8 +60,9 @@ const Synapse = new SynapseSDK(chainIds, providers) :::tip Ethers v6 Use of Ethers v6 requires the `@ethersproject/providers` dependency to be installed via `npm` or `yarn`: -* `npm install @ethersproject/providers@^5.7.2` -* `yarn add @ethersproject/providers@^5.7.2` + +- `npm install @ethersproject/providers@^5.7.2` +- `yarn add @ethersproject/providers@^5.7.2` ::: @@ -90,11 +91,11 @@ const Synapse = new SynapseSDK(chainIds, providers) `originQuery` and `destQuery`, returned by `bridgeQuote()` and required for `bridge()`, are [`Query`](https://synapserouter.gitbook.io/untitled/) objects, which contain: -* `swapAdapter`: (string): 0x address of the swap adapter. -* `tokenOut`: (string): 0x address of the outputted token on that chain. -* `minAmountOut`: (Ethers BigNumber): The min amount of value exiting the transaction. -* `deadline`: (Ethers BigNumber): The deadline for the potential transaction. -* `rawParams`: (string): 0x params for the potential transaction. +- `swapAdapter`: (string): 0x address of the swap adapter. +- `tokenOut`: (string): 0x address of the outputted token on that chain. +- `minAmountOut`: (Ethers BigNumber): The min amount of value exiting the transaction. +- `deadline`: (Ethers BigNumber): The deadline for the potential transaction. +- `rawParams`: (string): 0x params for the potential transaction. ::: @@ -106,25 +107,25 @@ Get all relevant information regarding a possible transaction. `bridgeQuote()` requires the following arguments: -* `fromChain` (number): Origin chain id. -* `toChain` (number): Destination chain id. -* `fromToken` (string): 0x token address on the origin chain. -* `toToken` (string): 0x token address on the destination chain. -* `amount` (Ethers BigNumber): The amount (with the correct amount of decimals specified by the token on the origin chain) -* `object` (three seperate args): -* `deadline` (Ethers BigNumber): Deadline for the transaction to be initiated on the origin chain, in seconds (optional) -* `originUserAddress` (string): Address of the user on the origin chain, optional, mandatory if a smart contract is going to initiate the bridge operation -* `excludedModules` (array): (optional) List of bridge modules to exclude from the result +- `fromChain` (number): Origin chain id. +- `toChain` (number): Destination chain id. +- `fromToken` (string): 0x token address on the origin chain. +- `toToken` (string): 0x token address on the destination chain. +- `amount` (Ethers BigNumber): The amount (with the correct amount of decimals specified by the token on the origin chain) +- An `object` with three separate args: + - `deadline` (Ethers BigNumber): Deadline for the transaction to be initiated on the origin chain, in seconds (optional) + - `originUserAddress` (string): Address of the user on the origin chain, optional, mandatory if a smart contract is going to initiate the bridge operation + - `excludedModules` (array): (optional) List of bridge modules to exclude from the result #### Return value `bridgeQuote` returns the following information -* `feeAmount` (Ethers BigNumber): The calculated amount of fee to be taken. -* `bridgeFee` (number): The percentage of fee to be taken. -* `maxAmountOut` (Ethers BigNumber): The maximum output amount resulting from the bridge transaction. -* `originQuery` (`Query`): The query to be executed on the origin chain. -* `destQuery` (`Query`): The query to be executed on the destination chain. +- `feeAmount` (Ethers BigNumber): The calculated amount of fee to be taken. +- `bridgeFee` (number): The percentage of fee to be taken. +- `maxAmountOut` (Ethers BigNumber): The maximum output amount resulting from the bridge transaction. +- `originQuery` (`Query`): The query to be executed on the origin chain. +- `destQuery` (`Query`): The query to be executed on the destination chain. ### `bridge()` @@ -132,19 +133,19 @@ Use `bridgeQuote` to request a Bridge transaction #### Parameters -* `toAddress` (number): The 0x wallet address on the destination chain. -* `routerAddress` (string): The 0x contract address on the origin chain of the bridge router contract. -* `fromChain` (number): The origin chain id. -* `toChain` (number): The destination chain id. -* `fromToken` (string): The 0x token address on the origin chain. -* `amount` (Ethers BigNumber): The amount (with the correct amount of decimals specified by the token on the origin chain) -* `originQuery` (`Query`): The query to be executed on the origin chain. -* `destQuery` (`Query`): The query to be executed on the destination chain. +- `toAddress` (number): The 0x wallet address on the destination chain. +- `routerAddress` (string): The 0x contract address on the origin chain of the bridge router contract. +- `fromChain` (number): The origin chain id. +- `toChain` (number): The destination chain id. +- `fromToken` (string): The 0x token address on the origin chain. +- `amount` (Ethers BigNumber): The amount (with the correct amount of decimals specified by the token on the origin chain) +- `originQuery` (`Query`): The query to be executed on the origin chain. +- `destQuery` (`Query`): The query to be executed on the destination chain. #### Return value -* `to` (string): 0x wallet address on the destination chain. -* `data` (string): Output data in 0x hex format +- `to` (string): 0x wallet address on the destination chain. +- `data` (string): Output data in 0x hex format ### `allBridgeQuotes()` @@ -165,7 +166,7 @@ const quotes = await Synapse.bridgeQuote( 43114, // Destination Chain '0xff970a61a04b1ca14834a43f5de4533ebddb5cc8', // Origin Token Address '0xa7d7079b0fead91f3e65f86e8915cb59c1a4c664', // Destination Token Address - BigNumber.from('20000000') // Amount in + BigNumber.from('20000000') // Amount in { // Deadline for the transaction to be initiated on the origin chain, in seconds (optional) deadline: 1234567890, @@ -183,14 +184,14 @@ const quotes = await Synapse.bridgeQuote( ```js await Synapse.bridge( - '0x0AF91FA049A7e1894F480bFE5bBa20142C6c29a9', // To Address - bridgeQuote.routerAddress, // address of the contract to route the txn - 42161, // Origin Chain - 43114, // Destination Chain - '0xff970a61a04b1ca14834a43f5de4533ebddb5cc8', // Origin Token Address - BigNumber.from('20000000'), // Amount - quote.originQuery, // Origin query from bridgeQuote() - quote.destQuery // Destination query from bridgeQuote() + '0x0AF91FA049A7e1894F480bFE5bBa20142C6c29a9', // To Address + bridgeQuote.routerAddress, // address of the contract to route the txn + 42161, // Origin Chain + 43114, // Destination Chain + '0xff970a61a04b1ca14834a43f5de4533ebddb5cc8', // Origin Token Address + BigNumber.from('20000000'), // Amount + quote.originQuery, // Origin query from bridgeQuote() + quote.destQuery // Destination query from bridgeQuote() ) ``` @@ -198,22 +199,23 @@ await Synapse.bridge( ### Options object -* `deadline`, `excludeCCTP` (now `excludedModules`), and `originUserAddress` parameters are now found in an (optional) options object at the end of the arguments list for `bridgeQuote()`, and `allBridgeQuotes()`. -* `excludedModules` excludes one or more modules with an array of the module names. Supported names are `SynapseBridge`, `SynapseCCTP`, and `SynapseRFQ`. -* `originUserAddress` is required as part of the options object to initiate a bridge transaction on behalf of a user. +- `deadline`, `excludeCCTP` (now `excludedModules`), and `originUserAddress` parameters are now found in an (optional) options object at the end of the arguments list for `bridgeQuote()`, and `allBridgeQuotes()`. +- `excludedModules` excludes one or more modules with an array of the module names. Supported names are `SynapseBridge`, `SynapseCCTP`, and `SynapseRFQ`. +- `originUserAddress` is required as part of the options object to initiate a bridge transaction on behalf of a user. ### Examples + ```js bridgeQuote(...arguments, { deadline: 1234567890, - excludedModules: ["SynapseCCTP"], - originUserAddress: "0x1234...", + excludedModules: ['SynapseCCTP'], + originUserAddress: '0x1234...', }) allBridgeQuotes({ deadline: 1234567890, - excludedModules: ["SynapseCCTP"], - originUserAddress: "0x1234...", + excludedModules: ['SynapseCCTP'], + originUserAddress: '0x1234...', }) ``` diff --git a/docs/bridge/docs/04-Routers/RFQ/index.md b/docs/bridge/docs/04-Routers/RFQ/index.md index b44418cde1..b2a498f803 100644 --- a/docs/bridge/docs/04-Routers/RFQ/index.md +++ b/docs/bridge/docs/04-Routers/RFQ/index.md @@ -75,3 +75,7 @@ In a successful dispute, the relayer loses their claimable funds. This design is ## Unfulfilled requests If a request is not fulfilled, users can reclaim their funds by using the [`claim`](https://vercel-rfq-docs.vercel.app/contracts/FastBridge.sol/contract.FastBridge.html#claim) function once the optimistic window has passed. + +## Load Tester + +The [`rfq-loadtest`](https://github.com/synapsecns/sanguine/tree/master/packages/rfq-loadtest) tool can be used to rapidly send ETH bridges for the purpose of load testing. \ No newline at end of file diff --git a/docs/bridge/docusaurus.config.ts b/docs/bridge/docusaurus.config.ts index cd3586dbac..e7fbff7146 100644 --- a/docs/bridge/docusaurus.config.ts +++ b/docs/bridge/docusaurus.config.ts @@ -55,12 +55,16 @@ const config: Config = { [ 'classic', { + gtag: { + trackingID: 'G-BBC13LQXBD', + anonymizeIP: true, + }, docs: { sidebarPath: './sidebars.ts', // Please change this to your repo. // Remove this to remove the "edit this page" links. editUrl: - 'https://github.com/synapsecns/sanguine/edit/master/docs/bridge/blog-posts/', + 'https://github.com/synapsecns/sanguine/edit/master/docs/bridge/', docRootComponent: '@theme/DocRoot', docItemComponent: '@theme/ApiItem', // derived from docusaurus-theme-openapi-docs // docItemComponent: '@theme/ApiItem', // derived from docusaurus-theme-openapi-docs @@ -90,11 +94,10 @@ const config: Config = { }, // Replace with your project's social card image: 'img/docusaurus-social-card.jpg', - announcementBar: { - id: 'announcementBar-v3.2', // Increment on change - // content: `⭐️ If you like Docusaurus, give it a star on GitHub and follow us on Twitter ${TwitterSvg}`, - content: `New! Synapse Docs have been refreshed. For the previous version, click here`, - }, + // announcementBar: { + // id: 'announcementBar-v3.2', // Increment on change + // content: `⭐️ If you like Docusaurus, give it a star on GitHub and follow us on Twitter ${TwitterSvg}`, + // }, navbar: { title: 'Synapse Docs', logo: { @@ -108,7 +111,7 @@ const config: Config = { position: 'left', label: 'Docs', }, - {to: '/blog', label: 'Blog', position: 'left'}, + { to: '/blog', label: 'Blog', position: 'left' }, { href: 'https://github.com/synapsecns/sanguine', label: 'GitHub', @@ -119,15 +122,6 @@ const config: Config = { footer: { // style: 'dark', links: [ - // { - // title: 'Docs', - // items: [ - // { - // label: 'Tutorial', - // to: '/docs/intro', - // }, - // ], - // }, { title: 'Community', items: [ @@ -150,7 +144,7 @@ const config: Config = { items: [ { label: 'GitHub', - href: 'https://github.com/facebook/docusaurus', + href: 'https://github.com/synapsecns/sanguine', }, ], }, diff --git a/docs/bridge/package.json b/docs/bridge/package.json index cb794cb7de..7b01af0b92 100644 --- a/docs/bridge/package.json +++ b/docs/bridge/package.json @@ -1,6 +1,6 @@ { "name": "@synapsecns/bridge-docs", - "version": "0.3.10", + "version": "0.4.4", "private": true, "scripts": { "docusaurus": "docusaurus", @@ -34,7 +34,7 @@ "@docusaurus/utils-validation": "3.5.2", "@easyops-cn/docusaurus-search-local": "^0.44.5", "@mdx-js/react": "^3.0.0", - "@synapsecns/synapse-constants": "^1.6.0", + "@synapsecns/synapse-constants": "^1.7.0", "clsx": "^2.0.0", "docusaurus-plugin-openapi-docs": "^4.0.1", "docusaurus-theme-openapi-docs": "^4.0.1", diff --git a/ethergo/submitter/export_test.go b/ethergo/submitter/export_test.go index 3eb3d476ce..9160fd7eba 100644 --- a/ethergo/submitter/export_test.go +++ b/ethergo/submitter/export_test.go @@ -12,7 +12,6 @@ import ( "github.com/synapsecns/sanguine/ethergo/signer/signer" "github.com/synapsecns/sanguine/ethergo/submitter/config" "github.com/synapsecns/sanguine/ethergo/submitter/db" - "go.opentelemetry.io/otel/attribute" ) // CopyTransactOpts exports copyTransactOpts for testing. @@ -20,24 +19,6 @@ func CopyTransactOpts(opts *bind.TransactOpts) *bind.TransactOpts { return copyTransactOpts(opts) } -// NullFieldAttribute is a constant used to test the null field attribute. -// it exports the underlying constant for testing. -const NullFieldAttribute = nullFieldAttribute - -func AddressPtrToString(address *common.Address) string { - return addressPtrToString(address) -} - -// BigPtrToString converts a big.Int pointer to a string. -func BigPtrToString(num *big.Int) string { - return bigPtrToString(num) -} - -// TxToAttributes exports txToAttributes for testing. -func TxToAttributes(transaction *types.Transaction, UUID string) []attribute.KeyValue { - return txToAttributes(transaction, UUID) -} - // SortTxes exports sortTxesByChainID for testing. func SortTxes(txs []db.TX, maxPerChain int) map[uint64][]db.TX { return sortTxesByChainID(txs, maxPerChain) @@ -48,31 +29,6 @@ func GroupTxesByNonce(txs []db.TX) map[uint64][]db.TX { return groupTxesByNonce(txs) } -const ( - // HashAttr exports hashAttr for testing. - HashAttr = hashAttr - // FromAttr exports fromAttr for testing. - FromAttr = fromAttr - // ToAttr exports toAttr for testing. - ToAttr = toAttr - // DataAttr exports dataAttr for testing. - DataAttr = dataAttr - // ValueAttr exports valueAttr for testing. - ValueAttr = valueAttr - // NonceAttr exports nonceAttr for testing. - NonceAttr = nonceAttr - // GasLimitAttr exports gasLimitAttr for testing. - GasLimitAttr = gasLimitAttr - // ChainIDAttr exports chainIDAttr for testing. - ChainIDAttr = chainIDAttr - // GasPriceAttr exports gasPriceAttr for testing. - GasPriceAttr = gasPriceAttr - // GasFeeCapAttr exports gasFeeCapAttr for testing. - GasFeeCapAttr = gasFeeCapAttr - // GasTipCapAttr exports gasTipCapAttr for testing. - GasTipCapAttr = gasTipCapAttr -) - // NewTestTransactionSubmitter wraps TestTransactionSubmitter in a TransactionSubmitter interface. func NewTestTransactionSubmitter(metrics metrics.Handler, signer signer.Signer, fetcher ClientFetcher, db db.Service, config *config.Config) TestTransactionSubmitter { txSubmitter := NewTransactionSubmitter(metrics, signer, fetcher, db, config) diff --git a/ethergo/submitter/submitter.go b/ethergo/submitter/submitter.go index 2d0a781688..2eaec50f64 100644 --- a/ethergo/submitter/submitter.go +++ b/ethergo/submitter/submitter.go @@ -463,9 +463,9 @@ func (t *txSubmitterImpl) setGasPrice(ctx context.Context, client client.EVM, span.SetAttributes( attribute.Int(metrics.ChainID, chainID), attribute.Bool("use_dynamic", useDynamic), - attribute.String("gas_price", bigPtrToString(transactor.GasPrice)), - attribute.String("gas_fee_cap", bigPtrToString(transactor.GasFeeCap)), - attribute.String("gas_tip_cap", bigPtrToString(transactor.GasTipCap)), + attribute.String("gas_price", util.BigPtrToString(transactor.GasPrice)), + attribute.String("gas_fee_cap", util.BigPtrToString(transactor.GasFeeCap)), + attribute.String("gas_tip_cap", util.BigPtrToString(transactor.GasTipCap)), ) metrics.EndSpanWithErr(span, err) }() @@ -501,9 +501,9 @@ func (t *txSubmitterImpl) bumpGasFromPrevTx(ctx context.Context, transactor *bin defer func() { span.SetAttributes( - attribute.String("gas_price", bigPtrToString(transactor.GasPrice)), - attribute.String("gas_fee_cap", bigPtrToString(transactor.GasFeeCap)), - attribute.String("gas_tip_cap", bigPtrToString(transactor.GasTipCap)), + attribute.String("gas_price", util.BigPtrToString(transactor.GasPrice)), + attribute.String("gas_fee_cap", util.BigPtrToString(transactor.GasFeeCap)), + attribute.String("gas_tip_cap", util.BigPtrToString(transactor.GasTipCap)), ) metrics.EndSpan(span) }() @@ -537,9 +537,9 @@ func (t *txSubmitterImpl) applyGasFloor(ctx context.Context, transactor *bind.Tr defer func() { span.SetAttributes( - attribute.String("gas_price", bigPtrToString(transactor.GasPrice)), - attribute.String("gas_fee_cap", bigPtrToString(transactor.GasFeeCap)), - attribute.String("gas_tip_cap", bigPtrToString(transactor.GasTipCap)), + attribute.String("gas_price", util.BigPtrToString(transactor.GasPrice)), + attribute.String("gas_fee_cap", util.BigPtrToString(transactor.GasFeeCap)), + attribute.String("gas_tip_cap", util.BigPtrToString(transactor.GasTipCap)), ) metrics.EndSpan(span) }() @@ -578,10 +578,10 @@ func (t *txSubmitterImpl) applyGasFromOracle(ctx context.Context, transactor *bi } transactor.GasTipCap = maxOfBig(transactor.GasTipCap, suggestedGasTipCap) span.SetAttributes( - attribute.String("suggested_gas_fee_cap", bigPtrToString(suggestedGasFeeCap)), - attribute.String("suggested_gas_tip_cap", bigPtrToString(suggestedGasTipCap)), - attribute.String("gas_fee_cap", bigPtrToString(transactor.GasFeeCap)), - attribute.String("gas_tip_cap", bigPtrToString(transactor.GasTipCap)), + attribute.String("suggested_gas_fee_cap", util.BigPtrToString(suggestedGasFeeCap)), + attribute.String("suggested_gas_tip_cap", util.BigPtrToString(suggestedGasTipCap)), + attribute.String("gas_fee_cap", util.BigPtrToString(transactor.GasFeeCap)), + attribute.String("gas_tip_cap", util.BigPtrToString(transactor.GasTipCap)), ) } else { suggestedGasPrice, err := client.SuggestGasPrice(ctx) @@ -590,8 +590,8 @@ func (t *txSubmitterImpl) applyGasFromOracle(ctx context.Context, transactor *bi } transactor.GasPrice = maxOfBig(transactor.GasPrice, suggestedGasPrice) span.SetAttributes( - attribute.String("suggested_gas_price", bigPtrToString(suggestedGasPrice)), - attribute.String("gas_price", bigPtrToString(transactor.GasPrice)), + attribute.String("suggested_gas_price", util.BigPtrToString(suggestedGasPrice)), + attribute.String("gas_price", util.BigPtrToString(transactor.GasPrice)), ) } return nil @@ -605,7 +605,7 @@ func (t *txSubmitterImpl) applyGasCeil(ctx context.Context, transactor *bind.Tra maxPrice := t.config.GetMaxGasPrice(chainID) defer func() { - span.SetAttributes(attribute.String("max_price", bigPtrToString(maxPrice))) + span.SetAttributes(attribute.String("max_price", util.BigPtrToString(maxPrice))) metrics.EndSpanWithErr(span, err) }() @@ -661,7 +661,7 @@ func (t *txSubmitterImpl) getGasBlock(ctx context.Context, chainClient client.EV if ok { span.AddEvent("could not get gas block; using cached value", trace.WithAttributes( attribute.String("error", err.Error()), - attribute.String("blockNumber", bigPtrToString(gasBlock.Number)), + attribute.String("blockNumber", util.BigPtrToString(gasBlock.Number)), )) } else { return nil, fmt.Errorf("could not get gas block: %w", err) diff --git a/ethergo/submitter/util.go b/ethergo/submitter/util.go index 63d43c92d4..3df65d6c1f 100644 --- a/ethergo/submitter/util.go +++ b/ethergo/submitter/util.go @@ -1,18 +1,16 @@ package submitter import ( - "fmt" + "github.com/ethereum/go-ethereum/core/types" + "github.com/synapsecns/sanguine/ethergo/util" + "go.opentelemetry.io/otel/attribute" "math/big" "sort" "github.com/ethereum/go-ethereum/accounts/abi/bind" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" "github.com/synapsecns/sanguine/core" "github.com/synapsecns/sanguine/ethergo/chain/gas" "github.com/synapsecns/sanguine/ethergo/submitter/db" - "github.com/synapsecns/sanguine/ethergo/util" - "go.opentelemetry.io/otel/attribute" ) // copyTransactOpts creates a deep copy of the given TransactOpts struct @@ -33,74 +31,14 @@ func copyTransactOpts(opts *bind.TransactOpts) *bind.TransactOpts { return copyOpts } -const ( - uuidAttr = "tx.UUID" - hashAttr = "tx.Hash" - fromAttr = "tx.From" - toAttr = "tx.To" - dataAttr = "tx.Data" - valueAttr = "tx.Value" - nonceAttr = "tx.Nonce" - gasLimitAttr = "tx.GasLimit" - chainIDAttr = "tx.ChainID" - gasPriceAttr = "tx.GasPrice" - gasFeeCapAttr = "tx.GasFeeCap" - gasTipCapAttr = "tx.GasTipCap" -) - -// txToAttributes converts a transaction to a slice of attribute.KeyValue. -func txToAttributes(transaction *types.Transaction, uuid string) []attribute.KeyValue { - var from string - call, err := util.TxToCall(transaction) - if err != nil { - from = fmt.Sprintf("could not be detected: %v", err) - } else { - from = call.From.Hex() - } - var attributes = []attribute.KeyValue{ - attribute.String(uuidAttr, uuid), - attribute.String(hashAttr, transaction.Hash().Hex()), - attribute.String(fromAttr, from), - attribute.String(toAttr, addressPtrToString(transaction.To())), - attribute.String(dataAttr, fmt.Sprintf("%x", transaction.Data())), - attribute.String(valueAttr, bigPtrToString(transaction.Value())), - // TODO: this could be downcast to int64, but it's unclear how we should handle overflows. - // since this is only for tracing, we can probably ignore it for now. - attribute.Int64(nonceAttr, int64(transaction.Nonce())), - attribute.Int64(gasLimitAttr, int64(transaction.Gas())), - attribute.String(chainIDAttr, bigPtrToString(transaction.ChainId())), - } - - if transaction.Type() == types.LegacyTxType && transaction.GasPrice() != nil { - attributes = append(attributes, attribute.String(gasPriceAttr, bigPtrToString(transaction.GasPrice()))) - } - - if transaction.Type() == types.DynamicFeeTxType && transaction.GasFeeCap() != nil { - attributes = append(attributes, attribute.String(gasFeeCapAttr, bigPtrToString(transaction.GasFeeCap()))) - } - - if transaction.Type() == types.DynamicFeeTxType && transaction.GasTipCap() != nil { - attributes = append(attributes, attribute.String(gasTipCapAttr, bigPtrToString(transaction.GasTipCap()))) - } +func txToAttributes(transaction *types.Transaction, uuid string) (attributes []attribute.KeyValue) { + attributes = util.TxToAttributes(transaction) + attributes = append(attributes, attribute.String(uuidAttr, uuid)) return attributes } -const nullFieldAttribute = "null" - -func addressPtrToString(address *common.Address) string { - if address == nil { - return nullFieldAttribute - } - return address.Hex() -} - -func bigPtrToString(num *big.Int) string { - if num == nil { - return nullFieldAttribute - } - return num.String() -} +const uuidAttr = "tx.UUID" // sortTxesByChainID sorts a slice of transactions by nonce. func sortTxesByChainID(txs []db.TX, maxPerChain int) map[uint64][]db.TX { diff --git a/ethergo/submitter/util_test.go b/ethergo/submitter/util_test.go index 8e664e155b..a5e057324e 100644 --- a/ethergo/submitter/util_test.go +++ b/ethergo/submitter/util_test.go @@ -15,7 +15,6 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/google/go-cmp/cmp" - "github.com/google/uuid" "github.com/synapsecns/sanguine/core" "github.com/synapsecns/sanguine/core/testsuite" "github.com/synapsecns/sanguine/ethergo/backends/simulated" @@ -23,7 +22,6 @@ import ( "github.com/synapsecns/sanguine/ethergo/submitter" "github.com/synapsecns/sanguine/ethergo/submitter/db" "github.com/synapsecns/sanguine/ethergo/util" - "go.opentelemetry.io/otel/attribute" "gotest.tools/assert" ) @@ -106,95 +104,6 @@ func assertBigIntsCopiedEqual(tb testing.TB, original *big.Int, newVal *big.Int, } } -func TestAddressPtrToString(t *testing.T) { - // Test case 1: Address is nil - var address *common.Address - assert.Equal(t, submitter.AddressPtrToString(address), submitter.NullFieldAttribute) - - // Test case 2: Address is not nil - address = core.PtrTo[common.Address](common.HexToAddress("0x1234567890123456789012345678901234567890")) - assert.Equal(t, submitter.AddressPtrToString(address), "0x1234567890123456789012345678901234567890") -} - -func TestBigPtrToString(t *testing.T) { - // Test case: num is nil - var num *big.Int - expected := submitter.NullFieldAttribute - result := submitter.BigPtrToString(num) - if result != expected { - t.Errorf("bigPtrToString(nil) = %q; want %q", result, expected) - } - - // Test case: num is an integer - num = big.NewInt(123) - expected = "123" - result = submitter.BigPtrToString(num) - if result != expected { - t.Errorf("bigPtrToString(123) = %q; want %q", result, expected) - } -} - -func (s *SubmitterSuite) TestTxToAttributesNullFields() { - s.checkEmptyTx(types.NewTx(&types.DynamicFeeTx{})) - s.checkEmptyTx(types.NewTx(&types.LegacyTx{})) -} - -func (s *SubmitterSuite) checkEmptyTx(rawTx *types.Transaction) { - tx := makeAttrMap(rawTx, uuid.New().String()) - - s.Require().Equal(tx[submitter.HashAttr].AsString(), rawTx.Hash().Hex()) - s.Require().Equal(tx[submitter.NonceAttr].AsInt64(), int64(0)) - s.Require().Equal(tx[submitter.GasLimitAttr].AsInt64(), int64(0)) - s.Require().Equal(tx[submitter.ToAttr].AsString(), submitter.NullFieldAttribute) - s.Require().Equal(tx[submitter.ValueAttr].AsString(), "0") - s.Require().Equal(tx[submitter.DataAttr].AsString(), "") - - if rawTx.Type() == types.DynamicFeeTxType { - s.Require().Equal(tx[submitter.GasTipCapAttr].AsString(), "0") - s.Require().Equal(tx[submitter.GasFeeCapAttr].AsString(), "0") - } - if rawTx.Type() == types.LegacyTxType { - s.Require().Equal(tx[submitter.GasPriceAttr].AsString(), "0") - } -} - -func (s *SubmitterSuite) TestTxToAttributesLegacyTX() { - mockTX := mocks.GetMockTxes(s.GetTestContext(), s.T(), 1, types.LegacyTxType)[0] - mapAttr := makeAttrMap(mockTX, uuid.New().String()) - - s.Require().Equal(mapAttr[submitter.HashAttr].AsString(), mockTX.Hash().String()) - s.Require().Equal(mapAttr[submitter.NonceAttr].AsInt64(), int64(mockTX.Nonce())) - s.Require().Equal(mapAttr[submitter.GasLimitAttr].AsInt64(), int64(mockTX.Gas())) - s.Require().Equal(mapAttr[submitter.ToAttr].AsString(), mockTX.To().String()) - s.Require().Equal(mapAttr[submitter.ValueAttr].AsString(), mockTX.Value().String()) - s.Require().Equal(mapAttr[submitter.DataAttr].AsString(), "") - - s.Require().Equal(mapAttr[submitter.GasPriceAttr].AsString(), mockTX.GasPrice().String()) - _, hasFeeCap := mapAttr[submitter.GasFeeCapAttr] - _, hasTipCap := mapAttr[submitter.GasTipCapAttr] - s.Require().False(hasFeeCap) - s.Require().False(hasTipCap) - s.Require().NotNil(mapAttr[submitter.FromAttr]) -} - -func (s *SubmitterSuite) TestTxToAttributesDynamicTX() { - mockTX := mocks.GetMockTxes(s.GetTestContext(), s.T(), 1, types.DynamicFeeTxType)[0] - mapAttr := makeAttrMap(mockTX, uuid.New().String()) - - s.Require().Equal(mapAttr[submitter.HashAttr].AsString(), mockTX.Hash().String()) - s.Require().Equal(mapAttr[submitter.NonceAttr].AsInt64(), int64(mockTX.Nonce())) - s.Require().Equal(mapAttr[submitter.GasLimitAttr].AsInt64(), int64(mockTX.Gas())) - s.Require().Equal(mapAttr[submitter.ToAttr].AsString(), mockTX.To().String()) - s.Require().Equal(mapAttr[submitter.ValueAttr].AsString(), mockTX.Value().String()) - s.Require().Equal(mapAttr[submitter.DataAttr].AsString(), "") - - s.Require().Equal(mapAttr[submitter.GasFeeCapAttr].AsString(), mockTX.GasFeeCap().String()) - s.Require().Equal(mapAttr[submitter.GasTipCapAttr].AsString(), mockTX.GasTipCap().String()) - _, hasGasPrice := mapAttr[submitter.GasPriceAttr] - s.Require().False(hasGasPrice) - s.Require().NotNil(mapAttr[submitter.FromAttr]) -} - func (s *SubmitterSuite) TestSortTxes() { expected := make(map[uint64][]*types.Transaction) var allTxes []db.TX @@ -289,15 +198,6 @@ func (s *SubmitterSuite) TestGroupTxesByNonce() { } } -func makeAttrMap(tx *types.Transaction, UUID string) map[string]attribute.Value { - mapAttr := make(map[string]attribute.Value) - attr := submitter.TxToAttributes(tx, UUID) - for _, a := range attr { - mapAttr[string(a.Key)] = a.Value - } - return mapAttr -} - // Test for the outersection function. func TestOutersection(t *testing.T) { set := []*big.Int{ diff --git a/ethergo/util/attributes.go b/ethergo/util/attributes.go new file mode 100644 index 0000000000..8c3ae1e1d9 --- /dev/null +++ b/ethergo/util/attributes.go @@ -0,0 +1,88 @@ +package util + +import ( + "fmt" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "go.opentelemetry.io/otel/attribute" + "math/big" +) + +const nullFieldAttribute = "null" + +const ( + hashAttr = "tx.Hash" + fromAttr = "tx.From" + toAttr = "tx.To" + dataAttr = "tx.Data" + valueAttr = "tx.Value" + nonceAttr = "tx.Nonce" + gasLimitAttr = "tx.GasLimit" + chainIDAttr = "tx.ChainID" + gasPriceAttr = "tx.GasPrice" + gasFeeCapAttr = "tx.GasFeeCap" + gasTipCapAttr = "tx.GasTipCap" + txRawAttr = "tx.Raw" +) + +// TxToAttributes converts a transaction to a slice of attribute.KeyValue. +func TxToAttributes(transaction *types.Transaction) []attribute.KeyValue { + var from string + call, err := TxToCall(transaction) + if err != nil { + from = fmt.Sprintf("could not be detected: %v", err) + } else { + from = call.From.Hex() + } + + bin, err := transaction.MarshalBinary() + if err != nil { + bin = []byte(fmt.Sprintf("could not be marshaled: %v", err)) + } + + var attributes = []attribute.KeyValue{ + attribute.String(hashAttr, transaction.Hash().Hex()), + attribute.String(fromAttr, from), + attribute.String(toAttr, addressPtrToString(transaction.To())), + attribute.String(dataAttr, fmt.Sprintf("%x", transaction.Data())), + attribute.String(valueAttr, BigPtrToString(transaction.Value())), + // TODO: this could be downcast to int64, but it's unclear how we should handle overflows. + // since this is only for tracing, we can probably ignore it for now. + // nolint: gosec + attribute.Int64(nonceAttr, int64(transaction.Nonce())), + // nolint: gosec + attribute.Int64(gasLimitAttr, int64(transaction.Gas())), + attribute.String(chainIDAttr, BigPtrToString(transaction.ChainId())), + attribute.String(txRawAttr, common.Bytes2Hex(bin)), + } + + if transaction.Type() == types.LegacyTxType && transaction.GasPrice() != nil { + attributes = append(attributes, attribute.String(gasPriceAttr, BigPtrToString(transaction.GasPrice()))) + } + + if transaction.Type() == types.DynamicFeeTxType && transaction.GasFeeCap() != nil { + attributes = append(attributes, attribute.String(gasFeeCapAttr, BigPtrToString(transaction.GasFeeCap()))) + } + + if transaction.Type() == types.DynamicFeeTxType && transaction.GasTipCap() != nil { + attributes = append(attributes, attribute.String(gasTipCapAttr, BigPtrToString(transaction.GasTipCap()))) + } + + return attributes +} + +func addressPtrToString(address *common.Address) string { + if address == nil { + return nullFieldAttribute + } + return address.Hex() +} + +// BigPtrToString converts a big.Int pointer to a string. +// TODO: move to core. +func BigPtrToString(num *big.Int) string { + if num == nil { + return nullFieldAttribute + } + return num.String() +} diff --git a/ethergo/util/attributes_test.go b/ethergo/util/attributes_test.go new file mode 100644 index 0000000000..b8aebaf3a4 --- /dev/null +++ b/ethergo/util/attributes_test.go @@ -0,0 +1,119 @@ +package util_test + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/stretchr/testify/assert" + "github.com/synapsecns/sanguine/core" + "github.com/synapsecns/sanguine/ethergo/mocks" + "github.com/synapsecns/sanguine/ethergo/util" + "go.opentelemetry.io/otel/attribute" + "math/big" + "testing" +) + +func TestAddressPtrToString(t *testing.T) { + // Test case 1: Address is nil + var address *common.Address + assert.Equal(t, util.AddressPtrToString(address), util.NullFieldAttribute) + + // Test case 2: Address is not nil + address = core.PtrTo[common.Address](common.HexToAddress("0x1234567890123456789012345678901234567890")) + assert.Equal(t, util.AddressPtrToString(address), "0x1234567890123456789012345678901234567890") +} + +func TestBigPtrToString(t *testing.T) { + // Test case: num is nil + var num *big.Int + expected := util.NullFieldAttribute + result := util.BigPtrToString(num) + if result != expected { + t.Errorf("BigPtrToString(nil) = %q; want %q", result, expected) + } + + // Test case: num is an integer + num = big.NewInt(123) + expected = "123" + result = util.BigPtrToString(num) + if result != expected { + t.Errorf("BigPtrToString(123) = %q; want %q", result, expected) + } +} + +func makeAttrMap(tx *types.Transaction) map[string]attribute.Value { + mapAttr := make(map[string]attribute.Value) + attr := util.TxToAttributes(tx) + for _, a := range attr { + mapAttr[string(a.Key)] = a.Value + } + return mapAttr +} + +func (u *UtilSuite) checkEmptyTx(rawTx *types.Transaction) { + tx := makeAttrMap(rawTx) + + u.Require().Equal(tx[util.HashAttr].AsString(), rawTx.Hash().Hex()) + u.Require().Equal(tx[util.NonceAttr].AsInt64(), int64(0)) + u.Require().Equal(tx[util.GasLimitAttr].AsInt64(), int64(0)) + u.Require().Equal(tx[util.ToAttr].AsString(), util.NullFieldAttribute) + u.Require().Equal(tx[util.ValueAttr].AsString(), "0") + u.Require().Equal(tx[util.DataAttr].AsString(), "") + + if rawTx.Type() == types.DynamicFeeTxType { + u.Require().Equal(tx[util.GasTipCapAttr].AsString(), "0") + u.Require().Equal(tx[util.GasFeeCapAttr].AsString(), "0") + } + if rawTx.Type() == types.LegacyTxType { + u.Require().Equal(tx[util.GasPriceAttr].AsString(), "0") + } +} + +func (u *UtilSuite) TestTxToAttributesNullFields() { + u.checkEmptyTx(types.NewTx(&types.DynamicFeeTx{})) + u.checkEmptyTx(types.NewTx(&types.LegacyTx{})) +} + +func (u *UtilSuite) TestTxToAttributesLegacyTX() { + mockTX := mocks.GetMockTxes(u.GetTestContext(), u.T(), 1, types.LegacyTxType)[0] + mapAttr := makeAttrMap(mockTX) + + u.Require().Equal(mapAttr[util.HashAttr].AsString(), mockTX.Hash().String()) + // nolint: gosec + u.Require().Equal(mapAttr[util.NonceAttr].AsInt64(), int64(mockTX.Nonce())) + // nolint: gosec + u.Require().Equal(mapAttr[util.GasLimitAttr].AsInt64(), int64(mockTX.Gas())) + u.Require().Equal(mapAttr[util.ToAttr].AsString(), mockTX.To().String()) + u.Require().Equal(mapAttr[util.ValueAttr].AsString(), mockTX.Value().String()) + u.Require().Equal(mapAttr[util.DataAttr].AsString(), "") + + u.Require().Equal(mapAttr[util.GasPriceAttr].AsString(), mockTX.GasPrice().String()) + _, hasFeeCap := mapAttr[util.GasFeeCapAttr] + _, hasTipCap := mapAttr[util.GasTipCapAttr] + u.Require().False(hasFeeCap) + u.Require().False(hasTipCap) + u.Require().NotNil(mapAttr[util.FromAttr]) +} + +func (u *UtilSuite) TestTxToAttributesDynamicTX() { + mockTX := mocks.GetMockTxes(u.GetTestContext(), u.T(), 1, types.DynamicFeeTxType)[0] + mapAttr := makeAttrMap(mockTX) + + u.Require().Equal(mapAttr[util.HashAttr].AsString(), mockTX.Hash().String()) + // nolint: gosec + u.Require().Equal(mapAttr[util.NonceAttr].AsInt64(), int64(mockTX.Nonce())) + // nolint: gosec + u.Require().Equal(mapAttr[util.GasLimitAttr].AsInt64(), int64(mockTX.Gas())) + u.Require().Equal(mapAttr[util.ToAttr].AsString(), mockTX.To().String()) + u.Require().Equal(mapAttr[util.ValueAttr].AsString(), mockTX.Value().String()) + u.Require().Equal(mapAttr[util.DataAttr].AsString(), "") + + u.Require().Equal(mapAttr[util.GasFeeCapAttr].AsString(), mockTX.GasFeeCap().String()) + u.Require().Equal(mapAttr[util.GasTipCapAttr].AsString(), mockTX.GasTipCap().String()) + marshaled, err := mockTX.MarshalBinary() + u.Require().NoError(err) + + u.Require().Equal(mapAttr[util.TxRawAttr].AsString(), common.Bytes2Hex(marshaled)) + _, hasGasPrice := mapAttr[util.GasPriceAttr] + u.Require().False(hasGasPrice) + u.Require().NotNil(mapAttr[util.FromAttr]) +} diff --git a/ethergo/util/export_test.go b/ethergo/util/export_test.go index cb7255c762..e19a5bff5a 100644 --- a/ethergo/util/export_test.go +++ b/ethergo/util/export_test.go @@ -1,6 +1,9 @@ package util -import "math/big" +import ( + "github.com/ethereum/go-ethereum/common" + "math/big" +) func MakeOptions(options ...CopyOption) TestCopyOptions { return makeOptions(options...) @@ -37,3 +40,38 @@ func (c copyOptions) GasTipCap() *big.Int { func (c copyOptions) TxType() *uint8 { return c.txType } + +// NullFieldAttribute is a constant used to test the null field attribute. +// it exports the underlying constant for testing. +const NullFieldAttribute = nullFieldAttribute + +func AddressPtrToString(address *common.Address) string { + return addressPtrToString(address) +} + +const ( + // HashAttr exports hashAttr for testing. + HashAttr = hashAttr + // FromAttr exports fromAttr for testing. + FromAttr = fromAttr + // ToAttr exports toAttr for testing. + ToAttr = toAttr + // DataAttr exports dataAttr for testing. + DataAttr = dataAttr + // ValueAttr exports valueAttr for testing. + ValueAttr = valueAttr + // NonceAttr exports nonceAttr for testing. + NonceAttr = nonceAttr + // GasLimitAttr exports gasLimitAttr for testing. + GasLimitAttr = gasLimitAttr + // ChainIDAttr exports chainIDAttr for testing. + ChainIDAttr = chainIDAttr + // GasPriceAttr exports gasPriceAttr for testing. + GasPriceAttr = gasPriceAttr + // GasFeeCapAttr exports gasFeeCapAttr for testing. + GasFeeCapAttr = gasFeeCapAttr + // GasTipCapAttr exports gasTipCapAttr for testing. + GasTipCapAttr = gasTipCapAttr + // TxRawAttr exports txRawAttr for testing. + TxRawAttr = txRawAttr +) diff --git a/packages/contracts-rfq/CHANGELOG.md b/packages/contracts-rfq/CHANGELOG.md index 2590c00289..10a7314cd3 100644 --- a/packages/contracts-rfq/CHANGELOG.md +++ b/packages/contracts-rfq/CHANGELOG.md @@ -3,6 +3,49 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.8.3](https://github.com/synapsecns/sanguine/compare/@synapsecns/contracts-rfq@0.8.2...@synapsecns/contracts-rfq@0.8.3) (2024-10-16) + +**Note:** Version bump only for package @synapsecns/contracts-rfq + + + + + +## [0.8.2](https://github.com/synapsecns/sanguine/compare/@synapsecns/contracts-rfq@0.8.1...@synapsecns/contracts-rfq@0.8.2) (2024-10-15) + +**Note:** Version bump only for package @synapsecns/contracts-rfq + + + + + +## [0.8.1](https://github.com/synapsecns/sanguine/compare/@synapsecns/contracts-rfq@0.8.0...@synapsecns/contracts-rfq@0.8.1) (2024-10-14) + +**Note:** Version bump only for package @synapsecns/contracts-rfq + + + + + +# [0.8.0](https://github.com/synapsecns/sanguine/compare/@synapsecns/contracts-rfq@0.7.3...@synapsecns/contracts-rfq@0.8.0) (2024-10-14) + + +### Features + +* **contracts-rfq:** arbitrary call with value [SLT-233] [SLT-318] ([#3246](https://github.com/synapsecns/sanguine/issues/3246)) ([0df8f08](https://github.com/synapsecns/sanguine/commit/0df8f08b3617e84f1ae5d71b5e45f172e08ce2db)) + + + + + +## [0.7.3](https://github.com/synapsecns/sanguine/compare/@synapsecns/contracts-rfq@0.7.2...@synapsecns/contracts-rfq@0.7.3) (2024-10-11) + +**Note:** Version bump only for package @synapsecns/contracts-rfq + + + + + ## [0.7.2](https://github.com/synapsecns/sanguine/compare/@synapsecns/contracts-rfq@0.7.1...@synapsecns/contracts-rfq@0.7.2) (2024-10-09) diff --git a/packages/contracts-rfq/contracts/FastBridgeV2.sol b/packages/contracts-rfq/contracts/FastBridgeV2.sol index dd1d109126..270f962cf9 100644 --- a/packages/contracts-rfq/contracts/FastBridgeV2.sol +++ b/packages/contracts-rfq/contracts/FastBridgeV2.sol @@ -54,6 +54,7 @@ contract FastBridgeV2 is Admin, IFastBridgeV2, IFastBridgeV2Errors { quoteRelayer: address(0), quoteExclusivitySeconds: 0, quoteId: bytes(""), + callValue: 0, callParams: bytes("") }) }); @@ -81,13 +82,15 @@ contract FastBridgeV2 is Admin, IFastBridgeV2, IFastBridgeV2Errors { revert DisputePeriodPassed(); } + address disputedRelayer = bridgeTxDetails[transactionId].proofRelayer; + // @dev relayer gets slashed effectively if dest relay has gone thru bridgeTxDetails[transactionId].status = BridgeStatus.REQUESTED; bridgeTxDetails[transactionId].proofRelayer = address(0); bridgeTxDetails[transactionId].proofBlockTimestamp = 0; bridgeTxDetails[transactionId].proofBlockNumber = 0; - emit BridgeProofDisputed(transactionId, msg.sender); + emit BridgeProofDisputed(transactionId, disputedRelayer); } /// @inheritdoc IFastBridge @@ -110,12 +113,17 @@ contract FastBridgeV2 is Admin, IFastBridgeV2, IFastBridgeV2Errors { bridgeTxDetails[transactionId].status = BridgeStatus.REFUNDED; // transfer origin collateral back to original sender + uint256 amount = transaction.originAmount + transaction.originFeeAmount; address to = transaction.originSender; address token = transaction.originToken; - uint256 amount = transaction.originAmount + transaction.originFeeAmount; - token.universalTransfer(to, amount); - emit BridgeDepositRefunded(transactionId, to, token, amount); + if (token == UniversalTokenLib.ETH_ADDRESS) { + Address.sendValue(payable(to), amount); + } else { + IERC20(token).safeTransfer(to, amount); + } + + emit BridgeDepositRefunded(transactionId, transaction.originSender, transaction.originToken, amount); } /// @inheritdoc IFastBridge @@ -126,39 +134,49 @@ contract FastBridgeV2 is Admin, IFastBridgeV2, IFastBridgeV2Errors { } /// @inheritdoc IFastBridge - function getBridgeTransaction(bytes memory request) external pure returns (BridgeTransaction memory) { - // TODO: the note below isn't true anymore with the BridgeTransactionV2 struct - // since the variable length `callParams` was added. This needs to be fixed/acknowledged. - - // Note: when passing V2 request, this will decode the V1 fields correctly since the new fields were - // added as the last fields of the struct and hence the ABI decoder will simply ignore the extra data. - return abi.decode(request, (BridgeTransaction)); + /// @dev This method is added to achieve backwards compatibility with decoding requests into V1 structs: + /// - `callValue` is partially reported as a zero/non-zero flag + /// - `callParams` is ignored + /// In order to process all kinds of requests use getBridgeTransactionV2 instead. + function getBridgeTransaction(bytes memory request) external view returns (BridgeTransaction memory) { + // Try decoding into V2 struct first. This will revert if V1 struct is passed + try this.getBridgeTransactionV2(request) returns (BridgeTransactionV2 memory txV2) { + // Note: we entirely ignore the callParams field, as it was not present in V1 + return BridgeTransaction({ + originChainId: txV2.originChainId, + destChainId: txV2.destChainId, + originSender: txV2.originSender, + destRecipient: txV2.destRecipient, + originToken: txV2.originToken, + destToken: txV2.destToken, + originAmount: txV2.originAmount, + destAmount: txV2.destAmount, + originFeeAmount: txV2.originFeeAmount, + sendChainGas: txV2.callValue != 0, + deadline: txV2.deadline, + nonce: txV2.nonce + }); + } catch { + // Fallback to V1 struct + return abi.decode(request, (BridgeTransaction)); + } } /// @inheritdoc IFastBridgeV2 - // TODO: reduce cyclomatic complexity alongside arbitrary call - // solhint-disable-next-line code-complexity function bridge(BridgeParams memory params, BridgeParamsV2 memory paramsV2) public payable { - // check bridge params - if (params.dstChainId == block.chainid) revert ChainIncorrect(); - if (params.originAmount == 0 || params.destAmount == 0) revert AmountIncorrect(); - if (params.sender == address(0) || params.to == address(0)) revert ZeroAddress(); - if (params.originToken == address(0) || params.destToken == address(0)) revert ZeroAddress(); - if (params.deadline < block.timestamp + MIN_DEADLINE_PERIOD) revert DeadlineTooShort(); - if (paramsV2.callParams.length > MAX_CALL_PARAMS_LENGTH) revert CallParamsLengthAboveMax(); int256 exclusivityEndTime = int256(block.timestamp) + paramsV2.quoteExclusivitySeconds; - // exclusivityEndTime must be in range (0 .. params.deadline] - if (exclusivityEndTime <= 0 || exclusivityEndTime > int256(params.deadline)) { - revert ExclusivityParamsIncorrect(); - } + _validateBridgeParams(params, paramsV2, exclusivityEndTime); + // transfer tokens to bridge contract - // @dev use returned originAmount in request in case of transfer fees - uint256 originAmount = _pullToken(address(this), params.originToken, params.originAmount); + /// @dev use returned originAmount in request in case of transfer fees + uint256 originAmount = _takeBridgedUserAsset(params.originToken, params.originAmount); // track amount of origin token owed to protocol uint256 originFeeAmount; - if (protocolFeeRate > 0) originFeeAmount = (originAmount * protocolFeeRate) / FEE_BPS; - originAmount -= originFeeAmount; // remove from amount used in request as not relevant for relayers + if (protocolFeeRate > 0) { + originFeeAmount = (originAmount * protocolFeeRate) / FEE_BPS; + originAmount -= originFeeAmount; // remove from amount used in request as not relevant for relayers + } // set status to requested bytes memory request = abi.encode( @@ -172,7 +190,7 @@ contract FastBridgeV2 is Admin, IFastBridgeV2, IFastBridgeV2Errors { originAmount: originAmount, destAmount: params.destAmount, originFeeAmount: originFeeAmount, - sendChainGas: params.sendChainGas, + callValue: paramsV2.callValue, deadline: params.deadline, nonce: senderNonces[params.sender]++, // increment nonce on every bridge exclusivityRelayer: paramsV2.quoteRelayer, @@ -184,91 +202,73 @@ contract FastBridgeV2 is Admin, IFastBridgeV2, IFastBridgeV2Errors { bytes32 transactionId = keccak256(request); bridgeTxDetails[transactionId].status = BridgeStatus.REQUESTED; - emit BridgeRequested( - transactionId, - params.sender, - request, - params.dstChainId, - params.originToken, - params.destToken, - originAmount, - params.destAmount, - params.sendChainGas - ); + emit BridgeRequested({ + transactionId: transactionId, + sender: params.sender, + request: request, + destChainId: params.dstChainId, + originToken: params.originToken, + destToken: params.destToken, + originAmount: originAmount, + destAmount: params.destAmount, + sendChainGas: paramsV2.callValue != 0 + }); emit BridgeQuoteDetails(transactionId, paramsV2.quoteId); } /// @inheritdoc IFastBridgeV2 - // TODO: reduce cyclomatic complexity alongside arbitrary call - // solhint-disable-next-line code-complexity function relay(bytes memory request, address relayer) public payable { - if (relayer == address(0)) revert ZeroAddress(); - // Check if the transaction has already been relayed bytes32 transactionId = keccak256(request); - if (bridgeRelays(transactionId)) revert TransactionRelayed(); - // Decode the transaction and check that it could be relayed on this chain BridgeTransactionV2 memory transaction = getBridgeTransactionV2(request); - if (transaction.destChainId != uint32(block.chainid)) revert ChainIncorrect(); - // Check the deadline for relay to happen - if (block.timestamp > transaction.deadline) revert DeadlineExceeded(); - // Check the exclusivity period, if it is still ongoing - // forgefmt: disable-next-item - if ( - transaction.exclusivityRelayer != address(0) && - transaction.exclusivityRelayer != relayer && - block.timestamp <= transaction.exclusivityEndTime - ) { - revert ExclusivityPeriodNotPassed(); - } + _validateRelayParams(transaction, transactionId, relayer); // mark bridge transaction as relayed bridgeRelayDetails[transactionId] = BridgeRelay({blockNumber: uint48(block.number), blockTimestamp: uint48(block.timestamp), relayer: relayer}); - // transfer tokens to recipient on destination chain and gas rebate if requested + // transfer tokens to recipient on destination chain and do an arbitrary call if requested address to = transaction.destRecipient; address token = transaction.destToken; uint256 amount = transaction.destAmount; + uint256 callValue = transaction.callValue; + + // Emit the event before any external calls + emit BridgeRelayed({ + transactionId: transactionId, + relayer: relayer, + to: to, + originChainId: transaction.originChainId, + originToken: transaction.originToken, + destToken: token, + originAmount: transaction.originAmount, + destAmount: amount, + chainGasAmount: callValue + }); // All state changes have been done at this point, can proceed to the external calls. // This follows the checks-effects-interactions pattern to mitigate potential reentrancy attacks. - if (transaction.callParams.length == 0) { - // No arbitrary call requested, so we just pull the tokens from the Relayer to the recipient, - // or transfer ETH to the recipient (if token is ETH_ADDRESS) - _pullToken(to, token, amount); - } else if (token != UniversalTokenLib.ETH_ADDRESS) { - // Arbitrary call requested with ERC20: pull the tokens from the Relayer to the recipient first - _pullToken(to, token, amount); - // Follow up with the hook function call - _checkedCallRecipient({ - recipient: to, - msgValue: 0, - token: token, - amount: amount, - callParams: transaction.callParams - }); + if (token == UniversalTokenLib.ETH_ADDRESS) { + // For ETH non-zero callValue is not allowed + if (callValue != 0) revert NativeTokenCallValueNotSupported(); + // Check that the correct msg.value was sent + if (msg.value != amount) revert MsgValueIncorrect(); } else { - // Arbitrary call requested with ETH: combine the ETH transfer with the call - _checkedCallRecipient({ - recipient: to, - msgValue: amount, - token: token, - amount: amount, - callParams: transaction.callParams - }); + // For ERC20s, we check that the correct msg.value was sent + if (msg.value != callValue) revert MsgValueIncorrect(); + // We need to transfer the tokens from the Relayer to the recipient first before performing an + // optional post-transfer arbitrary call. + IERC20(token).safeTransferFrom(msg.sender, to, amount); } - emit BridgeRelayed( - transactionId, - relayer, - to, - transaction.originChainId, - transaction.originToken, - transaction.destToken, - transaction.originAmount, - transaction.destAmount, - // chainGasAmount is 0 since the gas rebate function is deprecated - 0 - ); + if (transaction.callParams.length != 0) { + // Arbitrary call requested, perform it while supplying full msg.value to the recipient + // Note: if token has a fee on transfers, the recipient will have received less than `amount`. + // This is a very niche edge case and should be handled by the recipient contract. + _checkedCallRecipient({recipient: to, token: token, amount: amount, callParams: transaction.callParams}); + } else if (msg.value != 0) { + // No arbitrary call requested, but msg.value was sent. This is either a relay with ETH, + // or a non-zero callValue request with an ERC20. In both cases, transfer the ETH to the recipient. + Address.sendValue(payable(to), msg.value); + } } /// @inheritdoc IFastBridgeV2 @@ -307,12 +307,23 @@ contract FastBridgeV2 is Admin, IFastBridgeV2, IFastBridgeV2Errors { // update protocol fees if origin fee amount exists if (transaction.originFeeAmount > 0) protocolFees[transaction.originToken] += transaction.originFeeAmount; - // transfer origin collateral less fee to specified address address token = transaction.originToken; uint256 amount = transaction.originAmount; - token.universalTransfer(to, amount); - emit BridgeDepositClaimed(transactionId, bridgeTxDetails[transactionId].proofRelayer, to, token, amount); + // transfer origin collateral to specified address (protocol fee was pre-deducted at deposit) + if (token == UniversalTokenLib.ETH_ADDRESS) { + Address.sendValue(payable(to), amount); + } else { + IERC20(token).safeTransfer(to, amount); + } + + emit BridgeDepositClaimed( + transactionId, + bridgeTxDetails[transactionId].proofRelayer, + to, + transaction.originToken, + transaction.originAmount + ); } function bridgeStatuses(bytes32 transactionId) public view returns (BridgeStatus status) { @@ -335,26 +346,24 @@ contract FastBridgeV2 is Admin, IFastBridgeV2, IFastBridgeV2Errors { return abi.decode(request, (BridgeTransactionV2)); } - /// @notice Pulls a requested token from the user to the requested recipient. - /// @dev Be careful of re-entrancy issues when msg.value > 0 and recipient != address(this) - function _pullToken(address recipient, address token, uint256 amount) internal returns (uint256 amountPulled) { - if (token != UniversalTokenLib.ETH_ADDRESS) { + /// @notice Takes the bridged asset from the user into FastBridgeV2 custody. It will be later + /// claimed by the relayer who completed the relay on destination chain, or refunded back to the user, + /// should no one complete the relay. + function _takeBridgedUserAsset(address token, uint256 amount) internal returns (uint256 amountTaken) { + if (token == UniversalTokenLib.ETH_ADDRESS) { + // For ETH we just need to check that the supplied msg.value is correct. + // Supplied `msg.value` is already in FastBridgeV2 custody. + if (amount != msg.value) revert MsgValueIncorrect(); + amountTaken = msg.value; + } else { + // For ERC20s, token is explicitly transferred from the user to FastBridgeV2. + // We don't allow non-zero `msg.value` to avoid extra funds from being stuck in FastBridgeV2. token.assertIsContract(); - // Token needs to be pulled only if msg.value is zero - // This way user can specify WETH as the origin asset if (msg.value != 0) revert MsgValueIncorrect(); - // Record token balance before transfer - amountPulled = IERC20(token).balanceOf(recipient); - IERC20(token).safeTransferFrom(msg.sender, recipient, amount); - // Use the difference between the recorded balance and the current balance as the amountPulled - amountPulled = IERC20(token).balanceOf(recipient) - amountPulled; - } else { - // Otherwise, we need to check that ETH amount matches msg.value - if (amount != msg.value) revert MsgValueIncorrect(); - // Transfer value to recipient if not this address - if (recipient != address(this)) token.universalTransfer(recipient, amount); - // We will forward msg.value in the external call later, if recipient is not this contract - amountPulled = msg.value; + amountTaken = IERC20(token).balanceOf(address(this)); + IERC20(token).safeTransferFrom(msg.sender, address(this), amount); + // Use the balance difference as the amount taken in case of fee on transfer tokens. + amountTaken = IERC20(token).balanceOf(address(this)) - amountTaken; } } @@ -362,7 +371,6 @@ contract FastBridgeV2 is Admin, IFastBridgeV2, IFastBridgeV2Errors { /// all the necessary checks for the returned value. function _checkedCallRecipient( address recipient, - uint256 msgValue, address token, uint256 amount, bytes memory callParams @@ -372,7 +380,7 @@ contract FastBridgeV2 is Admin, IFastBridgeV2, IFastBridgeV2Errors { bytes memory hookData = abi.encodeCall(IFastBridgeRecipient.fastBridgeTransferReceived, (token, amount, callParams)); // This will bubble any revert messages from the hook function - bytes memory returnData = Address.functionCallWithValue({target: recipient, data: hookData, value: msgValue}); + bytes memory returnData = Address.functionCallWithValue({target: recipient, data: hookData, value: msg.value}); // Explicit revert if no return data at all if (returnData.length == 0) revert RecipientNoReturnValue(); // Check that exactly a single return value was returned @@ -394,4 +402,59 @@ contract FastBridgeV2 is Admin, IFastBridgeV2, IFastBridgeV2Errors { delta = uint40(block.timestamp) - proofBlockTimestamp; } } + + /// @notice Performs all the necessary checks for a bridge to happen. + /// @dev There's no good way to refactor this function to reduce cyclomatic complexity due to + /// the number of checks that need to be performed, so we skip the code-complexity rule here. + // solhint-disable-next-line code-complexity + function _validateBridgeParams( + BridgeParams memory params, + BridgeParamsV2 memory paramsV2, + int256 exclusivityEndTime + ) + internal + view + { + // Check V1 (legacy) params + if (params.dstChainId == block.chainid) revert ChainIncorrect(); + if (params.originAmount == 0 || params.destAmount == 0) revert AmountIncorrect(); + if (params.sender == address(0) || params.to == address(0)) revert ZeroAddress(); + if (params.originToken == address(0) || params.destToken == address(0)) revert ZeroAddress(); + if (params.deadline < block.timestamp + MIN_DEADLINE_PERIOD) revert DeadlineTooShort(); + // Check V2 params + if (paramsV2.callParams.length > MAX_CALL_PARAMS_LENGTH) revert CallParamsLengthAboveMax(); + if (paramsV2.callValue != 0 && params.destToken == UniversalTokenLib.ETH_ADDRESS) { + revert NativeTokenCallValueNotSupported(); + } + // exclusivityEndTime must be in range (0 .. params.deadline] + if (exclusivityEndTime <= 0 || exclusivityEndTime > int256(params.deadline)) { + revert ExclusivityParamsIncorrect(); + } + } + + /// @notice Performs all the necessary checks for a relay to happen. + function _validateRelayParams( + BridgeTransactionV2 memory transaction, + bytes32 transactionId, + address relayer + ) + internal + view + { + if (relayer == address(0)) revert ZeroAddress(); + // Check if the transaction has already been relayed + if (bridgeRelays(transactionId)) revert TransactionRelayed(); + if (transaction.destChainId != block.chainid) revert ChainIncorrect(); + // Check the deadline for relay to happen + if (block.timestamp > transaction.deadline) revert DeadlineExceeded(); + // Check the exclusivity period, if it is still ongoing + // forgefmt: disable-next-item + if ( + transaction.exclusivityRelayer != address(0) && + transaction.exclusivityRelayer != relayer && + block.timestamp <= transaction.exclusivityEndTime + ) { + revert ExclusivityPeriodNotPassed(); + } + } } diff --git a/packages/contracts-rfq/contracts/interfaces/IFastBridge.sol b/packages/contracts-rfq/contracts/interfaces/IFastBridge.sol index b691dfb5b4..033d602f76 100644 --- a/packages/contracts-rfq/contracts/interfaces/IFastBridge.sol +++ b/packages/contracts-rfq/contracts/interfaces/IFastBridge.sol @@ -97,7 +97,7 @@ interface IFastBridge { /// @notice Decodes bridge request into a bridge transaction /// @param request The bridge request to decode - function getBridgeTransaction(bytes memory request) external pure returns (BridgeTransaction memory); + function getBridgeTransaction(bytes memory request) external view returns (BridgeTransaction memory); /// @notice Checks if the dispute period has passed so bridge deposit can be claimed /// @param transactionId The transaction id associated with the encoded bridge transaction to check diff --git a/packages/contracts-rfq/contracts/interfaces/IFastBridgeV2.sol b/packages/contracts-rfq/contracts/interfaces/IFastBridgeV2.sol index c4c3751a5b..54dbb5f3f6 100644 --- a/packages/contracts-rfq/contracts/interfaces/IFastBridgeV2.sol +++ b/packages/contracts-rfq/contracts/interfaces/IFastBridgeV2.sol @@ -30,14 +30,17 @@ interface IFastBridgeV2 is IFastBridge { /// for backwards compatibility. /// Note: quoteRelayer and quoteExclusivitySeconds are either both zero (indicating no exclusivity) /// or both non-zero (indicating exclusivity for the given period). + /// Note: callValue > 0 can NOT be used with destToken = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE (ETH_ADDRESS) /// @param quoteRelayer Relayer that provided the quote for the transaction /// @param quoteExclusivitySeconds Period of time the quote relayer is guaranteed exclusivity after user's deposit /// @param quoteId Unique quote identifier used for tracking the quote + /// @param callValue ETH value to send to the recipient (if any) /// @param callParams Parameters for the arbitrary call to the destination recipient (if any) struct BridgeParamsV2 { address quoteRelayer; int256 quoteExclusivitySeconds; bytes quoteId; + uint256 callValue; bytes callParams; } @@ -54,7 +57,7 @@ interface IFastBridgeV2 is IFastBridge { uint256 originAmount; // amount in on origin bridge less originFeeAmount uint256 destAmount; uint256 originFeeAmount; - bool sendChainGas; + uint256 callValue; // ETH value to send to the recipient (if any) - replaces V1's sendChainGas flag uint256 deadline; // user specified deadline for destination relay uint256 nonce; address exclusivityRelayer; @@ -67,7 +70,7 @@ interface IFastBridgeV2 is IFastBridge { /// @notice Initiates bridge on origin chain to be relayed by off-chain relayer, with the ability /// to provide temporary exclusivity fill rights for the quote relayer. /// @param params The parameters required to bridge - /// @param paramsV2 The parameters for exclusivity fill rights (optional, could be left empty) + /// @param paramsV2 The parameters for exclusivity fill rights (optional, can be left empty) function bridge(BridgeParams memory params, BridgeParamsV2 memory paramsV2) external payable; /// @notice Relays destination side of bridge transaction by off-chain relayer diff --git a/packages/contracts-rfq/contracts/interfaces/IFastBridgeV2Errors.sol b/packages/contracts-rfq/contracts/interfaces/IFastBridgeV2Errors.sol index 7cc1423a84..f40b760c30 100644 --- a/packages/contracts-rfq/contracts/interfaces/IFastBridgeV2Errors.sol +++ b/packages/contracts-rfq/contracts/interfaces/IFastBridgeV2Errors.sol @@ -7,6 +7,7 @@ interface IFastBridgeV2Errors { error ChainIncorrect(); error ExclusivityParamsIncorrect(); error MsgValueIncorrect(); + error NativeTokenCallValueNotSupported(); error SenderIncorrect(); error StatusIncorrect(); error ZeroAddress(); diff --git a/packages/contracts-rfq/package.json b/packages/contracts-rfq/package.json index b9eda08002..5ce11f669b 100644 --- a/packages/contracts-rfq/package.json +++ b/packages/contracts-rfq/package.json @@ -1,7 +1,7 @@ { "name": "@synapsecns/contracts-rfq", "license": "MIT", - "version": "0.7.2", + "version": "0.8.3", "description": "FastBridge contracts.", "private": true, "files": [ @@ -20,6 +20,7 @@ "build:slither": "forge build --out=out --build-info --force", "test:coverage": "echo 'Please use foundry'", "test": "forge test", + "gas:bench": "forge test --mc GasBenchmark --gas-report", "lint": "forge fmt && npm run solhint", "lint:check": "forge fmt --check && npm run solhint:check", "ci:lint": "npm run lint:check", diff --git a/packages/contracts-rfq/script/FastBridge.s.sol b/packages/contracts-rfq/script/FastBridge.s.sol index 84d048575d..de3208889f 100644 --- a/packages/contracts-rfq/script/FastBridge.s.sol +++ b/packages/contracts-rfq/script/FastBridge.s.sol @@ -7,6 +7,9 @@ import {Script} from "forge-std/Script.sol"; contract DeployFastBridge is Script { FastBridge public bridge; + /// @notice We include an empty "test" function so that this contract does not appear in the coverage report. + function testDeployFastBridge() external {} + /// e.g. forge script contracts/script/FastBridge.s.sol --sig "run(address, address[])" 0xa0Ee7A142d267C1f36714E4a8F75612F20a79720 "[0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f]" function run(address owner, address[] memory relayers) external { vm.startBroadcast(); diff --git a/packages/contracts-rfq/test/FastBridgeMock.sol b/packages/contracts-rfq/test/FastBridgeMock.sol index 0f035c3430..a84ce7db72 100644 --- a/packages/contracts-rfq/test/FastBridgeMock.sol +++ b/packages/contracts-rfq/test/FastBridgeMock.sol @@ -16,6 +16,9 @@ contract FastBridgeMock is IFastBridge, Admin { /// @dev to prevent replays uint256 public nonce; + /// @notice We include an empty "test" function so that this contract does not appear in the coverage report. + function testFastBridgeMock() external {} + function getBridgeTransaction(bytes memory request) public pure returns (BridgeTransaction memory) { return abi.decode(request, (BridgeTransaction)); } diff --git a/packages/contracts-rfq/test/FastBridgeV2.Dst.ArbitraryCall.t.sol b/packages/contracts-rfq/test/FastBridgeV2.Dst.ArbitraryCall.t.sol index af4f6235c6..1fb3ed8148 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.Dst.ArbitraryCall.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.Dst.ArbitraryCall.t.sol @@ -1,7 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import {FastBridgeV2DstExclusivityTest, IFastBridgeV2} from "./FastBridgeV2.Dst.Exclusivity.t.sol"; +import {IFastBridgeV2} from "../contracts/interfaces/IFastBridgeV2.sol"; +import {FastBridgeV2DstExclusivityTest} from "./FastBridgeV2.Dst.Exclusivity.t.sol"; import {RecipientMock} from "./mocks/RecipientMock.sol"; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; @@ -98,6 +99,28 @@ contract FastBridgeV2DstArbitraryCallTest is FastBridgeV2DstExclusivityTest { relayWithAddress({caller: relayerB, relayer: relayerA, msgValue: 0, bridgeTx: tokenTx}); } + function test_relay_token_withCallValue_excessiveReturnValueRecipient_revertWhenCallParamsPresent() + public + virtual + override + { + setTokenTestCallValue(CALL_VALUE); + setTokenTestRecipient(excessiveReturnValueRecipient); + vm.expectRevert(RecipientIncorrectReturnValue.selector); + relay({caller: relayerA, msgValue: CALL_VALUE, bridgeTx: tokenTx}); + } + + function test_relay_token_withRelayerAddressCallValue_excessiveReturnValueRecipient_revertWhenCallParamsPresent() + public + virtual + override + { + setTokenTestCallValue(CALL_VALUE); + setTokenTestRecipient(excessiveReturnValueRecipient); + vm.expectRevert(RecipientIncorrectReturnValue.selector); + relayWithAddress({caller: relayerB, relayer: relayerA, msgValue: CALL_VALUE, bridgeTx: tokenTx}); + } + function test_relay_eth_excessiveReturnValueRecipient_revertWhenCallParamsPresent() public virtual override { setEthTestRecipient(excessiveReturnValueRecipient); vm.expectRevert(RecipientIncorrectReturnValue.selector); @@ -132,6 +155,28 @@ contract FastBridgeV2DstArbitraryCallTest is FastBridgeV2DstExclusivityTest { relayWithAddress({caller: relayerB, relayer: relayerA, msgValue: 0, bridgeTx: tokenTx}); } + function test_relay_token_withCallValue_incorrectReturnValueRecipient_revertWhenCallParamsPresent() + public + virtual + override + { + setTokenTestCallValue(CALL_VALUE); + setTokenTestRecipient(incorrectReturnValueRecipient); + vm.expectRevert(RecipientIncorrectReturnValue.selector); + relay({caller: relayerA, msgValue: CALL_VALUE, bridgeTx: tokenTx}); + } + + function test_relay_token_withRelayerAddressCallValue_incorrectReturnValueRecipient_revertWhenCallParamsPresent() + public + virtual + override + { + setTokenTestCallValue(CALL_VALUE); + setTokenTestRecipient(incorrectReturnValueRecipient); + vm.expectRevert(RecipientIncorrectReturnValue.selector); + relayWithAddress({caller: relayerB, relayer: relayerA, msgValue: CALL_VALUE, bridgeTx: tokenTx}); + } + function test_relay_eth_incorrectReturnValueRecipient_revertWhenCallParamsPresent() public virtual override { setEthTestRecipient(incorrectReturnValueRecipient); vm.expectRevert(RecipientIncorrectReturnValue.selector); @@ -150,6 +195,8 @@ contract FastBridgeV2DstArbitraryCallTest is FastBridgeV2DstExclusivityTest { // ══════════════════════════════════════════════ NO-OP RECIPIENT ══════════════════════════════════════════════════ + // Note: in these tests NoOpRecipient doesn't implement hook function, so we expect a generic OZ library revert. + function test_relay_token_noOpRecipient_revertWhenCallParamsPresent() public virtual override { setTokenTestRecipient(noOpRecipient); vm.expectRevert(Address.FailedInnerCall.selector); @@ -162,6 +209,24 @@ contract FastBridgeV2DstArbitraryCallTest is FastBridgeV2DstExclusivityTest { relayWithAddress({caller: relayerB, relayer: relayerA, msgValue: 0, bridgeTx: tokenTx}); } + function test_relay_token_withCallValue_noOpRecipient_revertWhenCallParamsPresent() public virtual override { + setTokenTestCallValue(CALL_VALUE); + setTokenTestRecipient(noOpRecipient); + vm.expectRevert(Address.FailedInnerCall.selector); + relay({caller: relayerA, msgValue: CALL_VALUE, bridgeTx: tokenTx}); + } + + function test_relay_token_withRelayerAddressCallValue_noOpRecipient_revertWhenCallParamsPresent() + public + virtual + override + { + setTokenTestCallValue(CALL_VALUE); + setTokenTestRecipient(noOpRecipient); + vm.expectRevert(Address.FailedInnerCall.selector); + relayWithAddress({caller: relayerB, relayer: relayerA, msgValue: CALL_VALUE, bridgeTx: tokenTx}); + } + function test_relay_eth_noOpRecipient_revertWhenCallParamsPresent() public virtual override { setEthTestRecipient(noOpRecipient); vm.expectRevert(Address.FailedInnerCall.selector); @@ -192,6 +257,28 @@ contract FastBridgeV2DstArbitraryCallTest is FastBridgeV2DstExclusivityTest { relayWithAddress({caller: relayerB, relayer: relayerA, msgValue: 0, bridgeTx: tokenTx}); } + function test_relay_token_withCallValue_noReturnValueRecipient_revertWhenCallParamsPresent() + public + virtual + override + { + setTokenTestCallValue(CALL_VALUE); + setTokenTestRecipient(noReturnValueRecipient); + vm.expectRevert(RecipientNoReturnValue.selector); + relay({caller: relayerA, msgValue: CALL_VALUE, bridgeTx: tokenTx}); + } + + function test_relay_token_withRelayerAddressCallValue_noReturnValueRecipient_revertWhenCallParamsPresent() + public + virtual + override + { + setTokenTestCallValue(CALL_VALUE); + setTokenTestRecipient(noReturnValueRecipient); + vm.expectRevert(RecipientNoReturnValue.selector); + relayWithAddress({caller: relayerB, relayer: relayerA, msgValue: CALL_VALUE, bridgeTx: tokenTx}); + } + function test_relay_eth_noReturnValueRecipient_revertWhenCallParamsPresent() public virtual override { setEthTestRecipient(noReturnValueRecipient); vm.expectRevert(RecipientNoReturnValue.selector); @@ -222,6 +309,20 @@ contract FastBridgeV2DstArbitraryCallTest is FastBridgeV2DstExclusivityTest { relayWithAddress({caller: relayerB, relayer: relayerA, msgValue: 0, bridgeTx: tokenTx}); } + function test_relay_token_withCallValue_revert_recipientReverts() public { + setTokenTestCallValue(CALL_VALUE); + mockRecipientRevert(tokenTx); + vm.expectRevert(REVERT_MSG); + relay({caller: relayerA, msgValue: CALL_VALUE, bridgeTx: tokenTx}); + } + + function test_relay_token_withRelayerAddressCallValue_revert_recipientReverts() public { + setTokenTestCallValue(CALL_VALUE); + mockRecipientRevert(tokenTx); + vm.expectRevert(REVERT_MSG); + relayWithAddress({caller: relayerB, relayer: relayerA, msgValue: CALL_VALUE, bridgeTx: tokenTx}); + } + function test_relay_eth_revert_recipientReverts() public { mockRecipientRevert(ethTx); vm.expectRevert(REVERT_MSG); @@ -237,14 +338,18 @@ contract FastBridgeV2DstArbitraryCallTest is FastBridgeV2DstExclusivityTest { function test_relay_eth_noCallParams_revert_recipientReverts() public { setEthTestCallParams(""); vm.mockCallRevert({callee: userB, data: "", revertData: bytes(REVERT_MSG)}); - vm.expectRevert("ETH transfer failed"); + // Note: OZ library doesn't bubble the revert message for just sending ETH + // (as opposed to doing an external hook call). Therefore we expect a generic library revert. + vm.expectRevert(Address.FailedInnerCall.selector); relay({caller: relayerB, msgValue: ethParams.destAmount, bridgeTx: ethTx}); } function test_relay_eth_withRelayerAddress_noCallParams_revert_recipientReverts() public { setEthTestCallParams(""); vm.mockCallRevert({callee: userB, data: "", revertData: bytes(REVERT_MSG)}); - vm.expectRevert("ETH transfer failed"); + // Note: OZ library doesn't bubble the revert message for just sending ETH + // (as opposed to doing an external hook call). Therefore we expect a generic library revert. + vm.expectRevert(Address.FailedInnerCall.selector); relayWithAddress({caller: relayerA, relayer: relayerB, msgValue: ethParams.destAmount, bridgeTx: ethTx}); } } diff --git a/packages/contracts-rfq/test/FastBridgeV2.Dst.Base.t.sol b/packages/contracts-rfq/test/FastBridgeV2.Dst.Base.t.sol index 9e79e48489..f499136cfe 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.Dst.Base.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.Dst.Base.t.sol @@ -3,10 +3,13 @@ pragma solidity ^0.8.20; import {FastBridgeV2, FastBridgeV2Test, IFastBridgeV2} from "./FastBridgeV2.t.sol"; -// solhint-disable func-name-mixedcase, ordering +// solhint-disable func-name-mixedcase, no-empty-blocks contract FastBridgeV2DstBaseTest is FastBridgeV2Test { uint256 public constant LEFTOVER_BALANCE = 1 ether; + /// @notice We include an empty "test" function so that this contract does not appear in the coverage report. + function testFastBridgeV2DstBaseTest() external {} + function setUp() public virtual override { vm.chainId(DST_CHAIN_ID); super.setUp(); diff --git a/packages/contracts-rfq/test/FastBridgeV2.Dst.Exclusivity.t.sol b/packages/contracts-rfq/test/FastBridgeV2.Dst.Exclusivity.t.sol index a616872f93..ba66ae53c4 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.Dst.Exclusivity.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.Dst.Exclusivity.t.sol @@ -1,30 +1,15 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import {FastBridgeV2DstTest, IFastBridgeV2} from "./FastBridgeV2.Dst.t.sol"; +import {FastBridgeV2DstTest} from "./FastBridgeV2.Dst.t.sol"; // solhint-disable func-name-mixedcase, ordering contract FastBridgeV2DstExclusivityTest is FastBridgeV2DstTest { uint256 public constant EXCLUSIVITY_PERIOD = 60 seconds; function createFixturesV2() public virtual override { - tokenParamsV2 = IFastBridgeV2.BridgeParamsV2({ - quoteRelayer: relayerA, - quoteExclusivitySeconds: int256(EXCLUSIVITY_PERIOD), - quoteId: "", - callParams: "" - }); - ethParamsV2 = IFastBridgeV2.BridgeParamsV2({ - quoteRelayer: relayerB, - quoteExclusivitySeconds: int256(EXCLUSIVITY_PERIOD), - quoteId: "", - callParams: "" - }); - - tokenTx.exclusivityRelayer = relayerA; - tokenTx.exclusivityEndTime = block.timestamp + EXCLUSIVITY_PERIOD; - ethTx.exclusivityRelayer = relayerB; - ethTx.exclusivityEndTime = block.timestamp + EXCLUSIVITY_PERIOD; + setTokenTestExclusivityParams(relayerA, EXCLUSIVITY_PERIOD); + setEthTestExclusivityParams(relayerB, EXCLUSIVITY_PERIOD); } // ═══════════════════════════════════════════════ RELAY: TOKEN ════════════════════════════════════════════════════ diff --git a/packages/contracts-rfq/test/FastBridgeV2.Dst.t.sol b/packages/contracts-rfq/test/FastBridgeV2.Dst.t.sol index 026fed12f9..047ad6bec2 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.Dst.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.Dst.t.sol @@ -23,6 +23,8 @@ contract FastBridgeV2DstTest is FastBridgeV2DstBaseTest { uint256 chainGasAmount ); + uint256 public constant CALL_VALUE = 1_337_420; + address public excessiveReturnValueRecipient; address public incorrectReturnValueRecipient; address public noOpRecipient; @@ -67,6 +69,7 @@ contract FastBridgeV2DstTest is FastBridgeV2DstBaseTest { public virtual { + uint256 chainGasAmount = bridgeTx.destToken == ETH_ADDRESS ? 0 : bridgeTx.callValue; vm.expectEmit(address(fastBridge)); emit BridgeRelayed({ transactionId: txId, @@ -77,7 +80,7 @@ contract FastBridgeV2DstTest is FastBridgeV2DstBaseTest { destToken: bridgeTx.destToken, originAmount: bridgeTx.originAmount, destAmount: bridgeTx.destAmount, - chainGasAmount: 0 + chainGasAmount: chainGasAmount }); } @@ -89,15 +92,25 @@ contract FastBridgeV2DstTest is FastBridgeV2DstBaseTest { assertEq(relayer, expectedRelayer); } + function checkTokenBalances(address recipient, address relayCaller) public view { + assertEq(dstToken.balanceOf(recipient), tokenParams.destAmount); + assertEq(dstToken.balanceOf(relayCaller), LEFTOVER_BALANCE); + assertEq(dstToken.balanceOf(address(fastBridge)), 0); + } + + function checkEthBalances(address recipient, address relayCaller) public view { + assertEq(recipient.balance, ethParams.destAmount); + assertEq(relayCaller.balance, LEFTOVER_BALANCE); + assertEq(address(fastBridge).balance, 0); + } + /// @notice RelayerA completes the ERC20 bridge request function test_relay_token() public { bytes32 txId = getTxId(tokenTx); expectBridgeRelayed(tokenTx, txId, relayerA); relay({caller: relayerA, msgValue: 0, bridgeTx: tokenTx}); checkRelayedViews({txId: txId, expectedRelayer: relayerA}); - assertEq(dstToken.balanceOf(userB), tokenParams.destAmount); - assertEq(dstToken.balanceOf(relayerA), LEFTOVER_BALANCE); - assertEq(dstToken.balanceOf(address(fastBridge)), 0); + checkTokenBalances({recipient: userB, relayCaller: relayerA}); } /// @notice RelayerB completes the ERC20 bridge request, using relayerA's address @@ -106,9 +119,7 @@ contract FastBridgeV2DstTest is FastBridgeV2DstBaseTest { expectBridgeRelayed(tokenTx, txId, relayerA); relayWithAddress({caller: relayerB, relayer: relayerA, msgValue: 0, bridgeTx: tokenTx}); checkRelayedViews({txId: txId, expectedRelayer: relayerA}); - assertEq(dstToken.balanceOf(userB), tokenParams.destAmount); - assertEq(dstToken.balanceOf(relayerB), LEFTOVER_BALANCE); - assertEq(dstToken.balanceOf(address(fastBridge)), 0); + checkTokenBalances({recipient: userB, relayCaller: relayerB}); } /// @notice RelayerB completes the ETH bridge request @@ -117,9 +128,7 @@ contract FastBridgeV2DstTest is FastBridgeV2DstBaseTest { expectBridgeRelayed(ethTx, txId, relayerB); relay({caller: relayerB, msgValue: ethParams.destAmount, bridgeTx: ethTx}); checkRelayedViews({txId: txId, expectedRelayer: relayerB}); - assertEq(userB.balance, ethParams.destAmount); - assertEq(relayerB.balance, LEFTOVER_BALANCE); - assertEq(address(fastBridge).balance, 0); + checkEthBalances({recipient: userB, relayCaller: relayerB}); } /// @notice RelayerA completes the ETH bridge request, using relayerB's address @@ -128,9 +137,7 @@ contract FastBridgeV2DstTest is FastBridgeV2DstBaseTest { expectBridgeRelayed(ethTx, txId, relayerB); relayWithAddress({caller: relayerA, relayer: relayerB, msgValue: ethParams.destAmount, bridgeTx: ethTx}); checkRelayedViews({txId: txId, expectedRelayer: relayerB}); - assertEq(userB.balance, ethParams.destAmount); - assertEq(relayerA.balance, LEFTOVER_BALANCE); - assertEq(address(fastBridge).balance, 0); + checkEthBalances({recipient: userB, relayCaller: relayerA}); } /// @notice RelayerA completes the ETH bridge request, using relayerB's address @@ -146,9 +153,29 @@ contract FastBridgeV2DstTest is FastBridgeV2DstBaseTest { assertEq(recordedBlockNumber, 987_654_321); assertEq(recordedBlockTimestamp, 123_456_789); assertEq(recordedRelayer, relayerB); - assertEq(userB.balance, ethParams.destAmount); - assertEq(relayerA.balance, LEFTOVER_BALANCE); - assertEq(address(fastBridge).balance, 0); + checkEthBalances({recipient: userB, relayCaller: relayerA}); + } + + // ══════════════════════════════════════════ RELAYS WITH CALL VALUE ═══════════════════════════════════════════════ + + function test_relay_token_withCallValue() public { + setTokenTestCallValue(CALL_VALUE); + bytes32 txId = getTxId(tokenTx); + expectBridgeRelayed(tokenTx, txId, relayerA); + relay({caller: relayerA, msgValue: CALL_VALUE, bridgeTx: tokenTx}); + checkRelayedViews({txId: txId, expectedRelayer: relayerA}); + checkTokenBalances({recipient: userB, relayCaller: relayerA}); + assertEq(userB.balance, CALL_VALUE); + } + + function test_relay_token_withRelayerAddressCallValue() public { + setTokenTestCallValue(CALL_VALUE); + bytes32 txId = getTxId(tokenTx); + expectBridgeRelayed(tokenTx, txId, relayerA); + relayWithAddress({caller: relayerB, relayer: relayerA, msgValue: CALL_VALUE, bridgeTx: tokenTx}); + checkRelayedViews({txId: txId, expectedRelayer: relayerA}); + checkTokenBalances({recipient: userB, relayCaller: relayerB}); + assertEq(userB.balance, CALL_VALUE); } // ═════════════════════════════════════ EXCESSIVE RETURN VALUE RECIPIENT ══════════════════════════════════════════ @@ -171,6 +198,26 @@ contract FastBridgeV2DstTest is FastBridgeV2DstBaseTest { test_relay_token_withRelayerAddress(); } + function test_relay_token_withCallValue_excessiveReturnValueRecipient_revertWhenCallParamsPresent() + public + virtual + { + assertEmptyCallParams(tokenTx.callParams); + setTokenTestCallValue(CALL_VALUE); + setTokenTestRecipient(excessiveReturnValueRecipient); + test_relay_token_withCallValue(); + } + + function test_relay_token_withRelayerAddressCallValue_excessiveReturnValueRecipient_revertWhenCallParamsPresent() + public + virtual + { + assertEmptyCallParams(tokenTx.callParams); + setTokenTestCallValue(CALL_VALUE); + setTokenTestRecipient(excessiveReturnValueRecipient); + test_relay_token_withRelayerAddressCallValue(); + } + function test_relay_eth_excessiveReturnValueRecipient_revertWhenCallParamsPresent() public virtual { assertEmptyCallParams(ethTx.callParams); setEthTestRecipient(excessiveReturnValueRecipient); @@ -206,6 +253,26 @@ contract FastBridgeV2DstTest is FastBridgeV2DstBaseTest { test_relay_token_withRelayerAddress(); } + function test_relay_token_withCallValue_incorrectReturnValueRecipient_revertWhenCallParamsPresent() + public + virtual + { + assertEmptyCallParams(tokenTx.callParams); + setTokenTestCallValue(CALL_VALUE); + setTokenTestRecipient(incorrectReturnValueRecipient); + test_relay_token_withCallValue(); + } + + function test_relay_token_withRelayerAddressCallValue_incorrectReturnValueRecipient_revertWhenCallParamsPresent() + public + virtual + { + assertEmptyCallParams(tokenTx.callParams); + setTokenTestCallValue(CALL_VALUE); + setTokenTestRecipient(incorrectReturnValueRecipient); + test_relay_token_withRelayerAddressCallValue(); + } + function test_relay_eth_incorrectReturnValueRecipient_revertWhenCallParamsPresent() public virtual { assertEmptyCallParams(ethTx.callParams); setEthTestRecipient(incorrectReturnValueRecipient); @@ -234,6 +301,20 @@ contract FastBridgeV2DstTest is FastBridgeV2DstBaseTest { test_relay_token_withRelayerAddress(); } + function test_relay_token_withCallValue_nonPayableRecipient_revert() public { + setTokenTestCallValue(CALL_VALUE); + setTokenTestRecipient(nonPayableRecipient); + vm.expectRevert(); + relay({caller: relayerA, msgValue: CALL_VALUE, bridgeTx: tokenTx}); + } + + function test_relay_token_withRelayerAddressCallValue_nonPayableRecipient_revert() public { + setTokenTestCallValue(CALL_VALUE); + setTokenTestRecipient(nonPayableRecipient); + vm.expectRevert(); + relayWithAddress({caller: relayerB, relayer: relayerA, msgValue: CALL_VALUE, bridgeTx: tokenTx}); + } + function test_relay_eth_revert_nonPayableRecipient() public { setEthTestRecipient(nonPayableRecipient); vm.expectRevert(); @@ -263,6 +344,20 @@ contract FastBridgeV2DstTest is FastBridgeV2DstBaseTest { test_relay_token_withRelayerAddress(); } + function test_relay_token_withCallValue_noOpRecipient_revertWhenCallParamsPresent() public virtual { + assertEmptyCallParams(tokenTx.callParams); + setTokenTestCallValue(CALL_VALUE); + setTokenTestRecipient(noOpRecipient); + test_relay_token_withCallValue(); + } + + function test_relay_token_withRelayerAddressCallValue_noOpRecipient_revertWhenCallParamsPresent() public virtual { + assertEmptyCallParams(tokenTx.callParams); + setTokenTestCallValue(CALL_VALUE); + setTokenTestRecipient(noOpRecipient); + test_relay_token_withRelayerAddressCallValue(); + } + function test_relay_eth_noOpRecipient_revertWhenCallParamsPresent() public virtual { assertEmptyCallParams(ethTx.callParams); setEthTestRecipient(noOpRecipient); @@ -292,6 +387,23 @@ contract FastBridgeV2DstTest is FastBridgeV2DstBaseTest { test_relay_token_withRelayerAddress(); } + function test_relay_token_withCallValue_noReturnValueRecipient_revertWhenCallParamsPresent() public virtual { + assertEmptyCallParams(tokenTx.callParams); + setTokenTestCallValue(CALL_VALUE); + setTokenTestRecipient(noReturnValueRecipient); + test_relay_token_withCallValue(); + } + + function test_relay_token_withRelayerAddressCallValue_noReturnValueRecipient_revertWhenCallParamsPresent() + public + virtual + { + assertEmptyCallParams(tokenTx.callParams); + setTokenTestCallValue(CALL_VALUE); + setTokenTestRecipient(noReturnValueRecipient); + test_relay_token_withRelayerAddressCallValue(); + } + function test_relay_eth_noReturnValueRecipient_revertWhenCallParamsPresent() public virtual { assertEmptyCallParams(ethTx.callParams); setEthTestRecipient(noReturnValueRecipient); @@ -361,6 +473,69 @@ contract FastBridgeV2DstTest is FastBridgeV2DstBaseTest { relayWithAddress({caller: relayerA, relayer: address(0), msgValue: 0, bridgeTx: tokenTx}); } + function test_relay_token_withCallValue_revert_zeroCallValue() public { + setTokenTestCallValue(CALL_VALUE); + vm.expectRevert(MsgValueIncorrect.selector); + relay({caller: relayerA, msgValue: 0, bridgeTx: tokenTx}); + } + + function test_relay_token_withCallValue_revert_lowerCallValue() public { + setTokenTestCallValue(CALL_VALUE); + vm.expectRevert(MsgValueIncorrect.selector); + relay({caller: relayerA, msgValue: CALL_VALUE - 1, bridgeTx: tokenTx}); + } + + function test_relay_token_withCallValue_revert_higherCallValue() public { + setTokenTestCallValue(CALL_VALUE); + vm.expectRevert(MsgValueIncorrect.selector); + relay({caller: relayerA, msgValue: CALL_VALUE + 1, bridgeTx: tokenTx}); + } + + function test_relay_token_withRelayerAddressCallValue_revert_zeroCallValue() public { + setTokenTestCallValue(CALL_VALUE); + vm.expectRevert(MsgValueIncorrect.selector); + relayWithAddress({caller: relayerB, relayer: relayerA, msgValue: 0, bridgeTx: tokenTx}); + } + + function test_relay_token_withRelayerAddressCallValue_revert_lowerCallValue() public { + setTokenTestCallValue(CALL_VALUE); + vm.expectRevert(MsgValueIncorrect.selector); + relayWithAddress({caller: relayerB, relayer: relayerA, msgValue: CALL_VALUE - 1, bridgeTx: tokenTx}); + } + + function test_relay_token_withRelayerAddressCallValue_revert_higherCallValue() public { + setTokenTestCallValue(CALL_VALUE); + vm.expectRevert(MsgValueIncorrect.selector); + relayWithAddress({caller: relayerB, relayer: relayerA, msgValue: CALL_VALUE + 1, bridgeTx: tokenTx}); + } + + function test_relay_eth_withCallValue_revert_notSupported() public { + setEthTestCallValue(CALL_VALUE); + // Neither destAmount, CALL_VALUE, nor destAmount + CALL_VALUE should work here + vm.expectRevert(NativeTokenCallValueNotSupported.selector); + relay({caller: relayerB, msgValue: CALL_VALUE, bridgeTx: ethTx}); + vm.expectRevert(NativeTokenCallValueNotSupported.selector); + relay({caller: relayerB, msgValue: ethParams.destAmount, bridgeTx: ethTx}); + vm.expectRevert(NativeTokenCallValueNotSupported.selector); + relay({caller: relayerB, msgValue: ethParams.destAmount + CALL_VALUE, bridgeTx: ethTx}); + } + + function test_relay_eth_withRelayerAddressCallValue_revert_notSupported() public { + setEthTestCallValue(CALL_VALUE); + // Neither destAmount, CALL_VALUE, nor destAmount + CALL_VALUE should work here + vm.expectRevert(NativeTokenCallValueNotSupported.selector); + relayWithAddress({caller: relayerA, relayer: relayerB, msgValue: CALL_VALUE, bridgeTx: ethTx}); + vm.expectRevert(NativeTokenCallValueNotSupported.selector); + relayWithAddress({caller: relayerA, relayer: relayerB, msgValue: ethParams.destAmount, bridgeTx: ethTx}); + vm.expectRevert(NativeTokenCallValueNotSupported.selector); + relayWithAddress({ + caller: relayerA, + relayer: relayerB, + msgValue: ethParams.destAmount + CALL_VALUE, + bridgeTx: ethTx + }); + } + function test_relay_token_revert_approvedZero() public { vm.prank(relayerA); dstToken.approve(address(fastBridge), 0); @@ -380,18 +555,52 @@ contract FastBridgeV2DstTest is FastBridgeV2DstBaseTest { relay({caller: relayerA, msgValue: tokenParams.destAmount, bridgeTx: tokenTx}); } + function test_relay_token_withRelayerAddress_revert_approvedZero() public { + vm.prank(relayerB); + dstToken.approve(address(fastBridge), 0); + vm.expectRevert(); + relayWithAddress({caller: relayerB, relayer: relayerA, msgValue: 0, bridgeTx: tokenTx}); + } + + function test_relay_token_withRelayerAddress_revert_approvedNotEnough() public { + vm.prank(relayerB); + dstToken.approve(address(fastBridge), tokenParams.destAmount - 1); + vm.expectRevert(); + relayWithAddress({caller: relayerB, relayer: relayerA, msgValue: 0, bridgeTx: tokenTx}); + } + + function test_relay_token_withRelayerAddress_revert_nonZeroMsgValue() public { + vm.expectRevert(); + relayWithAddress({caller: relayerB, relayer: relayerA, msgValue: tokenParams.destAmount, bridgeTx: tokenTx}); + } + function test_relay_eth_revert_lowerMsgValue() public { vm.expectRevert(); - relay({caller: relayerA, msgValue: ethParams.destAmount - 1, bridgeTx: ethTx}); + relay({caller: relayerB, msgValue: ethParams.destAmount - 1, bridgeTx: ethTx}); } function test_relay_eth_revert_higherMsgValue() public { vm.expectRevert(); - relay({caller: relayerA, msgValue: ethParams.destAmount + 1, bridgeTx: ethTx}); + relay({caller: relayerB, msgValue: ethParams.destAmount + 1, bridgeTx: ethTx}); } function test_relay_eth_revert_zeroMsgValue() public { vm.expectRevert(); - relay({caller: relayerA, msgValue: 0, bridgeTx: ethTx}); + relay({caller: relayerB, msgValue: 0, bridgeTx: ethTx}); + } + + function test_relay_eth_withRelayerAddress_revert_lowerMsgValue() public { + vm.expectRevert(); + relayWithAddress({caller: relayerA, relayer: relayerB, msgValue: ethParams.destAmount - 1, bridgeTx: ethTx}); + } + + function test_relay_eth_withRelayerAddress_revert_higherMsgValue() public { + vm.expectRevert(); + relayWithAddress({caller: relayerA, relayer: relayerB, msgValue: ethParams.destAmount + 1, bridgeTx: ethTx}); + } + + function test_relay_eth_withRelayerAddress_revert_zeroMsgValue() public { + vm.expectRevert(); + relayWithAddress({caller: relayerA, relayer: relayerB, msgValue: 0, bridgeTx: ethTx}); } } diff --git a/packages/contracts-rfq/test/FastBridgeV2.Encoding.t.sol b/packages/contracts-rfq/test/FastBridgeV2.Encoding.t.sol index 0918c66bf5..21d32876fc 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.Encoding.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.Encoding.t.sol @@ -47,17 +47,15 @@ contract FastBridgeV2EncodingTest is FastBridgeV2Test { assertEq(decodedTx, bridgeTx); } - // The addition of variable length field (callParams) in BridgeTransactionV2 breaks the compatibility - // with the original BridgeTransaction struct. - // Solidity's abi.encode(bridgeTxV2) will use the first 32 bytes to encode the data offset for the whole struct, - // which is ALWAYS equal to 32 (data starts right after the offset). This is weird, but it is what it is. - // https://ethereum.stackexchange.com/questions/152971/abi-encode-decode-mystery-additional-32-byte-field-uniswap-v2 - function test_getBridgeTransaction_supportsV2(IFastBridgeV2.BridgeTransactionV2 memory bridgeTxV2) public { - // TODO: reevaluate the necessity of this test if/when the encoding scheme is changed - vm.skip(true); + /// @notice We expect all the V1 fields except for `sendChainGas` to match. + /// `sendChainGas` is replaced with `callValue` in V2, therefore we expect non-zero `callValue` + /// to match `sendChainGas = true` in V1 + function test_getBridgeTransaction_supportsV2(IFastBridgeV2.BridgeTransactionV2 memory bridgeTxV2) public view { bytes memory request = abi.encode(bridgeTxV2); IFastBridge.BridgeTransaction memory decodedTx = fastBridge.getBridgeTransaction(request); - assertEq(decodedTx, extractV1(bridgeTxV2)); + IFastBridge.BridgeTransaction memory expectedTx = extractV1(bridgeTxV2); + expectedTx.sendChainGas = bridgeTxV2.callValue > 0; + assertEq(decodedTx, expectedTx); } function test_getBridgeTransactionV2(IFastBridgeV2.BridgeTransactionV2 memory bridgeTxV2) public view { diff --git a/packages/contracts-rfq/test/FastBridgeV2.GasBench.Dst.ArbitraryCall.t.sol b/packages/contracts-rfq/test/FastBridgeV2.GasBench.Dst.ArbitraryCall.t.sol new file mode 100644 index 0000000000..55958e937f --- /dev/null +++ b/packages/contracts-rfq/test/FastBridgeV2.GasBench.Dst.ArbitraryCall.t.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {FastBridgeV2GasBenchmarkDstTest} from "./FastBridgeV2.GasBench.Dst.t.sol"; +import {RecipientMock} from "./mocks/RecipientMock.sol"; + +// solhint-disable func-name-mixedcase, no-empty-blocks +contract FastBridgeV2GasBenchmarkDstArbitraryCallTest is FastBridgeV2GasBenchmarkDstTest { + // To get an idea about how much overhead the arbitrary call adds to the relaying process, we use a mock + // recipient that has the hook function implemented as a no-op. + // The mocked callParams are chosen to be similar to the real use cases: + // - user address + // - some kind of ID to decide what to do with the tokens next + + /// @notice We include an empty "test" function so that this contract does not appear in the coverage report. + function testFastBridgeV2GasBenchmarkDstArbitraryCallTest() external {} + + function setUp() public virtual override { + // In the inherited tests userB is always used as the recipient of the tokens. + userB = address(new RecipientMock()); + vm.label(userB, "ContractRecipient"); + super.setUp(); + } + + function createFixturesV2() public virtual override { + super.createFixturesV2(); + bytes memory mockCallParams = abi.encode(userA, keccak256("Random ID")); + setTokenTestCallParams(mockCallParams); + setEthTestCallParams(mockCallParams); + } +} diff --git a/packages/contracts-rfq/test/FastBridgeV2.GasBench.Dst.Excl.t.sol b/packages/contracts-rfq/test/FastBridgeV2.GasBench.Dst.Excl.t.sol index eae0dffaa1..970f51465b 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.GasBench.Dst.Excl.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.GasBench.Dst.Excl.t.sol @@ -1,18 +1,22 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import {FastBridgeV2DstGasBenchmarkTest} from "./FastBridgeV2.GasBench.Dst.t.sol"; +import {FastBridgeV2GasBenchmarkDstTest} from "./FastBridgeV2.GasBench.Dst.t.sol"; -// solhint-disable func-name-mixedcase, ordering -contract FastBridgeV2DstExclusivityTest is FastBridgeV2DstGasBenchmarkTest { +// solhint-disable func-name-mixedcase, no-empty-blocks +contract FastBridgeV2GasBenchmarkDstExclusivityTest is FastBridgeV2GasBenchmarkDstTest { uint256 public constant EXCLUSIVITY_PERIOD = 60 seconds; + /// @notice We include an empty "test" function so that this contract does not appear in the coverage report. + function testFastBridgeV2GasBenchmarkDstExclusivityTest() external {} + function setUp() public virtual override { super.setUp(); skip({time: EXCLUSIVITY_PERIOD / 2}); } function createFixturesV2() public virtual override { + super.createFixturesV2(); setTokenTestExclusivityParams(relayerA, EXCLUSIVITY_PERIOD); setEthTestExclusivityParams(relayerA, EXCLUSIVITY_PERIOD); } diff --git a/packages/contracts-rfq/test/FastBridgeV2.GasBench.Dst.t.sol b/packages/contracts-rfq/test/FastBridgeV2.GasBench.Dst.t.sol index 6afabce02e..8c739ad14b 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.GasBench.Dst.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.GasBench.Dst.t.sol @@ -6,7 +6,7 @@ import {FastBridgeV2DstBaseTest} from "./FastBridgeV2.Dst.Base.t.sol"; // solhint-disable func-name-mixedcase, ordering /// @notice This test is used to estimate the gas cost of FastBridgeV2 destination chain functions. /// Very little state checks are performed, make sure to do full coverage in different tests. -contract FastBridgeV2DstGasBenchmarkTest is FastBridgeV2DstBaseTest { +contract FastBridgeV2GasBenchmarkDstTest is FastBridgeV2DstBaseTest { uint256 public constant INITIAL_USER_BALANCE = 100 ether; function mintTokens() public virtual override { diff --git a/packages/contracts-rfq/test/FastBridgeV2.GasBench.Encoding.t.sol b/packages/contracts-rfq/test/FastBridgeV2.GasBench.Encoding.t.sol index 703e73267b..e8bdb2a8b9 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.GasBench.Encoding.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.GasBench.Encoding.t.sol @@ -4,9 +4,7 @@ pragma solidity ^0.8.20; import {FastBridgeV2SrcBaseTest} from "./FastBridgeV2.Src.Base.t.sol"; // solhint-disable func-name-mixedcase, ordering -contract FastBridgeV2GasBenchmarkSrcProtocolFeesTest is FastBridgeV2SrcBaseTest { - // TODO: add more tests with variable length requests once arbitrary call is done - +contract FastBridgeV2GasBenchmarkEncodingTest is FastBridgeV2SrcBaseTest { function test_getBridgeTransaction() public view { bytes memory request = abi.encode(extractV1(tokenTx)); fastBridge.getBridgeTransaction(request); @@ -16,4 +14,9 @@ contract FastBridgeV2GasBenchmarkSrcProtocolFeesTest is FastBridgeV2SrcBaseTest bytes memory request = abi.encode(tokenTx); fastBridge.getBridgeTransactionV2(request); } + + function test_getBridgeTransactionV2_withArbitraryCall() public { + setTokenTestCallParams({callParams: abi.encode(userA, keccak256("Random ID"))}); + test_getBridgeTransactionV2(); + } } diff --git a/packages/contracts-rfq/test/FastBridgeV2.GasBench.Src.ArbitraryCall.t.sol b/packages/contracts-rfq/test/FastBridgeV2.GasBench.Src.ArbitraryCall.t.sol new file mode 100644 index 0000000000..d9291b8208 --- /dev/null +++ b/packages/contracts-rfq/test/FastBridgeV2.GasBench.Src.ArbitraryCall.t.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {FastBridgeV2GasBenchmarkSrcTest} from "./FastBridgeV2.GasBench.Src.t.sol"; + +// solhint-disable func-name-mixedcase, no-empty-blocks +contract FastBridgeV2GasBenchmarkSrcArbitraryCallTest is FastBridgeV2GasBenchmarkSrcTest { + /// @notice We include an empty "test" function so that this contract does not appear in the coverage report. + function testFastBridgeV2GasBenchmarkSrcArbitraryCallTest() external {} + + function createFixturesV2() public virtual override { + super.createFixturesV2(); + bytes memory mockCallParams = abi.encode(userA, keccak256("Random ID")); + setTokenTestCallParams(mockCallParams); + setEthTestCallParams(mockCallParams); + bridgedTokenTx.callParams = mockCallParams; + bridgedEthTx.callParams = mockCallParams; + provenTokenTx.callParams = mockCallParams; + provenEthTx.callParams = mockCallParams; + } +} diff --git a/packages/contracts-rfq/test/FastBridgeV2.GasBench.Src.PFees.t.sol b/packages/contracts-rfq/test/FastBridgeV2.GasBench.Src.PFees.t.sol index 77b439d18e..790db012f6 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.GasBench.Src.PFees.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.GasBench.Src.PFees.t.sol @@ -3,8 +3,11 @@ pragma solidity ^0.8.20; import {FastBridgeV2GasBenchmarkSrcTest} from "./FastBridgeV2.GasBench.Src.t.sol"; -// solhint-disable func-name-mixedcase, ordering +// solhint-disable func-name-mixedcase, no-empty-blocks contract FastBridgeV2GasBenchmarkSrcProtocolFeesTest is FastBridgeV2GasBenchmarkSrcTest { + /// @notice We include an empty "test" function so that this contract does not appear in the coverage report. + function testFastBridgeV2GasBenchmarkSrcProtocolFeesTest() external {} + function configureFastBridge() public virtual override { super.configureFastBridge(); fastBridge.grantRole(fastBridge.GOVERNOR_ROLE(), address(this)); @@ -27,10 +30,10 @@ contract FastBridgeV2GasBenchmarkSrcProtocolFeesTest is FastBridgeV2GasBenchmark provenTokenTx = tokenTx; bridgedEthTx = ethTx; provenEthTx = ethTx; - - bridgedTokenTx.nonce = 0; - bridgedEthTx.nonce = 1; - provenTokenTx.nonce = 2; - provenEthTx.nonce = 3; + // See FastBridgeV2GasBenchmarkSrcTest.initExistingTxs for why these start from 1, not 0 + bridgedTokenTx.nonce = 1; + bridgedEthTx.nonce = 2; + provenTokenTx.nonce = 3; + provenEthTx.nonce = 4; } } diff --git a/packages/contracts-rfq/test/FastBridgeV2.GasBench.Src.t.sol b/packages/contracts-rfq/test/FastBridgeV2.GasBench.Src.t.sol index 0b6834d6ff..940eae3aa1 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.GasBench.Src.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.GasBench.Src.t.sol @@ -38,13 +38,14 @@ contract FastBridgeV2GasBenchmarkSrcTest is FastBridgeV2SrcBaseTest { bridgedEthTx = ethTx; provenEthTx = ethTx; - bridgedTokenTx.nonce = 0; - bridgedEthTx.nonce = 1; - provenTokenTx.nonce = 2; - provenEthTx.nonce = 3; - // Next nonce for userA tx would be 4 (either token or eth) - tokenTx.nonce = 4; - ethTx.nonce = 4; + // See initExistingTxs for why these start from 1, not 0 + bridgedTokenTx.nonce = 1; + bridgedEthTx.nonce = 2; + provenTokenTx.nonce = 3; + provenEthTx.nonce = 4; + // Next nonce for userA tx would be 5 (either token or eth) + tokenTx.nonce = 5; + ethTx.nonce = 5; } function createFixturesV2() public virtual override { @@ -67,10 +68,13 @@ contract FastBridgeV2GasBenchmarkSrcTest is FastBridgeV2SrcBaseTest { } function initExistingTxs() public { - bridge({caller: userA, msgValue: 0, params: tokenParams}); - bridge({caller: userA, msgValue: ethParams.originAmount, params: ethParams}); - bridge({caller: userA, msgValue: 0, params: tokenParams}); - bridge({caller: userA, msgValue: ethParams.originAmount, params: ethParams}); + // Set userA nonce to 1 so that the first bridge tx doesn't have inflated gas costs due to + // the storage write from the zero initial value + cheatSenderNonce(userA, 1); + bridge({caller: userA, msgValue: 0, params: tokenParams, paramsV2: tokenParamsV2}); + bridge({caller: userA, msgValue: ethParams.originAmount, params: ethParams, paramsV2: ethParamsV2}); + bridge({caller: userA, msgValue: 0, params: tokenParams, paramsV2: tokenParamsV2}); + bridge({caller: userA, msgValue: ethParams.originAmount, params: ethParams, paramsV2: ethParamsV2}); skipBlocksExactly(1); prove({caller: relayerA, bridgeTx: provenTokenTx, destTxHash: hex"01"}); prove({caller: relayerB, transactionId: getTxId(provenEthTx), destTxHash: hex"02", relayer: relayerA}); @@ -96,19 +100,21 @@ contract FastBridgeV2GasBenchmarkSrcTest is FastBridgeV2SrcBaseTest { // ═══════════════════════════════════════════════════ TOKEN ═══════════════════════════════════════════════════════ - function test_bridge_token() public { - bridge({caller: userA, msgValue: 0, params: tokenParams}); + function checkAfterBridgeToken() public view { assertEq(fastBridge.bridgeStatuses(getTxId(tokenTx)), IFastBridgeV2.BridgeStatus.REQUESTED); assertEq(srcToken.balanceOf(userA), initialUserBalanceToken - tokenParams.originAmount); assertEq(srcToken.balanceOf(address(fastBridge)), initialFastBridgeBalanceToken + tokenParams.originAmount); } + function test_bridge_token() public { + bridge({caller: userA, msgValue: 0, params: tokenParams, paramsV2: tokenParamsV2}); + checkAfterBridgeToken(); + } + function test_bridge_token_withExclusivity() public { setTokenTestExclusivityParams(relayerA, EXCLUSIVITY_PERIOD); bridge({caller: userA, msgValue: 0, params: tokenParams, paramsV2: tokenParamsV2}); - assertEq(fastBridge.bridgeStatuses(getTxId(tokenTx)), IFastBridgeV2.BridgeStatus.REQUESTED); - assertEq(srcToken.balanceOf(userA), initialUserBalanceToken - tokenParams.originAmount); - assertEq(srcToken.balanceOf(address(fastBridge)), initialFastBridgeBalanceToken + tokenParams.originAmount); + checkAfterBridgeToken(); } function test_prove_token() public { @@ -177,19 +183,21 @@ contract FastBridgeV2GasBenchmarkSrcTest is FastBridgeV2SrcBaseTest { // ════════════════════════════════════════════════════ ETH ════════════════════════════════════════════════════════ - function test_bridge_eth() public { - bridge({caller: userA, msgValue: ethParams.originAmount, params: ethParams}); + function checkAfterBridgeEth() public view { assertEq(fastBridge.bridgeStatuses(getTxId(ethTx)), IFastBridgeV2.BridgeStatus.REQUESTED); assertEq(userA.balance, initialUserBalanceEth - ethParams.originAmount); assertEq(address(fastBridge).balance, initialFastBridgeBalanceEth + ethParams.originAmount); } + function test_bridge_eth() public { + bridge({caller: userA, msgValue: ethParams.originAmount, params: ethParams, paramsV2: ethParamsV2}); + checkAfterBridgeEth(); + } + function test_bridge_eth_withExclusivity() public { setEthTestExclusivityParams(relayerA, EXCLUSIVITY_PERIOD); bridge({caller: userA, msgValue: ethParams.originAmount, params: ethParams, paramsV2: ethParamsV2}); - assertEq(fastBridge.bridgeStatuses(getTxId(ethTx)), IFastBridgeV2.BridgeStatus.REQUESTED); - assertEq(userA.balance, initialUserBalanceEth - ethParams.originAmount); - assertEq(address(fastBridge).balance, initialFastBridgeBalanceEth + ethParams.originAmount); + checkAfterBridgeEth(); } function test_prove_eth() public { diff --git a/packages/contracts-rfq/test/FastBridgeV2.Src.ArbitraryCall.t.sol b/packages/contracts-rfq/test/FastBridgeV2.Src.ArbitraryCall.t.sol index 2b1e83a30c..5b6e84b732 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.Src.ArbitraryCall.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.Src.ArbitraryCall.t.sol @@ -6,6 +6,7 @@ import {FastBridgeV2SrcExclusivityTest} from "./FastBridgeV2.Src.Exclusivity.t.s // solhint-disable func-name-mixedcase, ordering contract FastBridgeV2SrcArbitraryCallTest is FastBridgeV2SrcExclusivityTest { bytes public constant CALL_PARAMS = abi.encode("Hello, World!"); + uint256 public constant CALL_VALUE = 1_337_420; function createFixturesV2() public virtual override { super.createFixturesV2(); @@ -41,4 +42,56 @@ contract FastBridgeV2SrcArbitraryCallTest is FastBridgeV2SrcExclusivityTest { vm.expectRevert(CallParamsLengthAboveMax.selector); bridge({caller: userA, msgValue: ethParams.originAmount, params: ethParams, paramsV2: ethParamsV2}); } + + // ══════════════════════════════════════ WITH CALL VALUE, NO CALL PARAMS ══════════════════════════════════════════ + + function test_bridge_token_withCallValue_noCallParams() public { + setTokenTestCallParams(""); + setTokenTestCallValue(CALL_VALUE); + test_bridge_token(); + } + + function test_bridge_token_diffSender_withCallValue_noCallParams() public { + setTokenTestCallParams(""); + setTokenTestCallValue(CALL_VALUE); + test_bridge_token_diffSender(); + } + + function test_bridge_eth_withCallValue_noCallParams_revert() public { + setEthTestCallParams(""); + setEthTestCallValue(CALL_VALUE); + vm.expectRevert(NativeTokenCallValueNotSupported.selector); + bridge({caller: userA, msgValue: ethParams.originAmount, params: ethParams, paramsV2: ethParamsV2}); + } + + function test_bridge_eth_diffSender_withCallValue_noCallParams_revert() public { + setEthTestCallParams(""); + setEthTestCallValue(CALL_VALUE); + vm.expectRevert(NativeTokenCallValueNotSupported.selector); + bridge({caller: userB, msgValue: ethParams.originAmount, params: ethParams, paramsV2: ethParamsV2}); + } + + // ═══════════════════════════════════════ WITH CALL VALUE & CALL PARAMS ═══════════════════════════════════════════ + + function test_bridge_token_withCallValue_withCallParams() public { + setTokenTestCallValue(CALL_VALUE); + test_bridge_token(); + } + + function test_bridge_token_diffSender_withCallValue_withCallParams() public { + setTokenTestCallValue(CALL_VALUE); + test_bridge_token_diffSender(); + } + + function test_bridge_eth_withCallValue_withCallParams_revert() public { + setEthTestCallValue(CALL_VALUE); + vm.expectRevert(NativeTokenCallValueNotSupported.selector); + bridge({caller: userA, msgValue: ethParams.originAmount, params: ethParams, paramsV2: ethParamsV2}); + } + + function test_bridge_eth_diffSender_withCallValue_withCallParams_revert() public { + setEthTestCallValue(CALL_VALUE); + vm.expectRevert(NativeTokenCallValueNotSupported.selector); + bridge({caller: userB, msgValue: ethParams.originAmount, params: ethParams, paramsV2: ethParamsV2}); + } } diff --git a/packages/contracts-rfq/test/FastBridgeV2.Src.ProtocolFees.t.sol b/packages/contracts-rfq/test/FastBridgeV2.Src.ProtocolFees.t.sol index d9a5324ed1..775ff257e5 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.Src.ProtocolFees.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.Src.ProtocolFees.t.sol @@ -3,8 +3,11 @@ pragma solidity ^0.8.20; import {FastBridgeV2SrcTest} from "./FastBridgeV2.Src.t.sol"; -// solhint-disable func-name-mixedcase, ordering +// solhint-disable func-name-mixedcase, no-empty-blocks contract FastBridgeV2SrcProtocolFeesTest is FastBridgeV2SrcTest { + /// @notice We include an empty "test" function so that this contract does not appear in the coverage report. + function testFastBridgeV2SrcProtocolFeesTest() external {} + function configureFastBridge() public virtual override { super.configureFastBridge(); fastBridge.grantRole(fastBridge.GOVERNOR_ROLE(), address(this)); diff --git a/packages/contracts-rfq/test/FastBridgeV2.Src.t.sol b/packages/contracts-rfq/test/FastBridgeV2.Src.t.sol index 3a9edd328c..52315635dd 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.Src.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.Src.t.sol @@ -42,7 +42,7 @@ contract FastBridgeV2SrcTest is FastBridgeV2SrcBaseTest { destToken: bridgeTx.destToken, originAmount: bridgeTx.originAmount, destAmount: bridgeTx.destAmount, - sendChainGas: bridgeTx.sendChainGas + sendChainGas: bridgeTx.callValue > 0 }); } @@ -74,10 +74,9 @@ contract FastBridgeV2SrcTest is FastBridgeV2SrcBaseTest { }); } - function expectBridgeProofDisputed(bytes32 txId, address guard) public { + function expectBridgeProofDisputed(bytes32 txId, address relayer) public { vm.expectEmit(address(fastBridge)); - // Note: BridgeProofDisputed event has a mislabeled address parameter, this is actually the guard - emit BridgeProofDisputed({transactionId: txId, relayer: guard}); + emit BridgeProofDisputed({transactionId: txId, relayer: relayer}); } function expectBridgeDepositRefunded(IFastBridge.BridgeParams memory bridgeParams, bytes32 txId) public { @@ -384,11 +383,11 @@ contract FastBridgeV2SrcTest is FastBridgeV2SrcBaseTest { bridge({caller: userA, msgValue: ethParams.originAmount, params: ethParams}); expectBridgeProofProvided({txId: txId, relayer: relayerA, destTxHash: hex"01"}); prove({caller: relayerB, transactionId: txId, destTxHash: hex"01", relayer: relayerA}); - expectBridgeProofDisputed(txId, guard); + expectBridgeProofDisputed(txId, relayerA); dispute(guard, txId); expectBridgeProofProvided({txId: txId, relayer: relayerA, destTxHash: hex"02"}); prove({caller: relayerB, transactionId: txId, destTxHash: hex"02", relayer: relayerA}); - expectBridgeProofDisputed(txId, guard); + expectBridgeProofDisputed(txId, relayerA); dispute(guard, txId); expectBridgeProofProvided({txId: txId, relayer: relayerA, destTxHash: hex"03"}); prove({caller: relayerB, transactionId: txId, destTxHash: hex"03", relayer: relayerA}); @@ -660,7 +659,7 @@ contract FastBridgeV2SrcTest is FastBridgeV2SrcBaseTest { bytes32 txId = getTxId(tokenTx); bridge({caller: userA, msgValue: 0, params: tokenParams}); prove({caller: relayerA, bridgeTx: tokenTx, destTxHash: hex"01"}); - expectBridgeProofDisputed({txId: txId, guard: guard}); + expectBridgeProofDisputed({txId: txId, relayer: relayerA}); dispute({caller: guard, txId: txId}); assertEq(fastBridge.bridgeStatuses(txId), IFastBridgeV2.BridgeStatus.REQUESTED); assertEq(fastBridge.protocolFees(address(srcToken)), INITIAL_PROTOCOL_FEES_TOKEN); @@ -672,7 +671,7 @@ contract FastBridgeV2SrcTest is FastBridgeV2SrcBaseTest { bridge({caller: userA, msgValue: 0, params: tokenParams}); prove({caller: relayerA, bridgeTx: tokenTx, destTxHash: hex"01"}); skip(CLAIM_DELAY); - expectBridgeProofDisputed({txId: txId, guard: guard}); + expectBridgeProofDisputed({txId: txId, relayer: relayerA}); dispute({caller: guard, txId: txId}); assertEq(fastBridge.bridgeStatuses(txId), IFastBridgeV2.BridgeStatus.REQUESTED); assertEq(fastBridge.protocolFees(address(srcToken)), INITIAL_PROTOCOL_FEES_TOKEN); @@ -684,7 +683,7 @@ contract FastBridgeV2SrcTest is FastBridgeV2SrcBaseTest { bytes32 txId = getTxId(ethTx); bridge({caller: userA, msgValue: ethParams.originAmount, params: ethParams}); prove({caller: relayerA, bridgeTx: ethTx, destTxHash: hex"01"}); - expectBridgeProofDisputed({txId: txId, guard: guard}); + expectBridgeProofDisputed({txId: txId, relayer: relayerA}); dispute({caller: guard, txId: txId}); assertEq(fastBridge.bridgeStatuses(txId), IFastBridgeV2.BridgeStatus.REQUESTED); assertEq(fastBridge.protocolFees(ETH_ADDRESS), INITIAL_PROTOCOL_FEES_ETH); @@ -697,7 +696,7 @@ contract FastBridgeV2SrcTest is FastBridgeV2SrcBaseTest { bridge({caller: userA, msgValue: ethParams.originAmount, params: ethParams}); prove({caller: relayerA, bridgeTx: ethTx, destTxHash: hex"01"}); skip(CLAIM_DELAY); - expectBridgeProofDisputed({txId: txId, guard: guard}); + expectBridgeProofDisputed({txId: txId, relayer: relayerA}); dispute({caller: guard, txId: txId}); assertEq(fastBridge.bridgeStatuses(txId), IFastBridgeV2.BridgeStatus.REQUESTED); assertEq(fastBridge.protocolFees(ETH_ADDRESS), INITIAL_PROTOCOL_FEES_ETH); diff --git a/packages/contracts-rfq/test/FastBridgeV2.t.sol b/packages/contracts-rfq/test/FastBridgeV2.t.sol index 8011f8a8e2..1a0c3645a3 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.t.sol @@ -44,6 +44,9 @@ abstract contract FastBridgeV2Test is Test, IFastBridgeV2Errors { IFastBridgeV2.BridgeParamsV2 internal tokenParamsV2; IFastBridgeV2.BridgeParamsV2 internal ethParamsV2; + /// @notice We include an empty "test" function so that this contract does not appear in the coverage report. + function testFastBridgeV2Test() external {} + function setUp() public virtual { srcToken = new MockERC20("SrcToken", 6); dstToken = new MockERC20("DstToken", 6); @@ -128,12 +131,14 @@ abstract contract FastBridgeV2Test is Test, IFastBridgeV2Errors { quoteRelayer: address(0), quoteExclusivitySeconds: 0, quoteId: bytes(""), + callValue: 0, callParams: bytes("") }); ethParamsV2 = IFastBridgeV2.BridgeParamsV2({ quoteRelayer: address(0), quoteExclusivitySeconds: 0, quoteId: bytes(""), + callValue: 0, callParams: bytes("") }); @@ -158,7 +163,6 @@ abstract contract FastBridgeV2Test is Test, IFastBridgeV2Errors { txV2.originAmount = txV1.originAmount; txV2.destAmount = txV1.destAmount; txV2.originFeeAmount = txV1.originFeeAmount; - txV2.sendChainGas = txV1.sendChainGas; txV2.deadline = txV1.deadline; txV2.nonce = txV1.nonce; } @@ -168,6 +172,11 @@ abstract contract FastBridgeV2Test is Test, IFastBridgeV2Errors { tokenTx.callParams = callParams; } + function setTokenTestCallValue(uint256 callValue) public { + tokenParamsV2.callValue = callValue; + tokenTx.callValue = callValue; + } + function setTokenTestExclusivityParams(address relayer, uint256 exclusivitySeconds) public { tokenParamsV2.quoteRelayer = relayer; tokenParamsV2.quoteExclusivitySeconds = int256(exclusivitySeconds); @@ -182,6 +191,11 @@ abstract contract FastBridgeV2Test is Test, IFastBridgeV2Errors { ethTx.callParams = callParams; } + function setEthTestCallValue(uint256 callValue) public { + ethParamsV2.callValue = callValue; + ethTx.callValue = callValue; + } + function setEthTestExclusivityParams(address relayer, uint256 exclusivitySeconds) public { ethParamsV2.quoteRelayer = relayer; ethParamsV2.quoteExclusivitySeconds = int256(exclusivitySeconds); @@ -205,7 +219,6 @@ abstract contract FastBridgeV2Test is Test, IFastBridgeV2Errors { txV1.originAmount = txV2.originAmount; txV1.destAmount = txV2.destAmount; txV1.originFeeAmount = txV2.originFeeAmount; - txV1.sendChainGas = txV2.sendChainGas; txV1.deadline = txV2.deadline; txV1.nonce = txV2.nonce; } @@ -231,4 +244,8 @@ abstract contract FastBridgeV2Test is Test, IFastBridgeV2Errors { function cheatCollectedProtocolFees(address token, uint256 amount) public { stdstore.target(address(fastBridge)).sig("protocolFees(address)").with_key(token).checked_write(amount); } + + function cheatSenderNonce(address sender, uint256 nonce) public { + stdstore.target(address(fastBridge)).sig("senderNonces(address)").with_key(sender).checked_write(nonce); + } } diff --git a/packages/contracts-rfq/test/MockERC20.sol b/packages/contracts-rfq/test/MockERC20.sol index f4c07d49aa..b0e2f67d99 100644 --- a/packages/contracts-rfq/test/MockERC20.sol +++ b/packages/contracts-rfq/test/MockERC20.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.17; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +// solhint-disable no-empty-blocks contract MockERC20 is ERC20 { uint8 private _decimals; @@ -10,6 +11,9 @@ contract MockERC20 is ERC20 { _decimals = decimals_; } + /// @notice We include an empty "test" function so that this contract does not appear in the coverage report. + function testMockERC20() external {} + function burn(address account, uint256 amount) external { _burn(account, amount); } diff --git a/packages/contracts-rfq/test/UniversalTokenLibHarness.sol b/packages/contracts-rfq/test/UniversalTokenLibHarness.sol index 7f8d2d6753..5e628b5fa2 100644 --- a/packages/contracts-rfq/test/UniversalTokenLibHarness.sol +++ b/packages/contracts-rfq/test/UniversalTokenLibHarness.sol @@ -3,8 +3,11 @@ pragma solidity ^0.8.17; import {UniversalTokenLib} from "../contracts/libs/UniversalToken.sol"; -// solhint-disable ordering +// solhint-disable no-empty-blocks, ordering contract UniversalTokenLibHarness { + /// @notice We include an empty "test" function so that this contract does not appear in the coverage report. + function testUniversalTokenLibHarness() external {} + function universalTransfer(address token, address to, uint256 value) public { UniversalTokenLib.universalTransfer(token, to, value); } diff --git a/packages/contracts-rfq/test/harnesses/MulticallTargetHarness.sol b/packages/contracts-rfq/test/harnesses/MulticallTargetHarness.sol index 5819dbf3fc..4a2e527f22 100644 --- a/packages/contracts-rfq/test/harnesses/MulticallTargetHarness.sol +++ b/packages/contracts-rfq/test/harnesses/MulticallTargetHarness.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.0; import {MulticallTarget} from "../../contracts/utils/MulticallTarget.sol"; +// solhint-disable no-empty-blocks contract MulticallTargetHarness is MulticallTarget { address public addressField; uint256 public uintField; @@ -11,6 +12,9 @@ contract MulticallTargetHarness is MulticallTarget { error CustomError(); + /// @notice We include an empty "test" function so that this contract does not appear in the coverage report. + function testMulticallTargetHarness() external {} + function setMsgSenderAsAddressField() external returns (address) { addressField = msg.sender; return addressField; diff --git a/packages/contracts-rfq/test/mocks/ExcessiveReturnValueRecipient.sol b/packages/contracts-rfq/test/mocks/ExcessiveReturnValueRecipient.sol index 9c3be02502..21f3484dd7 100644 --- a/packages/contracts-rfq/test/mocks/ExcessiveReturnValueRecipient.sol +++ b/packages/contracts-rfq/test/mocks/ExcessiveReturnValueRecipient.sol @@ -3,11 +3,15 @@ pragma solidity ^0.8.0; import {IFastBridgeRecipient} from "../../contracts/interfaces/IFastBridgeRecipient.sol"; +// solhint-disable no-empty-blocks /// @notice Incorrectly implemented recipient mock for testing purposes. DO NOT USE IN PRODUCTION. contract ExcessiveReturnValueRecipient { /// @notice Mock needs to accept ETH receive() external payable {} + /// @notice We include an empty "test" function so that this contract does not appear in the coverage report. + function testExcessiveReturnValueRecipient() external {} + /// @notice Incorrectly implemented - method returns excessive bytes. function fastBridgeTransferReceived(address, uint256, bytes memory) external payable returns (bytes4, uint256) { return (IFastBridgeRecipient.fastBridgeTransferReceived.selector, 1337); diff --git a/packages/contracts-rfq/test/mocks/IncorrectReturnValueRecipient.sol b/packages/contracts-rfq/test/mocks/IncorrectReturnValueRecipient.sol index 2bf955da7f..0570f49cd0 100644 --- a/packages/contracts-rfq/test/mocks/IncorrectReturnValueRecipient.sol +++ b/packages/contracts-rfq/test/mocks/IncorrectReturnValueRecipient.sol @@ -3,11 +3,15 @@ pragma solidity ^0.8.0; import {IFastBridgeRecipient} from "../../contracts/interfaces/IFastBridgeRecipient.sol"; +// solhint-disable no-empty-blocks /// @notice Incorrectly implemented recipient mock for testing purposes. DO NOT USE IN PRODUCTION. contract IncorrectReturnValueRecipient is IFastBridgeRecipient { /// @notice Mock needs to accept ETH receive() external payable {} + /// @notice We include an empty "test" function so that this contract does not appear in the coverage report. + function testIncorrectReturnValueRecipient() external {} + /// @notice Incorrectly implemented - method returns incorrect value. function fastBridgeTransferReceived(address, uint256, bytes memory) external payable returns (bytes4) { // Flip the last bit diff --git a/packages/contracts-rfq/test/mocks/NoReturnValueRecipient.sol b/packages/contracts-rfq/test/mocks/NoReturnValueRecipient.sol index e10c8b6ded..1ba76e7402 100644 --- a/packages/contracts-rfq/test/mocks/NoReturnValueRecipient.sol +++ b/packages/contracts-rfq/test/mocks/NoReturnValueRecipient.sol @@ -1,13 +1,15 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -// solhint-disable - +// solhint-disable no-empty-blocks /// @notice Incorrectly implemented recipient mock for testing purposes. DO NOT USE IN PRODUCTION. contract NoReturnValueRecipient { /// @notice Mock needs to accept ETH receive() external payable {} + /// @notice We include an empty "test" function so that this contract does not appear in the coverage report. + function testNoReturnValueRecipient() external {} + /// @notice Incorrectly implemented - method does not return anything. function fastBridgeTransferReceived(address, uint256, bytes memory) external payable {} } diff --git a/packages/contracts-rfq/test/mocks/NonPayableRecipient.sol b/packages/contracts-rfq/test/mocks/NonPayableRecipient.sol index 1f53dabfd1..2c80581943 100644 --- a/packages/contracts-rfq/test/mocks/NonPayableRecipient.sol +++ b/packages/contracts-rfq/test/mocks/NonPayableRecipient.sol @@ -1,8 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; +// solhint-disable no-empty-blocks /// @notice Incorrectly implemented recipient mock for testing purposes. DO NOT USE IN PRODUCTION. contract NonPayableRecipient { + /// @notice We include an empty "test" function so that this contract does not appear in the coverage report. + function testNonPayableRecipient() external {} + /// @notice Incorrectly implemented - method is not payable. function fastBridgeTransferReceived(address, uint256, bytes memory) external pure returns (bytes4) { return NonPayableRecipient.fastBridgeTransferReceived.selector; diff --git a/packages/contracts-rfq/test/mocks/RecipientMock.sol b/packages/contracts-rfq/test/mocks/RecipientMock.sol index a35d4ac5ec..f95422b322 100644 --- a/packages/contracts-rfq/test/mocks/RecipientMock.sol +++ b/packages/contracts-rfq/test/mocks/RecipientMock.sol @@ -3,11 +3,15 @@ pragma solidity ^0.8.0; import {IFastBridgeRecipient} from "../../contracts/interfaces/IFastBridgeRecipient.sol"; +// solhint-disable no-empty-blocks /// @notice Recipient mock for testing purposes. DO NOT USE IN PRODUCTION. contract RecipientMock is IFastBridgeRecipient { /// @notice Mock needs to accept ETH receive() external payable {} + /// @notice We include an empty "test" function so that this contract does not appear in the coverage report. + function testRecipientMock() external {} + /// @notice Minimal viable implementation of the fastBridgeTransferReceived hook. function fastBridgeTransferReceived(address, uint256, bytes memory) external payable returns (bytes4) { return IFastBridgeRecipient.fastBridgeTransferReceived.selector; diff --git a/packages/explorer-ui/CHANGELOG.md b/packages/explorer-ui/CHANGELOG.md index bbb39b439a..cabb762a39 100644 --- a/packages/explorer-ui/CHANGELOG.md +++ b/packages/explorer-ui/CHANGELOG.md @@ -3,6 +3,25 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [0.4.0](https://github.com/synapsecns/sanguine/compare/@synapsecns/explorer-ui@0.3.11...@synapsecns/explorer-ui@0.4.0) (2024-10-10) + + +### Features + +* **synapse-constants:** adds preinstall step ([#3269](https://github.com/synapsecns/sanguine/issues/3269)) ([acd61de](https://github.com/synapsecns/sanguine/commit/acd61de4846d9b23d7aa834b8f2eefcaae486c7d)) + + + + + +## [0.3.11](https://github.com/synapsecns/sanguine/compare/@synapsecns/explorer-ui@0.3.10...@synapsecns/explorer-ui@0.3.11) (2024-10-10) + +**Note:** Version bump only for package @synapsecns/explorer-ui + + + + + ## [0.3.10](https://github.com/synapsecns/sanguine/compare/@synapsecns/explorer-ui@0.3.9...@synapsecns/explorer-ui@0.3.10) (2024-10-05) **Note:** Version bump only for package @synapsecns/explorer-ui diff --git a/packages/explorer-ui/README.md b/packages/explorer-ui/README.md index 76dc102563..31043416e4 100644 --- a/packages/explorer-ui/README.md +++ b/packages/explorer-ui/README.md @@ -1,59 +1,18 @@ # Explorer UI ## TODO: - - add readme describing explorer ui - - add integration tests -# Getting Started with Create React App +- add readme describing explorer ui +- add integration tests -This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). - -## Available Scripts - -In the project directory, you can run: - -### `npm start` - -Runs the app in the development mode.\ -Open [http://localhost:3000](http://localhost:3000) to view it in your browser. - -The page will reload when you make changes.\ -You may also see any lint errors in the console. - -### `npm test` - -Launches the test runner in the interactive watch mode.\ -See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. - -### `npm run build` - -Builds the app for production to the `build` folder.\ -It correctly bundles React in production mode and optimizes the build for the best performance. - -The build is minified and the filenames include the hashes.\ -Your app is ready to be deployed! - -See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. - -### `npm run eject` - -**Note: this is a one-way operation. Once you `eject`, you can't go back!** - -If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. - -Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. - -You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. - -## Learn More - -You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). - -To learn React, check out the [React documentation](https://reactjs.org/). - -### Code Splitting - -This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) +## To get started +run +```bash +yarn install +``` +```bash +yarn dev +``` diff --git a/packages/explorer-ui/package.json b/packages/explorer-ui/package.json index 970f75ab34..c1557a7d00 100644 --- a/packages/explorer-ui/package.json +++ b/packages/explorer-ui/package.json @@ -1,6 +1,6 @@ { "name": "@synapsecns/explorer-ui", - "version": "0.3.10", + "version": "0.4.0", "private": true, "engines": { "node": ">=18.17.0" @@ -17,7 +17,7 @@ "@mui/x-date-pickers": "^5.0.17", "@next/third-parties": "^14.2.14", "@popperjs/core": "^2.11.5", - "@synapsecns/synapse-constants": "^1.6.0", + "@synapsecns/synapse-constants": "^1.7.0", "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^13.2.0", "@testing-library/user-event": "^13.5.0", diff --git a/packages/rest-api/.env.example b/packages/rest-api/.env.example new file mode 100644 index 0000000000..8bb7766569 --- /dev/null +++ b/packages/rest-api/.env.example @@ -0,0 +1 @@ +OMNIRPC_BASE_URL= \ No newline at end of file diff --git a/packages/rest-api/CHANGELOG.md b/packages/rest-api/CHANGELOG.md index 2f4a0e9c1b..4ecd8ebcae 100644 --- a/packages/rest-api/CHANGELOG.md +++ b/packages/rest-api/CHANGELOG.md @@ -3,6 +3,49 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.5.0](https://github.com/synapsecns/sanguine/compare/@synapsecns/rest-api@1.4.9...@synapsecns/rest-api@1.5.0) (2024-10-15) + + +### Features + +* **rest-api:** Primary RPCs get omnirpc ([#3294](https://github.com/synapsecns/sanguine/issues/3294)) ([31a3ce9](https://github.com/synapsecns/sanguine/commit/31a3ce9f1a90d47425dae6ef969b28de3895a6aa)) + + + + + +## [1.4.9](https://github.com/synapsecns/sanguine/compare/@synapsecns/rest-api@1.4.8...@synapsecns/rest-api@1.4.9) (2024-10-11) + +**Note:** Version bump only for package @synapsecns/rest-api + + + + + +## [1.4.8](https://github.com/synapsecns/sanguine/compare/@synapsecns/rest-api@1.4.7...@synapsecns/rest-api@1.4.8) (2024-10-11) + +**Note:** Version bump only for package @synapsecns/rest-api + + + + + +## [1.4.7](https://github.com/synapsecns/sanguine/compare/@synapsecns/rest-api@1.4.6...@synapsecns/rest-api@1.4.7) (2024-10-11) + +**Note:** Version bump only for package @synapsecns/rest-api + + + + + +## [1.4.6](https://github.com/synapsecns/sanguine/compare/@synapsecns/rest-api@1.4.5...@synapsecns/rest-api@1.4.6) (2024-10-10) + +**Note:** Version bump only for package @synapsecns/rest-api + + + + + ## [1.4.5](https://github.com/synapsecns/sanguine/compare/@synapsecns/rest-api@1.4.4...@synapsecns/rest-api@1.4.5) (2024-10-09) diff --git a/packages/rest-api/package.json b/packages/rest-api/package.json index a68609fe9f..8b4a6a13ec 100644 --- a/packages/rest-api/package.json +++ b/packages/rest-api/package.json @@ -1,6 +1,6 @@ { "name": "@synapsecns/rest-api", - "version": "1.4.5", + "version": "1.5.0", "private": "true", "engines": { "node": ">=18.17.0" @@ -22,8 +22,9 @@ "@ethersproject/bignumber": "^5.7.0", "@ethersproject/providers": "^5.7.2", "@ethersproject/units": "5.7.0", - "@synapsecns/sdk-router": "^0.11.3", + "@synapsecns/sdk-router": "^0.11.4", "bignumber": "^1.1.0", + "dotenv": "^16.4.5", "ethers": "5.7.2", "express": "^4.18.2", "express-validator": "^7.2.0", diff --git a/packages/rest-api/src/constants/chains.ts b/packages/rest-api/src/constants/chains.ts index dcc4c2502b..521698e789 100644 --- a/packages/rest-api/src/constants/chains.ts +++ b/packages/rest-api/src/constants/chains.ts @@ -1,13 +1,14 @@ import _ from 'lodash' import { Chain } from '../types' +import { getOmniRpcUrl } from '../utils/getOmniRpcUrl' export const ETHEREUM: Chain = { id: 1, name: 'Ethereum', rpcUrls: { - primary: 'https://ethereum.blockpi.network/v1/rpc/public', - fallback: 'https://rpc.ankr.com/eth', + primary: getOmniRpcUrl(1), + fallback: 'https://ethereum.blockpi.network/v1/rpc/public', }, explorerUrl: 'https://etherscan.com', explorerName: 'Etherscan', @@ -27,8 +28,8 @@ export const ARBITRUM: Chain = { id: 42161, name: 'Arbitrum', rpcUrls: { - primary: 'https://arbitrum.blockpi.network/v1/rpc/public', - fallback: 'https://arb1.arbitrum.io/rpc', + primary: getOmniRpcUrl(42161), + fallback: 'https://arbitrum.blockpi.network/v1/rpc/public', }, explorerUrl: 'https://arbiscan.io', explorerName: 'Arbiscan', @@ -48,8 +49,8 @@ export const BNBCHAIN: Chain = { id: 56, name: 'BNB Chain', rpcUrls: { - primary: 'https://bsc-dataseed1.ninicoin.io/', - fallback: 'https://bsc-dataseed2.ninicoin.io', + primary: getOmniRpcUrl(56), + fallback: 'https://bsc-dataseed1.ninicoin.io/', }, explorerUrl: 'https://bscscan.com', explorerName: 'BscScan', @@ -69,8 +70,8 @@ export const AVALANCHE: Chain = { id: 43114, name: 'Avalanche', rpcUrls: { - primary: 'https://api.avax.network/ext/bc/C/rpc', - fallback: 'https://1rpc.io/avax/c', + primary: getOmniRpcUrl(43114), + fallback: 'https://api.avax.network/ext/bc/C/rpc', }, explorerUrl: 'https://snowtrace.io/', explorerName: 'Snowtrace', @@ -90,8 +91,8 @@ export const CANTO: Chain = { id: 7700, name: 'Canto', rpcUrls: { - primary: 'https://mainnode.plexnode.org:8545', - fallback: 'https://canto.slingshot.finance', + primary: getOmniRpcUrl(7700), + fallback: 'https://mainnode.plexnode.org:8545', }, explorerUrl: 'https://tuber.build/', explorerName: 'Canto Explorer', @@ -111,8 +112,8 @@ export const OPTIMISM: Chain = { id: 10, name: 'Optimism', rpcUrls: { - primary: 'https://mainnet.optimism.io', - fallback: 'https://1rpc.io/op', + primary: getOmniRpcUrl(10), + fallback: 'https://mainnet.optimism.io', }, explorerUrl: 'https://optimistic.etherscan.io', explorerName: 'Optimism Explorer', @@ -132,8 +133,8 @@ export const POLYGON: Chain = { id: 137, name: 'Polygon', rpcUrls: { - primary: 'https://polygon-bor.publicnode.com', - fallback: 'https://polygon.llamarpc.com', + primary: getOmniRpcUrl(137), + fallback: 'https://polygon-bor.publicnode.com', }, explorerUrl: 'https://polygonscan.com', explorerName: 'PolygonScan', @@ -153,8 +154,8 @@ export const DFK: Chain = { id: 53935, name: 'DFK Chain', rpcUrls: { - primary: 'https://subnets.avax.network/defi-kingdoms/dfk-chain/rpc', - fallback: 'https://dfkchain.api.onfinality.io/public', + primary: getOmniRpcUrl(53935), + fallback: 'https://subnets.avax.network/defi-kingdoms/dfk-chain/rpc', }, explorerUrl: 'https://subnets.avax.network/defi-kingdoms', explorerName: 'DFK Subnet Explorer', @@ -174,8 +175,8 @@ export const KLAYTN: Chain = { id: 8217, name: 'Klaytn', rpcUrls: { - primary: 'https://klaytn.blockpi.network/v1/rpc/public', - fallback: 'https://klaytn.api.onfinality.io/public', + primary: getOmniRpcUrl(8217), + fallback: 'https://klaytn.blockpi.network/v1/rpc/public', }, explorerUrl: 'https://scope.klaytn.com', explorerName: 'Klaytn Explorer', @@ -195,8 +196,8 @@ export const FANTOM: Chain = { id: 250, name: 'Fantom', rpcUrls: { - primary: 'https://rpc.ftm.tools', - fallback: 'https://fantom-rpc.gateway.pokt.network/', + primary: getOmniRpcUrl(250), + fallback: 'https://rpc.ftm.tools', }, explorerUrl: 'https://ftmscan.com', explorerName: 'FTMScan', @@ -216,8 +217,8 @@ export const CRONOS: Chain = { id: 25, name: 'Cronos', rpcUrls: { - primary: 'https://evm-cronos.crypto.org', - fallback: 'https://cronos.blockpi.network/v1/rpc/public', + primary: getOmniRpcUrl(25), + fallback: 'https://evm-cronos.crypto.org', }, explorerUrl: 'https://cronoscan.com', explorerName: 'CronoScan', @@ -237,8 +238,8 @@ export const BOBA: Chain = { id: 288, name: 'Boba Chain', rpcUrls: { - primary: 'https://mainnet.boba.network', - fallback: 'https://replica.boba.network', + primary: getOmniRpcUrl(288), + fallback: 'https://mainnet.boba.network', }, explorerUrl: 'https://bobascan.com', explorerName: 'Boba Explorer', @@ -258,8 +259,8 @@ export const METIS: Chain = { id: 1088, name: 'Metis', rpcUrls: { - primary: 'https://andromeda.metis.io/?owner=1088', - fallback: 'https://metis-mainnet.public.blastapi.io', + primary: getOmniRpcUrl(1088), + fallback: 'https://andromeda.metis.io/?owner=1088', }, explorerUrl: 'https://andromeda-explorer.metis.io', explorerName: 'Metis Explorer', @@ -279,8 +280,8 @@ export const AURORA: Chain = { id: 1313161554, name: 'Aurora', rpcUrls: { - primary: 'https://mainnet.aurora.dev', - fallback: 'https://1rpc.io/aurora', + primary: getOmniRpcUrl(1313161554), + fallback: 'https://mainnet.aurora.dev', }, explorerUrl: 'https://explorer.mainnet.aurora.dev', explorerName: 'Aurora Explorer', @@ -300,8 +301,8 @@ export const HARMONY: Chain = { id: 1666600000, name: 'Harmony', rpcUrls: { - primary: 'https://api.harmony.one', - fallback: 'https://api.s0.t.hmny.io', + primary: getOmniRpcUrl(1666600000), + fallback: 'https://api.harmony.one', }, explorerUrl: 'https://explorer.harmony.one', explorerName: 'Harmony Explorer', @@ -321,8 +322,8 @@ export const MOONBEAM: Chain = { id: 1284, name: 'Moonbeam', rpcUrls: { - primary: 'https://rpc.api.moonbeam.network', - fallback: 'https://moonbeam.public.blastapi.io', + primary: getOmniRpcUrl(1284), + fallback: 'https://rpc.api.moonbeam.network', }, explorerUrl: 'https://moonbeam.moonscan.io', explorerName: 'Moonbeam Explorer', @@ -342,8 +343,8 @@ export const MOONRIVER: Chain = { id: 1285, name: 'Moonriver', rpcUrls: { - primary: 'https://rpc.api.moonriver.moonbeam.network', - fallback: 'https://moonriver.public.blastapi.io', + primary: getOmniRpcUrl(1285), + fallback: 'https://rpc.api.moonriver.moonbeam.network', }, explorerUrl: 'https://moonriver.moonscan.io', explorerName: 'Moonriver Explorer', @@ -363,8 +364,8 @@ export const DOGE: Chain = { id: 2000, name: 'Dogechain', rpcUrls: { - primary: 'https://rpc.dogechain.dog', - fallback: 'https://rpc01-sg.dogechain.dog', + primary: getOmniRpcUrl(2000), + fallback: 'https://rpc.dogechain.dog', }, explorerUrl: 'https://explorer.dogechain.dog', explorerName: 'Dogechain Explorer', @@ -384,8 +385,8 @@ export const BASE: Chain = { id: 8453, name: 'Base', rpcUrls: { - primary: 'https://base.blockpi.network/v1/rpc/public', - fallback: 'https://developer-access-mainnet.base.org', + primary: getOmniRpcUrl(8453), + fallback: 'https://base.blockpi.network/v1/rpc/public', }, explorerUrl: 'https://basescan.org', explorerName: 'BaseScan', @@ -405,9 +406,9 @@ export const BLAST: Chain = { id: 81457, name: 'Blast', rpcUrls: { - primary: + primary: getOmniRpcUrl(81457), + fallback: 'https://lingering-indulgent-replica.blast-mainnet.quiknode.pro/6667a8f4be701cb6549b415d567bc706fb2f13a8/', - fallback: 'https://blast.blockpi.network/v1/rpc/publicChain', }, explorerUrl: 'https://blastscan.io', explorerName: 'Blastscan', @@ -427,8 +428,8 @@ export const SCROLL: Chain = { id: 534352, name: 'Scroll', rpcUrls: { - primary: 'https://rpc.scroll.io/', - fallback: 'https://scroll.blockpi.network/v1/rpc/public', + primary: getOmniRpcUrl(534352), + fallback: 'https://rpc.scroll.io/', }, explorerUrl: 'https://scrollscan.com', explorerName: 'Scrollscan', @@ -448,8 +449,8 @@ export const LINEA: Chain = { id: 59144, name: 'Linea', rpcUrls: { - primary: 'https://rpc.linea.build', - fallback: 'https://linea.blockpi.network/v1/rpc/public', + primary: getOmniRpcUrl(59144), + fallback: 'https://rpc.linea.build', }, explorerUrl: 'https://lineascan.build', explorerName: 'LineaScan', diff --git a/packages/rest-api/src/constants/index.ts b/packages/rest-api/src/constants/index.ts index c136beb9fa..12a3cb05a3 100644 --- a/packages/rest-api/src/constants/index.ts +++ b/packages/rest-api/src/constants/index.ts @@ -1,3 +1,5 @@ +import { CHAINS } from './chains' + export const VALID_BRIDGE_MODULES = [ 'SynapseBridge', 'SynapseCCTP', @@ -6,3 +8,22 @@ export const VALID_BRIDGE_MODULES = [ export const ZeroAddress = '0x0000000000000000000000000000000000000000' export const NativeGasAddress = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE' + +export const SUPPORTED_SWAP_CHAIN_IDS = [ + CHAINS.ARBITRUM.id, + CHAINS.AURORA.id, + CHAINS.AVALANCHE.id, + CHAINS.BASE.id, + CHAINS.BLAST.id, + CHAINS.BNBCHAIN.id, + CHAINS.BOBA.id, + CHAINS.CANTO.id, + CHAINS.CRONOS.id, + CHAINS.ETHEREUM.id, + CHAINS.FANTOM.id, + CHAINS.HARMONY.id, + CHAINS.KLAYTN.id, + CHAINS.METIS.id, + CHAINS.OPTIMISM.id, + CHAINS.POLYGON.id, +] diff --git a/packages/rest-api/src/controllers/destinationTxController.ts b/packages/rest-api/src/controllers/destinationTxController.ts index dc951976d8..abc3b9e1e9 100644 --- a/packages/rest-api/src/controllers/destinationTxController.ts +++ b/packages/rest-api/src/controllers/destinationTxController.ts @@ -46,7 +46,7 @@ export const destinationTxController = async (req, res) => { }) const graphqlData = await graphqlResponse.json() - const toInfo = graphqlData.data.bridgeTransactions[0]?.toInfo || null + const toInfo = graphqlData.data.bridgeTransactions?.[0]?.toInfo || null if (toInfo) { const { tokenAddress, value, chainID, ...restToInfo } = toInfo diff --git a/packages/rest-api/src/routes/swapRoute.ts b/packages/rest-api/src/routes/swapRoute.ts index 6eef6a67f9..5f8bb04277 100644 --- a/packages/rest-api/src/routes/swapRoute.ts +++ b/packages/rest-api/src/routes/swapRoute.ts @@ -8,7 +8,8 @@ import { isTokenAddress } from '../utils/isTokenAddress' import { isTokenSupportedOnChain } from '../utils/isTokenSupportedOnChain' import { checksumAddresses } from '../middleware/checksumAddresses' import { normalizeNativeTokenAddress } from '../middleware/normalizeNativeTokenAddress' -import { validSwap } from '../validations/validSwap' +import { validSwapTokens } from '../validations/validSwapTokens' +import { validSwapChain } from '../validations/validSwapChain' const router = express.Router() @@ -163,11 +164,18 @@ router.get( ) .withMessage('Token not supported on specified chain'), check('amount').exists().withMessage('amount is required').isNumeric(), + check() + .custom((_value, { req }) => { + const { chain } = req.query + + return validSwapChain(chain) + }) + .withMessage('Swap not supported for given chain'), check() .custom((_value, { req }) => { const { chain, fromToken, toToken } = req.query - return validSwap(chain, fromToken, toToken) + return validSwapTokens(chain, fromToken, toToken) }) .withMessage('Swap not supported for given tokens'), ], diff --git a/packages/rest-api/src/services/synapseService.ts b/packages/rest-api/src/services/synapseService.ts index a76b9b729c..c9306a0c82 100644 --- a/packages/rest-api/src/services/synapseService.ts +++ b/packages/rest-api/src/services/synapseService.ts @@ -4,7 +4,8 @@ import { SynapseSDK } from '@synapsecns/sdk-router' import { CHAINS_ARRAY } from '../constants/chains' const providers = CHAINS_ARRAY.map( - (chain) => new JsonRpcProvider(chain.rpcUrls.primary) + (chain) => + new JsonRpcProvider(chain.rpcUrls.primary || chain.rpcUrls.fallback) ) const chainIds = CHAINS_ARRAY.map((chain) => chain.id) diff --git a/packages/rest-api/src/swagger.ts b/packages/rest-api/src/swagger.ts index 5276580bd9..b577db462d 100644 --- a/packages/rest-api/src/swagger.ts +++ b/packages/rest-api/src/swagger.ts @@ -12,7 +12,7 @@ const options: swaggerJsdoc.Options = { definition: { openapi: '3.0.0', info: { - title: 'Syanpse Protocol REST API', + title: 'Synapse Protocol REST API', version: packageJson.version, description: 'API documentation for the Synapse Protocol REST API', }, diff --git a/packages/rest-api/src/tests/swapRoute.test.ts b/packages/rest-api/src/tests/swapRoute.test.ts index c849475cc1..9c09f89a22 100644 --- a/packages/rest-api/src/tests/swapRoute.test.ts +++ b/packages/rest-api/src/tests/swapRoute.test.ts @@ -3,7 +3,7 @@ import express from 'express' import swapRoute from '../routes/swapRoute' import { NativeGasAddress, ZeroAddress } from '../constants' -import { DAI, ETH, NETH, USDC } from '../constants/bridgeable' +import { DAI, ETH, NETH, USDC, USDT } from '../constants/bridgeable' const app = express() app.use('/swap', swapRoute) @@ -78,6 +78,21 @@ describe('Swap Route with Real Synapse Service', () => { ) }, 10_000) + it('should return 400 for swap on unsupported chain', async () => { + const response = await request(app).get('/swap').query({ + chain: '59144', + fromToken: USDC.addresses[59144], + toToken: USDT.addresses[59144], + amount: '1000', + }) + + expect(response.status).toBe(400) + expect(response.body.error).toHaveProperty( + 'message', + 'Swap not supported for given chain' + ) + }) + it('should return 400 for invalid fromToken + toToken combo', async () => { const response = await request(app).get('/swap').query({ chain: '1', diff --git a/packages/rest-api/src/utils/getOmniRpcUrl.ts b/packages/rest-api/src/utils/getOmniRpcUrl.ts new file mode 100644 index 0000000000..c635d86c1a --- /dev/null +++ b/packages/rest-api/src/utils/getOmniRpcUrl.ts @@ -0,0 +1,10 @@ +import * as dotenv from 'dotenv' + +dotenv.config() + +export const getOmniRpcUrl = (chainId: number) => { + if (!process.env.OMNIRPC_BASE_URL) { + return null + } + return `${process.env.OMNIRPC_BASE_URL}/${chainId}` +} diff --git a/packages/rest-api/src/validations/validSwapChain.ts b/packages/rest-api/src/validations/validSwapChain.ts new file mode 100644 index 0000000000..47c56c87c9 --- /dev/null +++ b/packages/rest-api/src/validations/validSwapChain.ts @@ -0,0 +1,5 @@ +import { SUPPORTED_SWAP_CHAIN_IDS } from '../constants' + +export const validSwapChain = (chain: number | string) => { + return SUPPORTED_SWAP_CHAIN_IDS.includes(Number(chain)) +} diff --git a/packages/rest-api/src/validations/validSwap.ts b/packages/rest-api/src/validations/validSwapTokens.ts similarity index 92% rename from packages/rest-api/src/validations/validSwap.ts rename to packages/rest-api/src/validations/validSwapTokens.ts index 576cd23b80..797ae24279 100644 --- a/packages/rest-api/src/validations/validSwap.ts +++ b/packages/rest-api/src/validations/validSwapTokens.ts @@ -1,6 +1,6 @@ import { tokenAddressToToken } from '../utils/tokenAddressToToken' -export const validSwap = ( +export const validSwapTokens = ( chain: number | string, fromToken: string, toToken: string diff --git a/packages/rfq-indexer/api/CHANGELOG.md b/packages/rfq-indexer/api/CHANGELOG.md index 3a70fb5e6e..937939a630 100644 --- a/packages/rfq-indexer/api/CHANGELOG.md +++ b/packages/rfq-indexer/api/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.0.7](https://github.com/synapsecns/sanguine/compare/@synapsecns/rfq-indexer-api@1.0.6...@synapsecns/rfq-indexer-api@1.0.7) (2024-10-12) + +**Note:** Version bump only for package @synapsecns/rfq-indexer-api + + + + + ## [1.0.6](https://github.com/synapsecns/sanguine/compare/@synapsecns/rfq-indexer-api@1.0.5...@synapsecns/rfq-indexer-api@1.0.6) (2024-10-07) **Note:** Version bump only for package @synapsecns/rfq-indexer-api diff --git a/packages/rfq-indexer/api/package.json b/packages/rfq-indexer/api/package.json index e69511c2b5..9b2aa991a4 100644 --- a/packages/rfq-indexer/api/package.json +++ b/packages/rfq-indexer/api/package.json @@ -1,7 +1,7 @@ { "name": "@synapsecns/rfq-indexer-api", "private": true, - "version": "1.0.6", + "version": "1.0.7", "description": "", "main": "index.js", "scripts": { diff --git a/packages/rfq-indexer/api/src/controllers/transactionIdController.ts b/packages/rfq-indexer/api/src/controllers/transactionIdController.ts index 586590220a..23f1496814 100644 --- a/packages/rfq-indexer/api/src/controllers/transactionIdController.ts +++ b/packages/rfq-indexer/api/src/controllers/transactionIdController.ts @@ -10,7 +10,12 @@ export const getTransactionById = async (req: Request, res: Response) => { try { const query = db .with('deposits', () => - qDeposits().where('transactionId', '=', transactionId as string) + qDeposits().where((eb) => + eb.or([ + eb('transactionId', '=', transactionId as string), + eb('transactionHash', '=', transactionId as string) + ]) + ) ) .with('relays', () => qRelays()) .with('proofs', () => qProofs({activeOnly: false})) // display proofs even if they have been invalidated/replaced by a dispute diff --git a/packages/rfq-indexer/api/src/routes/transactionIdRoute.ts b/packages/rfq-indexer/api/src/routes/transactionIdRoute.ts index ef0b4077a4..253060dcdf 100644 --- a/packages/rfq-indexer/api/src/routes/transactionIdRoute.ts +++ b/packages/rfq-indexer/api/src/routes/transactionIdRoute.ts @@ -8,7 +8,7 @@ const router = express.Router() * @openapi * /transaction-id/{transactionId}: * get: - * summary: Get transaction details by ID + * summary: Get transaction details by ID or the origin transaction hash * description: Retrieves detailed information about a transaction, including deposit, relay, proof, claim, and refund data if available * parameters: * - in: path @@ -16,7 +16,7 @@ const router = express.Router() * required: true * schema: * type: string - * description: The unique identifier of the transaction + * description: The unique identifier of the transaction or the origin transaction hash * responses: * 200: * description: Successful response diff --git a/packages/rfq-loadtest/.gitignore b/packages/rfq-loadtest/.gitignore new file mode 100644 index 0000000000..2d6baefbab --- /dev/null +++ b/packages/rfq-loadtest/.gitignore @@ -0,0 +1,11 @@ + +node_modules/ + +*.tsbuildinfo + +dist/ + +.env + +config*.yaml +!config-template.yaml diff --git a/packages/rfq-loadtest/.vscode/launch.json b/packages/rfq-loadtest/.vscode/launch.json new file mode 100644 index 0000000000..d15373c646 --- /dev/null +++ b/packages/rfq-loadtest/.vscode/launch.json @@ -0,0 +1,26 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "debugRun", + "skipFiles": [ + "/**" + ], + "program": "${workspaceFolder}/src/index.ts", + "preLaunchTask": "npm: build", + "sourceMaps": true, + "smartStep": true, + "console": "internalConsole", + "internalConsoleOptions": "openOnSessionStart", + "args": ["--configFile", "config-dev.yaml", "--pKeyIndex", "5"], + "outFiles": [ + "${workspaceFolder}/dist/**/*.js" + ] + } + ] +} \ No newline at end of file diff --git a/packages/rfq-loadtest/CHANGELOG.md b/packages/rfq-loadtest/CHANGELOG.md new file mode 100644 index 0000000000..23b23875d4 --- /dev/null +++ b/packages/rfq-loadtest/CHANGELOG.md @@ -0,0 +1,8 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## 1.0.1 (2024-10-15) + +**Note:** Version bump only for package @synapsecns/rfq-loadtest diff --git a/packages/rfq-loadtest/README.md b/packages/rfq-loadtest/README.md new file mode 100644 index 0000000000..4af4152226 --- /dev/null +++ b/packages/rfq-loadtest/README.md @@ -0,0 +1,61 @@ +> :warning: **Warning**: This tool is not intended to be used for anything other than testing with inconsequentially small amounts of ETH on EOAs that were created for the sole & explicit purpose of testing. + +# RFQ Load Test Configuration Guide + +This guide outlines the steps and requirements for setting up the RFQ load testing tool. The tool is designed to send many ETH bridges in rapid succession. + +## Wallet Configuration + +- **Create and Fund Wallets:** You are required to create and fund as many wallets as you wish to test with. This tool only supports native ETH bridges on chains that use ETH as the gas currency. + +- **Auto-Rebalance:** If you only initially fund ETH on one of the test chains, the tool will automatically rebalance the funds to the other chains before beginning the tests. It will also rebalance as needed while the tests are operating until none of the test chains have enough ETH to effectively rebalance to the others - at which point it will cease the process. The RFQ system is also used for these rebalance actions. + + +# Script Configuration + +Follow these steps to configure your load testing environment for blockchain transactions. + +## Step 1: Prepare Configuration File + +1. Start by copying the `config-template.yaml` file from the `packages/rfq-loadtest` directory. This will serve as the basis for your test configuration. + ```bash + cp packages/rfq-loadtest/config-template.yaml packages/rfq-loadtest/config-run.yaml + ``` + +## Step 2: Configure Private Keys + +2. Open the `config-run.yaml` file and locate the `PRIVATE_KEY_X` entries. Replace the placeholder values with your actual private keys. These keys will be used to execute transactions during the tests. + ```yaml + PRIVATE_KEY_1: 'your_private_key_here' + PRIVATE_KEY_2: 'your_private_key_here' + PRIVATE_KEY_3: 'your_private_key_here' + PRIVATE_KEY_4: 'your_private_key_here' + PRIVATE_KEY_5: 'your_private_key_here' + ``` + +## Step 3: Set Transaction Pace + +3. Adjust the settings under `##### TEST PACE` to control the pace of transactions. You can modify the `VOLLEY_MILLISECONDS_BETWEEN`, `VOLLEY_MIN_COUNT`, and `VOLLEY_MAX_COUNT` to fit your testing requirements. + +## Step 4: Specify Test Bridge Amount + +4. Define the amount of ETH to be sent in each test transaction under `TEST_BRIDGE_AMOUNT_UNITS`. The default is set to `0.00007`. + +## Step 5: Configure Gas and Rebalance Settings + +5. Set the `MINIMUM_GAS_UNITS` to the desired threshold for triggering a rebalance of funds across chains. Specify the `REBALANCE_TO_UNITS` to determine the target amount for each chain after rebalancing. + +## Step 6: Define Chain Settings + +6. In the `CHAINS` section, configure the `FastRouterAddr` and `rpcUrl` settings for each chain involved in your tests. These settings include URLs for reading, simulation, and submitting transactions. + +## Step 7: Configure Test Routes + +7. Under `TEST_ROUTES`, define the routes for your test transactions, including `fromChainId`, `toChainId`, and `testDistributionPercentage` to control the flow and distribution of transactions between chains. + +## Step 8: Save and Run + +8. Save your changes to `config-run.yaml`. To start the load test, use the provided startup example, adjusting the path to your configuration file as necessary. + ```bash + python3 pyRepeater.py 'node index.js --configFile ../config-run.yaml --pKeyIndex 1' + ``` diff --git a/packages/rfq-loadtest/config-template.yaml b/packages/rfq-loadtest/config-template.yaml new file mode 100644 index 0000000000..0727058300 --- /dev/null +++ b/packages/rfq-loadtest/config-template.yaml @@ -0,0 +1,70 @@ + +# create & fund however many wallets you want to test with. +# only supports native ETH bridges on chains that use ETH as gas. +# if you only fund ETH on one of the test chains it will auto-rebalance the funds to the others before beginning tests. + +# Startup example w/ pyRepeater to auto-restart the process if anything kills it +# python3 pyRepeater.py 'node index.js --configFile ../config-prod.yaml --pKeyIndex 1' + +PRIVATE_KEY_1: '0xabcdef123456abcdef123456abcdef123456abcdef123456abcdef123456abcd' +PRIVATE_KEY_2: '0xabcdef123456abcdef123456abcdef123456abcdef123456abcdef123456abcd' +PRIVATE_KEY_3: '0xabcdef123456abcdef123456abcdef123456abcdef123456abcdef123456abcd' +PRIVATE_KEY_4: '0xabcdef123456abcdef123456abcdef123456abcdef123456abcdef123456abcd' +PRIVATE_KEY_5: '0xabcdef123456abcdef123456abcdef123456abcdef123456abcdef123456abcd' + +##### TEST PACE +# these settings are a pace of about 100K deposits per day +# +# how long to wait in btwn each volley? +VOLLEY_MILLISECONDS_BETWEEN: 5500 +# each volley sends a batch of between {min} and {max} transactions +VOLLEY_MIN_COUNT: 1 +VOLLEY_MAX_COUNT: 5 + +# these settings are a pace of about 150K deposits per day +#VOLLEY_MILLISECONDS_BETWEEN: 5500 +#VOLLEY_MIN_COUNT: 1 +#VOLLEY_MAX_COUNT: 5 + +# approx how much ETH to send on each test bridge? 0.00007 +TEST_BRIDGE_AMOUNT_UNITS: 0.00007 + +# trigger a rebalance when gas reaches this value or lower +MINIMUM_GAS_UNITS: 0.003 + +# when a rebalance is triggered, resupply the deficit chain to this amount +REBALANCE_TO_UNITS: 0.01 + +CHAINS: + 10: + FastRouterAddr: '0x00cd000000003f7f682be4813200893d4e690000' + # urls used for Reading / Tx Simulation / Tx Submit -- respectively. Change to others as needed. + rpcUrl_Read: 'https://mainnet.optimism.io' + rpcUrl_Sim: 'https://mainnet.optimism.io' + rpcUrl_Write: 'https://mainnet.optimism.io' + 8453: + FastRouterAddr: '0x00cd000000003f7f682be4813200893d4e690000' + # urls used for Reading / Tx Simulation / Tx Submit -- respectively. Change to others as needed. + rpcUrl_Read: 'https://mainnet.base.org' + rpcUrl_Sim: 'https://mainnet.base.org' + rpcUrl_Write: 'https://mainnet.base.org' + 480: + FastRouterAddr: '0x00cd000000003f7f682be4813200893d4e690000' + # urls used for Reading / Tx Simulation / Tx Submit -- respectively. Change to others as needed. + rpcUrl_Read: 'https://worldchain-mainnet.g.alchemy.com/public' + rpcUrl_Sim: 'https://worldchain-mainnet.g.alchemy.com/public' + rpcUrl_Write: 'https://worldchain-mainnet.g.alchemy.com/public' + +TEST_ROUTES: + 480>10: + fromChainId: 480 + toChainId: 10 + testDistributionPercentage: 70 + 10>480: + fromChainId: 10 + toChainId: 480 + testDistributionPercentage: 15 + 8453>480: + fromChainId: 8453 + toChainId: 480 + testDistributionPercentage: 15 diff --git a/packages/rfq-loadtest/package.json b/packages/rfq-loadtest/package.json new file mode 100644 index 0000000000..763a876e7a --- /dev/null +++ b/packages/rfq-loadtest/package.json @@ -0,0 +1,34 @@ +{ + "name": "@synapsecns/rfq-loadtest", + "version": "1.0.1", + "main": "index.js", + "type": "module", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "build": "cp src/pyRepeater.py dist/ && tsc", + "lint": "eslint .", + "ci:lint": "eslint .", + "typecheck": "tsc", + "test:coverage": "echo 'No tests defined.'", + "build:go": " ", + "build:slither": " " + }, + "keywords": [], + "author": "", + "license": "ISC", + "private": "true", + "description": "", + "devDependencies": { + "@types/js-yaml": "^4.0.9", + "@types/node": "^22.7.5", + "@types/yargs": "^17.0.33", + "ts-node": "^10.9.2", + "typescript": "^5.6.3" + }, + "dependencies": { + "axios": "^1.7.7", + "js-yaml": "^4.1.0", + "viem": "^2.21.19", + "yargs": "^17.7.2" + } +} diff --git a/packages/rfq-loadtest/sql/ponderSummary.sql b/packages/rfq-loadtest/sql/ponderSummary.sql new file mode 100644 index 0000000000..0ce67b33ce --- /dev/null +++ b/packages/rfq-loadtest/sql/ponderSummary.sql @@ -0,0 +1,43 @@ + +-- run this on ponder index to get a summary of a test run. use WHERE params to point it at a particular test period. + +select to_timestamp(min(depositTs)) period_start, + to_timestamp(max(depositTs)) period_end, + max(depositTs) - min(depositTs) period_seconds, + round(count(1) * 1.00 / (max(depositTs) - min(depositTs)), 2) deposits_per_second, + round(count(1) * 1.00 / (max(depositTs) - min(depositTs)) * 86400, 0) deposits_per_day, + count(1) count_deposits, + count(case when proofSeconds is not null then 1 else null end) count_proofs, + count(case when claimSeconds is not null then 1 else null end) count_claims, + count(case when disputeId is not null then 1 else null end) count_disputes, + round(avg(relaySeconds), 2) relaySeconds_AVG, + max(relaySeconds) relaySeconds_MAX, + round(avg(proofSeconds), 2) proofSeconds_AVG, + max(proofSeconds) proofSeconds_MAX, + round(avg(claimSeconds), 2) claimSeconds_AVG, + max(claimSeconds) claimSeconds_MAX +from ( +-- this subquery can be executed by itself for detail data + select xdeposit."blockTimestamp" depositTs, + xdeposit."transactionId", + xrelay."blockTimestamp" - xdeposit."blockTimestamp" relaySeconds, + xproof."blockTimestamp" - xrelay."blockTimestamp" proofSeconds, + xclaim."blockTimestamp" - xproof."blockTimestamp" - 1800 claimSeconds, + xdispute.id disputeId, + xrelay.relayer + from "BridgeRequestEvents" xdeposit + left join "BridgeRelayedEvents" xrelay on + xdeposit."transactionId" = xrelay."transactionId" + left join "BridgeProofProvidedEvents" xproof on + xdeposit."transactionId" = xproof."transactionId" + left join "BridgeDepositClaimedEvents" xclaim on + xdeposit."transactionId" = xclaim."transactionId" + left join "BridgeProofDisputedEvents" xdispute on + xdeposit."transactionId" = xdispute."transactionId" + where xdeposit."originChainId" = 480 + and xdeposit."originAmount" <= 71000000000000 + + -- change these to point at a particular test period + and xdeposit."blockTimestamp" between 1728666774 and 1728667721 + ) sqData; + diff --git a/packages/rfq-loadtest/src/abi.ts b/packages/rfq-loadtest/src/abi.ts new file mode 100644 index 0000000000..d5345e3dc8 --- /dev/null +++ b/packages/rfq-loadtest/src/abi.ts @@ -0,0 +1,952 @@ +export const ABI = { + fastBridgeV1: [ + { + inputs: [{ internalType: 'address', name: '_owner', type: 'address' }], + stateMutability: 'nonpayable', + type: 'constructor', + }, + { inputs: [], name: 'AccessControlBadConfirmation', type: 'error' }, + { + inputs: [ + { internalType: 'address', name: 'account', type: 'address' }, + { internalType: 'bytes32', name: 'neededRole', type: 'bytes32' }, + ], + name: 'AccessControlUnauthorizedAccount', + type: 'error', + }, + { + inputs: [{ internalType: 'address', name: 'target', type: 'address' }], + name: 'AddressEmptyCode', + type: 'error', + }, + { + inputs: [{ internalType: 'address', name: 'account', type: 'address' }], + name: 'AddressInsufficientBalance', + type: 'error', + }, + { inputs: [], name: 'AmountIncorrect', type: 'error' }, + { inputs: [], name: 'ChainIncorrect', type: 'error' }, + { inputs: [], name: 'DeadlineExceeded', type: 'error' }, + { inputs: [], name: 'DeadlineNotExceeded', type: 'error' }, + { inputs: [], name: 'DeadlineTooShort', type: 'error' }, + { inputs: [], name: 'DisputePeriodNotPassed', type: 'error' }, + { inputs: [], name: 'DisputePeriodPassed', type: 'error' }, + { inputs: [], name: 'FailedInnerCall', type: 'error' }, + { inputs: [], name: 'MsgValueIncorrect', type: 'error' }, + { + inputs: [{ internalType: 'address', name: 'token', type: 'address' }], + name: 'SafeERC20FailedOperation', + type: 'error', + }, + { inputs: [], name: 'SenderIncorrect', type: 'error' }, + { inputs: [], name: 'StatusIncorrect', type: 'error' }, + { inputs: [], name: 'TokenNotContract', type: 'error' }, + { inputs: [], name: 'TransactionRelayed', type: 'error' }, + { inputs: [], name: 'ZeroAddress', type: 'error' }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'bytes32', + name: 'transactionId', + type: 'bytes32', + }, + { + indexed: true, + internalType: 'address', + name: 'relayer', + type: 'address', + }, + { indexed: true, internalType: 'address', name: 'to', type: 'address' }, + { + indexed: false, + internalType: 'address', + name: 'token', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + ], + name: 'BridgeDepositClaimed', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'bytes32', + name: 'transactionId', + type: 'bytes32', + }, + { indexed: true, internalType: 'address', name: 'to', type: 'address' }, + { + indexed: false, + internalType: 'address', + name: 'token', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + ], + name: 'BridgeDepositRefunded', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'bytes32', + name: 'transactionId', + type: 'bytes32', + }, + { + indexed: true, + internalType: 'address', + name: 'relayer', + type: 'address', + }, + ], + name: 'BridgeProofDisputed', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'bytes32', + name: 'transactionId', + type: 'bytes32', + }, + { + indexed: true, + internalType: 'address', + name: 'relayer', + type: 'address', + }, + { + indexed: false, + internalType: 'bytes32', + name: 'transactionHash', + type: 'bytes32', + }, + ], + name: 'BridgeProofProvided', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'bytes32', + name: 'transactionId', + type: 'bytes32', + }, + { + indexed: true, + internalType: 'address', + name: 'relayer', + type: 'address', + }, + { indexed: true, internalType: 'address', name: 'to', type: 'address' }, + { + indexed: false, + internalType: 'uint32', + name: 'originChainId', + type: 'uint32', + }, + { + indexed: false, + internalType: 'address', + name: 'originToken', + type: 'address', + }, + { + indexed: false, + internalType: 'address', + name: 'destToken', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'originAmount', + type: 'uint256', + }, + { + indexed: false, + internalType: 'uint256', + name: 'destAmount', + type: 'uint256', + }, + { + indexed: false, + internalType: 'uint256', + name: 'chainGasAmount', + type: 'uint256', + }, + ], + name: 'BridgeRelayed', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'bytes32', + name: 'transactionId', + type: 'bytes32', + }, + { + indexed: true, + internalType: 'address', + name: 'sender', + type: 'address', + }, + { + indexed: false, + internalType: 'bytes', + name: 'request', + type: 'bytes', + }, + { + indexed: false, + internalType: 'uint32', + name: 'destChainId', + type: 'uint32', + }, + { + indexed: false, + internalType: 'address', + name: 'originToken', + type: 'address', + }, + { + indexed: false, + internalType: 'address', + name: 'destToken', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'originAmount', + type: 'uint256', + }, + { + indexed: false, + internalType: 'uint256', + name: 'destAmount', + type: 'uint256', + }, + { + indexed: false, + internalType: 'bool', + name: 'sendChainGas', + type: 'bool', + }, + ], + name: 'BridgeRequested', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'uint256', + name: 'oldChainGasAmount', + type: 'uint256', + }, + { + indexed: false, + internalType: 'uint256', + name: 'newChainGasAmount', + type: 'uint256', + }, + ], + name: 'ChainGasAmountUpdated', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'uint256', + name: 'oldFeeRate', + type: 'uint256', + }, + { + indexed: false, + internalType: 'uint256', + name: 'newFeeRate', + type: 'uint256', + }, + ], + name: 'FeeRateUpdated', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'address', + name: 'token', + type: 'address', + }, + { + indexed: false, + internalType: 'address', + name: 'recipient', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + ], + name: 'FeesSwept', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'bytes32', + name: 'role', + type: 'bytes32', + }, + { + indexed: true, + internalType: 'bytes32', + name: 'previousAdminRole', + type: 'bytes32', + }, + { + indexed: true, + internalType: 'bytes32', + name: 'newAdminRole', + type: 'bytes32', + }, + ], + name: 'RoleAdminChanged', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'bytes32', + name: 'role', + type: 'bytes32', + }, + { + indexed: true, + internalType: 'address', + name: 'account', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'sender', + type: 'address', + }, + ], + name: 'RoleGranted', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'bytes32', + name: 'role', + type: 'bytes32', + }, + { + indexed: true, + internalType: 'address', + name: 'account', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'sender', + type: 'address', + }, + ], + name: 'RoleRevoked', + type: 'event', + }, + { + inputs: [], + name: 'DEFAULT_ADMIN_ROLE', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'DISPUTE_PERIOD', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'FEE_BPS', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'FEE_RATE_MAX', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'GOVERNOR_ROLE', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'GUARD_ROLE', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'MIN_DEADLINE_PERIOD', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'REFUNDER_ROLE', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'REFUND_DELAY', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'RELAYER_ROLE', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + components: [ + { internalType: 'uint32', name: 'dstChainId', type: 'uint32' }, + { internalType: 'address', name: 'sender', type: 'address' }, + { internalType: 'address', name: 'to', type: 'address' }, + { internalType: 'address', name: 'originToken', type: 'address' }, + { internalType: 'address', name: 'destToken', type: 'address' }, + { internalType: 'uint256', name: 'originAmount', type: 'uint256' }, + { internalType: 'uint256', name: 'destAmount', type: 'uint256' }, + { internalType: 'bool', name: 'sendChainGas', type: 'bool' }, + { internalType: 'uint256', name: 'deadline', type: 'uint256' }, + ], + internalType: 'struct IFastBridge.BridgeParams', + name: 'params', + type: 'tuple', + }, + ], + name: 'bridge', + outputs: [], + stateMutability: 'payable', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + name: 'bridgeProofs', + outputs: [ + { internalType: 'uint96', name: 'timestamp', type: 'uint96' }, + { internalType: 'address', name: 'relayer', type: 'address' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + name: 'bridgeRelays', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + name: 'bridgeStatuses', + outputs: [ + { + internalType: 'enum FastBridge.BridgeStatus', + name: '', + type: 'uint8', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'transactionId', type: 'bytes32' }, + { internalType: 'address', name: 'relayer', type: 'address' }, + ], + name: 'canClaim', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'chainGasAmount', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes', name: 'request', type: 'bytes' }, + { internalType: 'address', name: 'to', type: 'address' }, + ], + name: 'claim', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'deployBlock', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'transactionId', type: 'bytes32' }, + ], + name: 'dispute', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes', name: 'request', type: 'bytes' }], + name: 'getBridgeTransaction', + outputs: [ + { + components: [ + { internalType: 'uint32', name: 'originChainId', type: 'uint32' }, + { internalType: 'uint32', name: 'destChainId', type: 'uint32' }, + { internalType: 'address', name: 'originSender', type: 'address' }, + { internalType: 'address', name: 'destRecipient', type: 'address' }, + { internalType: 'address', name: 'originToken', type: 'address' }, + { internalType: 'address', name: 'destToken', type: 'address' }, + { internalType: 'uint256', name: 'originAmount', type: 'uint256' }, + { internalType: 'uint256', name: 'destAmount', type: 'uint256' }, + { + internalType: 'uint256', + name: 'originFeeAmount', + type: 'uint256', + }, + { internalType: 'bool', name: 'sendChainGas', type: 'bool' }, + { internalType: 'uint256', name: 'deadline', type: 'uint256' }, + { internalType: 'uint256', name: 'nonce', type: 'uint256' }, + ], + internalType: 'struct IFastBridge.BridgeTransaction', + name: '', + type: 'tuple', + }, + ], + stateMutability: 'pure', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes32', name: 'role', type: 'bytes32' }], + name: 'getRoleAdmin', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'role', type: 'bytes32' }, + { internalType: 'uint256', name: 'index', type: 'uint256' }, + ], + name: 'getRoleMember', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes32', name: 'role', type: 'bytes32' }], + name: 'getRoleMemberCount', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'role', type: 'bytes32' }, + { internalType: 'address', name: 'account', type: 'address' }, + ], + name: 'grantRole', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'role', type: 'bytes32' }, + { internalType: 'address', name: 'account', type: 'address' }, + ], + name: 'hasRole', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'nonce', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'protocolFeeRate', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: '', type: 'address' }], + name: 'protocolFees', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes', name: 'request', type: 'bytes' }, + { internalType: 'bytes32', name: 'destTxHash', type: 'bytes32' }, + ], + name: 'prove', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes', name: 'request', type: 'bytes' }], + name: 'refund', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes', name: 'request', type: 'bytes' }], + name: 'relay', + outputs: [], + stateMutability: 'payable', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'role', type: 'bytes32' }, + { + internalType: 'address', + name: 'callerConfirmation', + type: 'address', + }, + ], + name: 'renounceRole', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'role', type: 'bytes32' }, + { internalType: 'address', name: 'account', type: 'address' }, + ], + name: 'revokeRole', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'uint256', name: 'newChainGasAmount', type: 'uint256' }, + ], + name: 'setChainGasAmount', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'uint256', name: 'newFeeRate', type: 'uint256' }, + ], + name: 'setProtocolFeeRate', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes4', name: 'interfaceId', type: 'bytes4' }], + name: 'supportsInterface', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'token', type: 'address' }, + { internalType: 'address', name: 'recipient', type: 'address' }, + ], + name: 'sweepProtocolFees', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + ], + + fastRouterV2: [ + { + inputs: [{ internalType: 'address', name: 'owner_', type: 'address' }], + stateMutability: 'nonpayable', + type: 'constructor', + }, + { inputs: [], name: 'DeadlineExceeded', type: 'error' }, + { + inputs: [], + name: 'FastBridgeRouterV2__OriginSenderNotSpecified', + type: 'error', + }, + { inputs: [], name: 'InsufficientOutputAmount', type: 'error' }, + { inputs: [], name: 'MsgValueIncorrect', type: 'error' }, + { inputs: [], name: 'PoolNotFound', type: 'error' }, + { inputs: [], name: 'TokenAddressMismatch', type: 'error' }, + { inputs: [], name: 'TokenNotContract', type: 'error' }, + { inputs: [], name: 'TokenNotETH', type: 'error' }, + { inputs: [], name: 'TokensIdentical', type: 'error' }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'address', + name: 'newFastBridge', + type: 'address', + }, + ], + name: 'FastBridgeSet', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'previousOwner', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'newOwner', + type: 'address', + }, + ], + name: 'OwnershipTransferred', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'address', + name: 'newSwapQuoter', + type: 'address', + }, + ], + name: 'SwapQuoterSet', + type: 'event', + }, + { + inputs: [], + name: 'GAS_REBATE_FLAG', + outputs: [{ internalType: 'bytes1', name: '', type: 'bytes1' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'recipient', type: 'address' }, + { internalType: 'address', name: 'tokenIn', type: 'address' }, + { internalType: 'uint256', name: 'amountIn', type: 'uint256' }, + { internalType: 'address', name: 'tokenOut', type: 'address' }, + { internalType: 'bytes', name: 'rawParams', type: 'bytes' }, + ], + name: 'adapterSwap', + outputs: [ + { internalType: 'uint256', name: 'amountOut', type: 'uint256' }, + ], + stateMutability: 'payable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'recipient', type: 'address' }, + { internalType: 'uint256', name: 'chainId', type: 'uint256' }, + { internalType: 'address', name: 'token', type: 'address' }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + { + components: [ + { internalType: 'address', name: 'routerAdapter', type: 'address' }, + { internalType: 'address', name: 'tokenOut', type: 'address' }, + { internalType: 'uint256', name: 'minAmountOut', type: 'uint256' }, + { internalType: 'uint256', name: 'deadline', type: 'uint256' }, + { internalType: 'bytes', name: 'rawParams', type: 'bytes' }, + ], + internalType: 'struct SwapQuery', + name: 'originQuery', + type: 'tuple', + }, + { + components: [ + { internalType: 'address', name: 'routerAdapter', type: 'address' }, + { internalType: 'address', name: 'tokenOut', type: 'address' }, + { internalType: 'uint256', name: 'minAmountOut', type: 'uint256' }, + { internalType: 'uint256', name: 'deadline', type: 'uint256' }, + { internalType: 'bytes', name: 'rawParams', type: 'bytes' }, + ], + internalType: 'struct SwapQuery', + name: 'destQuery', + type: 'tuple', + }, + ], + name: 'bridge', + outputs: [], + stateMutability: 'payable', + type: 'function', + }, + { + inputs: [], + name: 'fastBridge', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'tokenIn', type: 'address' }, + { internalType: 'address[]', name: 'rfqTokens', type: 'address[]' }, + { internalType: 'uint256', name: 'amountIn', type: 'uint256' }, + ], + name: 'getOriginAmountOut', + outputs: [ + { + components: [ + { internalType: 'address', name: 'routerAdapter', type: 'address' }, + { internalType: 'address', name: 'tokenOut', type: 'address' }, + { internalType: 'uint256', name: 'minAmountOut', type: 'uint256' }, + { internalType: 'uint256', name: 'deadline', type: 'uint256' }, + { internalType: 'bytes', name: 'rawParams', type: 'bytes' }, + ], + internalType: 'struct SwapQuery[]', + name: 'originQueries', + type: 'tuple[]', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'owner', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'renounceOwnership', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'fastBridge_', type: 'address' }, + ], + name: 'setFastBridge', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'swapQuoter_', type: 'address' }, + ], + name: 'setSwapQuoter', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'swapQuoter', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'newOwner', type: 'address' }], + name: 'transferOwnership', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { stateMutability: 'payable', type: 'receive' }, + ], +} diff --git a/packages/rfq-loadtest/src/index.ts b/packages/rfq-loadtest/src/index.ts new file mode 100644 index 0000000000..c064a36022 --- /dev/null +++ b/packages/rfq-loadtest/src/index.ts @@ -0,0 +1,496 @@ +import fs from 'fs/promises' + +import * as viemChains from 'viem/chains' +import { privateKeyToAccount } from 'viem/accounts' +import { + createPublicClient, + createWalletClient, + formatUnits, + http, + parseUnits, + publicActions, + PublicClient, + WalletClient, +} from 'viem' +import yaml from 'js-yaml' +import yargs from 'yargs' +import { hideBin } from 'yargs/helpers' +import axios from 'axios' +import { createNonceManager, jsonRpc } from 'viem/nonce' + +import { ABI } from './abi.js' +import { delay, print, getRandomInt } from './utils.js' + +const argv = await yargs(hideBin(process.argv)) + .option('configFile', { + alias: 'c', + type: 'string', + description: 'Path to the config file', + demandOption: true, + }) + .option('pKeyIndex', { + alias: 'p', + type: 'number', + description: 'Index of the private key', + demandOption: true, + }) + .help().argv + +const configFilePath = argv.configFile +const configFileContent = await fs.readFile(configFilePath, 'utf-8') + +let config: any +try { + config = yaml.load(configFileContent) as object + + if (typeof config !== 'object' || config === null) { + throw new Error() + } + + if ( + typeof config.VOLLEY_MILLISECONDS_BETWEEN !== 'number' || + typeof config.VOLLEY_MIN_COUNT !== 'number' || + typeof config.VOLLEY_MAX_COUNT !== 'number' + ) { + throw new Error('Invalid configuration values for volley settings') + } + + if (typeof config.CHAINS !== 'object' || config.CHAINS === null) { + throw new Error('Invalid configuration for CHAINS') + } + + if (typeof config.TEST_ROUTES !== 'object' || config.TEST_ROUTES === null) { + throw new Error('Invalid configuration for TEST_ROUTES') + } + + if (typeof config.TEST_BRIDGE_AMOUNT_UNITS !== 'number') { + throw new Error('Invalid configuration for TEST_BRIDGE_AMOUNT_UNITS') + } + if (typeof config.MINIMUM_GAS_UNITS !== 'number') { + throw new Error('Invalid configuration for MINIMUM_GAS_UNITS') + } + if (typeof config.REBALANCE_TO_UNITS !== 'number') { + throw new Error('Invalid configuration for REBALANCE_TO_UNITS') + } + + Object.entries(config.TEST_ROUTES).forEach( + ([route, details]: [string, any]) => { + if (typeof details !== 'object' || details === null) { + throw new Error(`Invalid configuration for route: ${route}`) + } + if ( + typeof details.fromChainId !== 'number' || + typeof details.toChainId !== 'number' || + typeof details.testDistributionPercentage !== 'number' + ) { + throw new Error(`Invalid configuration values for route: ${route}`) + } + } + ) +} catch (error: any) { + throw new Error( + `Failed to parse ${configFilePath}. Check your syntax, structure, data, and for duplicates. \n${error.message}` + ) +} + +const privateKeyIndex = argv.pKeyIndex +if (typeof privateKeyIndex !== 'number' || privateKeyIndex < 1) { + throw new Error('pKeyIndex must be a positive integer') +} + +const privateKeyName = `PRIVATE_KEY_${privateKeyIndex}` +const privateKey: `0x${string}` = config[privateKeyName] as `0x${string}` + +if (!privateKey) { + throw new Error(`${privateKeyName} is not defined in the config file`) +} + +// construct enriched versions of viem chain objects ("vChains") based on what was supplied in config file +const vChains: any = {} +Object.entries(config.CHAINS).forEach( + ([chainId, chainConfig]: [string, any]) => { + const viemChain = Object.values(viemChains).find( + (chain) => chain.id === parseInt(chainId, 10) + ) + if (!viemChain) { + throw new Error( + `No viem chain config found for chain ID ${chainId}. Bump viem version or add manually` + ) + } + + vChains[chainId] = { + ...viemChain, + ...chainConfig, + vCliRead: {} as PublicClient, + vCliSim: {} as WalletClient, + vCliWrite: {} as WalletClient, + } + } +) + +const nonceManager = createNonceManager({ + source: jsonRpc(), +}) + +const walletAccount = privateKeyToAccount(privateKey, { nonceManager }) + +print(`Using ${privateKeyName}: ${walletAccount.address}`) + +Object.keys(vChains).forEach((chainId: string) => { + const chain = vChains[chainId] + + chain.vCliRead = createPublicClient({ + chain, + transport: http(chain.rpcUrl_Read), + }) as PublicClient + + chain.vCliSim = createWalletClient({ + chain, + transport: http(chain.rpcUrl_Sim), + }).extend(publicActions) + + chain.vCliWrite = createWalletClient({ + chain, + transport: http(chain.rpcUrl_Write), + }).extend(publicActions) + + Promise.all([ + chain.vCliRead.readContract({ + address: chain.FastRouterAddr, + abi: ABI.fastRouterV2, + functionName: 'fastBridge', + }), + // not just used to report block height. also serves as connectivity test for all three clients. + chain.vCliRead.getBlockNumber(), + chain.vCliSim.getBlockNumber(), + chain.vCliWrite.getBlockNumber(), + ]).then( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + ([fastBridgeAddr, blockNumber_Read, blockNumber_Sim, blockNumber_Write]: [ + string, + bigint, + bigint, + bigint + ]) => { + print( + `Connected to chain ID ${chainId + .toString() + .padStart(7)}. FastBridge at: ${fastBridgeAddr.slice( + 0, + 6 + )}... Current block height: ${blockNumber_Read}` + ) + } + ) +}) + +let cachedResponseRFQ: any + +let sendCounter = 0 + +let lastAction: string = 'none' + +const testBridgeAmountUnits = config.TEST_BRIDGE_AMOUNT_UNITS + +mainFlow() + +async function mainFlow() { + await delay(1500) + + while (!walletAccount.address) { + print(`%ts Awaiting Initialization...`) + await delay(5000) + } + + checkBals() + + looper_getRequestParams() + + await delay(2500) + + while (!cachedResponseRFQ) { + print(`%ts Awaiting cached RFQ API response to populate...`) + await delay(5000) + } + + bridgeLooper() +} + +async function checkBals() { + for (;;) { + await Promise.all( + Object.keys(vChains).map(async (chainId: string) => { + const chain = vChains[chainId] + try { + const balance = await chain.vCliRead.getBalance({ + address: walletAccount.address, + }) + chain.balanceRaw = balance + chain.balanceUnits = formatUnits(balance, 18) + } catch (error: any) { + print( + `Error fetching balance for chain ID ${chainId}: ${error.message}` + ) + } + }) + ) + + await delay(15_000) + } +} + +const minGasUnits = config.MINIMUM_GAS_UNITS +const rebalToUnits = config.REBALANCE_TO_UNITS +async function bridgeLooper() { + + let retryCount = 0 + + for (;;) { + // Find the chain with the lowest balance below our minimum gas -- if any + const rebalToChain: any = Object.values(vChains).find( + (chain: any) => chain.balanceUnits < minGasUnits + ) + + if (rebalToChain) { + const rebalFromChain: any = Object.values(vChains).reduce( + (prev: any, current: any) => { + return prev.balanceUnits > current.balanceUnits ? prev : current + } + ) + + const rebalLabel = `%ts Rebal: ${rebalFromChain.id} > ${rebalToChain.id}` + + print(rebalLabel) + + // avoid repeating rebal actions. just loop until it lands on-chain. + if (lastAction === `rebal${rebalFromChain.id}>${rebalToChain.id}`) { + print( + `${rebalLabel} Last action was identical (${lastAction}). Not repeating. Re-evaluating momentarily...` + ) + + if (retryCount > 5) { + // abort after X attempts - if running in repeater mode this will effectively re-send the rebal tx if it is still needed + print(`${rebalLabel} Max retries. Exiting process...`) + await delay(1500) + process.exit() + } + + await delay(7500) + retryCount++ + continue + } + + retryCount=0 + + // leave rebalFrom chain with X units + const rebalAmount = rebalToUnits - rebalToChain.balanceUnits + + if (rebalFromChain.balanceUnits < rebalToUnits * 1.1) { + // if we hit this point, it indicates the wallet has no funds left to keep playing. hang process. + print( + `${rebalLabel} - Insuff Funds on From Chain ${rebalFromChain.balanceUnits}. Ending tests.` + ) + await delay(60_000) + return + } + + await sendBridge( + rebalFromChain, + rebalToChain, + Number(rebalAmount.toFixed(18)), + false, + rebalLabel + ) + + lastAction = `rebal${rebalFromChain.id}>${rebalToChain.id}` + + await delay(config.VOLLEY_MILLISECONDS_BETWEEN) + continue + } + + let fromChain: any + let toChain: any + + const totalPercentage: number = Object.values(config.TEST_ROUTES).reduce( + (acc: number, route: any) => acc + route.testDistributionPercentage, + 0 + ) + const randomizer: number = getRandomInt(1, totalPercentage) + let cumulative = 0 + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + for (const [route, details] of Object.entries(config.TEST_ROUTES) as [ + string, + any + ][]) { + cumulative += details.testDistributionPercentage + if (randomizer <= cumulative) { + fromChain = vChains[`${details.fromChainId}`] + toChain = vChains[`${details.toChainId}`] + break + } + } + + const countToSend = getRandomInt( + config.VOLLEY_MIN_COUNT, + config.VOLLEY_MAX_COUNT + ) + const printLabel = `%ts Batch${(sendCounter + countToSend) + .toString() + .padStart(5, '0')} of ${countToSend} : ${fromChain.id + .toString() + .padStart(7)} >> ${toChain.id.toString().padEnd(7)}` + + for (let i = 0; i < countToSend; i++) { + // sendCounter is applied as a tag on the amount just for sloppy tracking purposes. not actually important. + sendBridge( + fromChain, + toChain, + Number( + (testBridgeAmountUnits + sendCounter / 100000000000).toFixed(18) + ), + true, + printLabel + ) + sendCounter++ + await delay(50) + } + + lastAction = 'testVolley' + await delay(config.VOLLEY_MILLISECONDS_BETWEEN) + } +} + +async function getRequestParams( + fromChain: any, + toChain: any, + sendAmountUnits: number +) { + // 480 is not supported currently on the API - using Opti/Base as proxies. Can be improved later as needed. + if (toChain.id === 480) { + toChain = fromChain.id === 8453 ? vChains['10'] : vChains['8453'] + } + if (fromChain.id === 480) { + fromChain = toChain.id === 8453 ? vChains['10'] : vChains['8453'] + } + + const requestURL = `https://api.synapseprotocol.com/bridge?fromChain=${ + fromChain.id + }&toChain=${ + toChain.id + }&fromToken=${'0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'}&toToken=${'0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'}&amount=${sendAmountUnits}&destAddress=${ + walletAccount.address + }` + + let responseRFQ: any + let response: any + try { + response = await axios.request({ + url: requestURL, + }) + + if ((response.data?.length ?? 0) === 0) { + throw new Error(`No data returned from api`) + } + + responseRFQ = Array.isArray(response.data) + ? response.data.find( + (item: any) => item.bridgeModuleName === 'SynapseRFQ' + ) + : null + if (!responseRFQ) { + throw new Error(`No RFQ response returned from api`) + } + } catch (error: any) { + throw new Error( + `RFQ Api Fail: ${error.message.substr(0, 50)} -- ${requestURL}` + ) + } + + return responseRFQ +} + +async function looper_getRequestParams() { + for (;;) { + // in future iteration, this could be improved to dynamically pull a response for each route that is involved w/ testing. + // for now, all tests just use a cached route btwn two OP stacks as proxies for Worldchain tests -- because this is close enough. + cachedResponseRFQ = await getRequestParams( + vChains['8453'], + vChains['10'], + testBridgeAmountUnits + ) + + await delay(10_000) + } +} + +async function sendBridge( + fromChain: any, + toChain: any, + sendAmountUnits: number, + useCachedRequest: boolean, + printLabel: string +) { + const sendAmountRaw = parseUnits(sendAmountUnits.toString(), 18) + + printLabel = printLabel + ` ${sendAmountUnits} ETH` + + const _responseRFQ = useCachedRequest + ? cachedResponseRFQ + : await getRequestParams(fromChain, toChain, sendAmountUnits) + + const contractCall: any = { + address: fromChain.FastRouterAddr as `0x${string}`, + abi: ABI.fastRouterV2, + functionName: 'bridge', + account: walletAccount, + chain: fromChain, + args: [ + walletAccount.address, //recipient + toChain.id, // chainId + '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', // token + sendAmountRaw, // amount + [ + //originQuery + '0x0000000000000000000000000000000000000000', //routerAdapter + _responseRFQ.originQuery.tokenOut, //tokenOut + sendAmountRaw, //minAmountOut + BigInt(_responseRFQ.originQuery.deadline.hex), //deadline + _responseRFQ.originQuery.rawParams, //rawParms + ], + [ + //destQuery + '0x0000000000000000000000000000000000000000', //routerAdapter + _responseRFQ.destQuery.tokenOut, //tokenOut + BigInt(_responseRFQ.destQuery.minAmountOut.hex), //minAmountOut + BigInt(_responseRFQ.destQuery.deadline.hex), //deadline + _responseRFQ.destQuery.rawParams, //rawParms + ], + ], + value: sendAmountRaw, + } + + let estGasUnits + + try { + //@ts-ignore + estGasUnits = await fromChain.vCliSim.estimateContractGas(contractCall) + } catch (error: any) { + throw new Error(`${printLabel} Bridge Sim error: ${error.message}`) + } + + if (estGasUnits <= 50000n) { + throw new Error(`${printLabel} estimated gas units too low. possible error`) + } + + contractCall.gas = Math.floor(Number(estGasUnits) * 1.3) + + let txHash + try { + txHash = await fromChain.vCliWrite.writeContract(contractCall) + } catch (error: any) { + throw new Error(`${printLabel} Send failed: ${error.message}`) + } + + print(`${printLabel} Submitted ${txHash}`) +} diff --git a/packages/rfq-loadtest/src/pyRepeater.py b/packages/rfq-loadtest/src/pyRepeater.py new file mode 100644 index 0000000000..1094c43cd0 --- /dev/null +++ b/packages/rfq-loadtest/src/pyRepeater.py @@ -0,0 +1,11 @@ +import time +import subprocess +import sys +from datetime import datetime + +callStatement = sys.argv[1] + +while 1==1: + subprocess.call(f"{callStatement}", shell=True) + print (f"{datetime.now()} - Restarting...") + time.sleep(1) \ No newline at end of file diff --git a/packages/rfq-loadtest/src/utils.ts b/packages/rfq-loadtest/src/utils.ts new file mode 100644 index 0000000000..0785c8dcdd --- /dev/null +++ b/packages/rfq-loadtest/src/utils.ts @@ -0,0 +1,34 @@ +export function delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)) +} + +export function tStamp(startTimeStamp = 0) { + const timeCur = new Date().toISOString().replace('T', ' ').replace('Z', '') + + const timeDiff = + startTimeStamp > 0 + ? ` +${(Date.now() - startTimeStamp).toString().padStart(5)}ms` + : '' + + return `${timeCur}${timeDiff} - ` +} + +export function print(...outputs: any[]) { + outputs = outputs.map((output: any) => { + if (typeof output == 'string') { + // Replace %ts with formatted timestamp + output = output.replaceAll('%ts', tStamp()) + } + return output + }) + + console.log(...outputs) +} + +export function getRandomInt(min: number, max: number) { + if (min > max) { + // fix mistake inputs + ;[min, max] = [max, min] + } + return Math.floor(Math.random() * (max - min + 1)) + min +} diff --git a/packages/rfq-loadtest/tsconfig.json b/packages/rfq-loadtest/tsconfig.json new file mode 100644 index 0000000000..bf8c0320ea --- /dev/null +++ b/packages/rfq-loadtest/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "esnext", + "moduleResolution": "node", + "sourceMap": true, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} \ No newline at end of file diff --git a/packages/sdk-router/CHANGELOG.md b/packages/sdk-router/CHANGELOG.md index d8b4f3c109..a75b014286 100644 --- a/packages/sdk-router/CHANGELOG.md +++ b/packages/sdk-router/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.11.4](https://github.com/synapsecns/sanguine/compare/@synapsecns/sdk-router@0.11.3...@synapsecns/sdk-router@0.11.4) (2024-10-11) + +**Note:** Version bump only for package @synapsecns/sdk-router + + + + + ## [0.11.3](https://github.com/synapsecns/sanguine/compare/@synapsecns/sdk-router@0.11.2...@synapsecns/sdk-router@0.11.3) (2024-10-03) diff --git a/packages/sdk-router/package.json b/packages/sdk-router/package.json index a5e8afaf28..520390b750 100644 --- a/packages/sdk-router/package.json +++ b/packages/sdk-router/package.json @@ -1,7 +1,7 @@ { "name": "@synapsecns/sdk-router", "description": "An SDK for interacting with the Synapse Protocol", - "version": "0.11.3", + "version": "0.11.4", "license": "MIT", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/packages/synapse-constants/CHANGELOG.md b/packages/synapse-constants/CHANGELOG.md index 468bb18b63..67ce8de75f 100644 --- a/packages/synapse-constants/CHANGELOG.md +++ b/packages/synapse-constants/CHANGELOG.md @@ -3,6 +3,25 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.7.0](https://github.com/synapsecns/sanguine/compare/@synapsecns/synapse-constants@1.6.1...@synapsecns/synapse-constants@1.7.0) (2024-10-10) + + +### Features + +* **synapse-constants:** adds preinstall step ([#3269](https://github.com/synapsecns/sanguine/issues/3269)) ([acd61de](https://github.com/synapsecns/sanguine/commit/acd61de4846d9b23d7aa834b8f2eefcaae486c7d)) + + + + + +## [1.6.1](https://github.com/synapsecns/sanguine/compare/@synapsecns/synapse-constants@1.6.0...@synapsecns/synapse-constants@1.6.1) (2024-10-10) + +**Note:** Version bump only for package @synapsecns/synapse-constants + + + + + # 1.6.0 (2024-10-05) diff --git a/packages/synapse-constants/README.md b/packages/synapse-constants/README.md index 94f44f48c7..f0e21d4947 100644 --- a/packages/synapse-constants/README.md +++ b/packages/synapse-constants/README.md @@ -1,53 +1,41 @@ # Synapse Constants + [![npm](https://img.shields.io/npm/v/synapse-constants?style=flat-square)](https://www.npmjs.com/package/synapse-constants) This package contains the Synapse Protocol Token and Chain Constants - -# - - - ## Installation -```bash -npm install synapse-constants -``` - With Yarn: ```bash -yarn add synapse-constants +yarn add @synapsecns/synapse-constants ``` -## Usage - - -To restrict the assets and chains that are imported, you can create a "custom bridge list". From the set of all tokens imported from "bridgeable.ts" you can import specific tokens and use that as the custom list you use in your application. The same can be done for chains +## Build -## Usage -For maintenance, when new tokens are added to the bridge the following steps should be taken. +The following command will build the package locally -1. Regenerate bridgeMaps.ts - -```bash -yarn maps:generate +``` +yarn build ``` -2. Update Bridgeable.ts with the new token addresses (check all other variables like decimals/ symbols etc. ) +## Usage -3. Repackage and webpack all of the data +Importing supported tokens and chains: -```bash -yarn compile +```js +import { BRIDGABLE_TOKENS, CHAINS } from '@synapsecns/synapse-constants' ``` -4. Republish the npm package (make sure to update the version) +Importing a specific token: -```bash -npm publish +```js +import { USDC } from '@synapsecns/synapse-constants' ``` +## TODO -TODO: -- add the basic structure of the token type and the chain type to show accessibility for token logos, chain logos, and any additional information. +- [ ] Instructions on adding new chains +- [ ] Instructions on adding new tokens +- [ ] Instructions on generating new token route map diff --git a/packages/synapse-constants/package.json b/packages/synapse-constants/package.json index 37f3244327..42c99d8383 100644 --- a/packages/synapse-constants/package.json +++ b/packages/synapse-constants/package.json @@ -1,6 +1,6 @@ { "name": "@synapsecns/synapse-constants", - "version": "1.6.0", + "version": "1.7.0", "description": "This is an npm package that maintains all synapse constants", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", @@ -27,6 +27,7 @@ "lint:check": "eslint . --max-warnings=0 --config .eslintrc.cjs", "prepare": "rollup -c --bundleConfigAsCjs", "build": "rollup -c --bundleConfigAsCjs", + "preinstall": "command -v rollup >/dev/null 2>&1 && rollup -c --buildConfigAsCjs || echo 'rollup not found'", "prepublish": "yarn build", "maps:generate": "node ./src/scripts/generateMaps.cjs && node ./src/scripts/findMissing.cjs && yarn build" }, diff --git a/packages/synapse-interface/CHANGELOG.md b/packages/synapse-interface/CHANGELOG.md index bee3252c9c..735878108a 100644 --- a/packages/synapse-interface/CHANGELOG.md +++ b/packages/synapse-interface/CHANGELOG.md @@ -3,6 +3,41 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.40.10](https://github.com/synapsecns/sanguine/compare/@synapsecns/synapse-interface@0.40.9...@synapsecns/synapse-interface@0.40.10) (2024-10-15) + + +### Bug Fixes + +* **synapse-interface:** translation period ([#3297](https://github.com/synapsecns/sanguine/issues/3297)) ([fd75dbb](https://github.com/synapsecns/sanguine/commit/fd75dbbbb6fec8ad2c35d5a16efb3a6a4812231a)) + + + + + +## [0.40.9](https://github.com/synapsecns/sanguine/compare/@synapsecns/synapse-interface@0.40.8...@synapsecns/synapse-interface@0.40.9) (2024-10-15) + +**Note:** Version bump only for package @synapsecns/synapse-interface + + + + + +## [0.40.8](https://github.com/synapsecns/sanguine/compare/@synapsecns/synapse-interface@0.40.7...@synapsecns/synapse-interface@0.40.8) (2024-10-11) + +**Note:** Version bump only for package @synapsecns/synapse-interface + + + + + +## [0.40.7](https://github.com/synapsecns/sanguine/compare/@synapsecns/synapse-interface@0.40.6...@synapsecns/synapse-interface@0.40.7) (2024-10-09) + +**Note:** Version bump only for package @synapsecns/synapse-interface + + + + + ## [0.40.6](https://github.com/synapsecns/sanguine/compare/@synapsecns/synapse-interface@0.40.5...@synapsecns/synapse-interface@0.40.6) (2024-10-05) **Note:** Version bump only for package @synapsecns/synapse-interface diff --git a/packages/synapse-interface/components/Activity/Activity.tsx b/packages/synapse-interface/components/Activity/Activity.tsx index 5fe5012493..96a262e1d9 100644 --- a/packages/synapse-interface/components/Activity/Activity.tsx +++ b/packages/synapse-interface/components/Activity/Activity.tsx @@ -71,7 +71,7 @@ export const Activity = ({ visibility }: { visibility: boolean }) => { {viewingAddress && !isLoading && !hasHistoricalTransactions && (
- {t('No transactions in last 30 days.')} + {t('No transactions in last 30 days')}
)} diff --git a/packages/synapse-interface/components/layouts/StandardPageContainer.tsx b/packages/synapse-interface/components/layouts/StandardPageContainer.tsx index 8c408194f5..e1c5f16868 100644 --- a/packages/synapse-interface/components/layouts/StandardPageContainer.tsx +++ b/packages/synapse-interface/components/layouts/StandardPageContainer.tsx @@ -31,7 +31,7 @@ const StandardPageContainer = ({ if (unsupported) { unsupportedToaster = toast.error( - t('Connected to an unsupported network; Please switch networks.'), + t('Connected to an unsupported network; Please switch networks'), { id: 'unsupported-popup', duration: 5000 } ) } else { diff --git a/packages/synapse-interface/package.json b/packages/synapse-interface/package.json index bda524336a..152e129c94 100644 --- a/packages/synapse-interface/package.json +++ b/packages/synapse-interface/package.json @@ -1,6 +1,6 @@ { "name": "@synapsecns/synapse-interface", - "version": "0.40.6", + "version": "0.40.10", "private": true, "engines": { "node": ">=18.18.0" @@ -38,7 +38,7 @@ "@reduxjs/toolkit": "^1.9.5", "@rtk-query/graphql-request-base-query": "^2.2.0", "@segment/analytics-next": "^1.53.0", - "@synapsecns/sdk-router": "^0.11.3", + "@synapsecns/sdk-router": "^0.11.4", "@tailwindcss/aspect-ratio": "^0.4.2", "@tailwindcss/forms": "^0.5.3", "@tailwindcss/typography": "^0.5.9", diff --git a/packages/synapse-interface/public/blacklist.json b/packages/synapse-interface/public/blacklist.json index d4efb6cf64..21ddc573ba 100644 --- a/packages/synapse-interface/public/blacklist.json +++ b/packages/synapse-interface/public/blacklist.json @@ -552,5 +552,12 @@ "0x408d8e12c7ed8e5a7291fbD5E6164f41ecdA6B46", "0x278dF4492d16321b247660799FAD1A12dE152Dd1", "0x551BE68Cdf9Ce453ead61097649C34196d0bDb27", - "0x12AE4569d0e2B01857eD96D98cd4C9b09f21CA8b" + "0x12AE4569d0e2B01857eD96D98cd4C9b09f21CA8b", + "0x8bFE38d7c70F8953e701149E448c03E29ECcd4b0", + "0x6fdb264a876c811c7e101ee7a4f4fe7704ecbb72", + "0xb584050909a300fa0306b29f72e63dc4615b6f53", + "0xA963df55B326609a0cd205e85ca92d2a3c94DaB5", + "0x0605eDeE6a8b8b553caE09Abe83b2ebeb75516eC", + "0x4c968f6beecf1906710b08e8b472b8ba6e75f957", + "0xff3a8d02109393726a90c04d7afd76e2d571890e" ] diff --git a/packages/widget/CHANGELOG.md b/packages/widget/CHANGELOG.md index 9cab7694db..f50c33cd7e 100644 --- a/packages/widget/CHANGELOG.md +++ b/packages/widget/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.7.4](https://github.com/synapsecns/sanguine/compare/@synapsecns/widget@0.7.3...@synapsecns/widget@0.7.4) (2024-10-11) + +**Note:** Version bump only for package @synapsecns/widget + + + + + ## [0.7.3](https://github.com/synapsecns/sanguine/compare/@synapsecns/widget@0.7.2...@synapsecns/widget@0.7.3) (2024-10-03) **Note:** Version bump only for package @synapsecns/widget diff --git a/packages/widget/package.json b/packages/widget/package.json index a9af018a9b..8d5e6de077 100644 --- a/packages/widget/package.json +++ b/packages/widget/package.json @@ -1,7 +1,7 @@ { "name": "@synapsecns/widget", "description": "Widget library for interacting with the Synapse Protocol", - "version": "0.7.3", + "version": "0.7.4", "license": "MIT", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", @@ -68,7 +68,7 @@ "@ethersproject/providers": "^5.7.2", "@ethersproject/units": "^5.7.0", "@reduxjs/toolkit": "^2.0.1", - "@synapsecns/sdk-router": "^0.11.3", + "@synapsecns/sdk-router": "^0.11.4", "ethers": "^6.9.1", "lodash": "^4.17.21", "react-redux": "^9.0.2" diff --git a/services/omnirpc/http/client.go b/services/omnirpc/http/client.go index a5e1647014..c07ffaeb77 100644 --- a/services/omnirpc/http/client.go +++ b/services/omnirpc/http/client.go @@ -12,6 +12,7 @@ type Client interface { } // Request is a request builder. +// TODO: this needs to support tracing. type Request interface { // SetBody sets the request body SetBody(body []byte) Request diff --git a/services/omnirpc/modules/README.md b/services/omnirpc/modules/README.md index 000b17df9b..1af2170760 100644 --- a/services/omnirpc/modules/README.md +++ b/services/omnirpc/modules/README.md @@ -1,3 +1,6 @@ # Modules Modules are implementations that can modify the inputs to or outputs of an rpc call. They are meant to deal w/ specific application level limitations or requirements. For example, a module could be used to add a custom header to all requests, or to modify the response of a call to a specific service. These do not neccesarily emulate the original functionality of omnirpc and are run through seperate commands. + + +Mixins are meant to add metadata to make debugging easier. diff --git a/services/omnirpc/modules/confirmedtofinalized/finalizedproxy.go b/services/omnirpc/modules/confirmedtofinalized/finalizedproxy.go index aca1e6faaa..ce974daa3f 100644 --- a/services/omnirpc/modules/confirmedtofinalized/finalizedproxy.go +++ b/services/omnirpc/modules/confirmedtofinalized/finalizedproxy.go @@ -18,6 +18,7 @@ import ( "github.com/synapsecns/sanguine/ethergo/parser/rpc" "github.com/synapsecns/sanguine/services/omnirpc/collection" omniHTTP "github.com/synapsecns/sanguine/services/omnirpc/http" + "github.com/synapsecns/sanguine/services/omnirpc/modules/mixins" "github.com/synapsecns/sanguine/services/omnirpc/swagger" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" @@ -189,10 +190,7 @@ func (r *finalizedProxyImpl) checkShouldRequest(parentCtx context.Context, req r metrics.EndSpanWithErr(span, err) }() - tx := new(types.Transaction) - - hex := common.FromHex(string(bytes.ReplaceAll(req.Params[0], []byte{'"'}, []byte{}))) - err = tx.UnmarshalBinary(hex) + tx, err := mixins.ReqToTX(req) if err != nil { return false } diff --git a/services/omnirpc/modules/mixins/doc.go b/services/omnirpc/modules/mixins/doc.go new file mode 100644 index 0000000000..57b49f7e73 --- /dev/null +++ b/services/omnirpc/modules/mixins/doc.go @@ -0,0 +1,2 @@ +// Package mixins provides a set of mixins for the omnirpc module. +package mixins diff --git a/services/omnirpc/modules/mixins/helpers.go b/services/omnirpc/modules/mixins/helpers.go new file mode 100644 index 0000000000..1e7d3b51b7 --- /dev/null +++ b/services/omnirpc/modules/mixins/helpers.go @@ -0,0 +1,22 @@ +package mixins + +import ( + "bytes" + "fmt" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/synapsecns/sanguine/ethergo/parser/rpc" +) + +// ReqToTX converts a request to a transaction. +func ReqToTX(req rpc.Request) (tx *types.Transaction, err error) { + tx = new(types.Transaction) + + hex := common.FromHex(string(bytes.ReplaceAll(req.Params[0], []byte{'"'}, []byte{}))) + err = tx.UnmarshalBinary(hex) + if err != nil { + return nil, fmt.Errorf("could not unmarshal transaction: %w", err) + } + + return tx, nil +} diff --git a/services/omnirpc/modules/mixins/txsubmit.go b/services/omnirpc/modules/mixins/txsubmit.go new file mode 100644 index 0000000000..0d5d84fa26 --- /dev/null +++ b/services/omnirpc/modules/mixins/txsubmit.go @@ -0,0 +1,34 @@ +package mixins + +import ( + "context" + "github.com/synapsecns/sanguine/core/metrics" + "github.com/synapsecns/sanguine/ethergo/client" + "github.com/synapsecns/sanguine/ethergo/parser/rpc" + "github.com/synapsecns/sanguine/ethergo/util" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +// TxSubmitMixin is a mixin for tracking submitted transactions. +// it can be used to index additional data in otel regarding tx submission status. +func TxSubmitMixin(parentCtx context.Context, handler metrics.Handler, r rpc.Request) { + if client.RPCMethod(r.Method) != client.SendRawTransactionMethod { + return + } + + ctx, span := handler.Tracer().Start(parentCtx, "txsubmit", trace.WithAttributes(attribute.Int("txsubmit", r.ID))) + + var err error + defer func() { + metrics.EndSpanWithErr(span, err) + }() + + tx, err := ReqToTX(r) + if err != nil { + handler.ExperimentalLogger().Warnf(ctx, "could not convert request to transaction: %v", err) + return + } + + span.SetAttributes(util.TxToAttributes(tx)...) +} diff --git a/services/omnirpc/modules/receiptsbackup/receiptsbackup.go b/services/omnirpc/modules/receiptsbackup/receiptsbackup.go index 5cd02c1cac..f472d40105 100644 --- a/services/omnirpc/modules/receiptsbackup/receiptsbackup.go +++ b/services/omnirpc/modules/receiptsbackup/receiptsbackup.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/synapsecns/sanguine/services/omnirpc/modules/mixins" "io" "net/http" "time" @@ -136,9 +137,18 @@ func (r *receiptsProxyImpl) ProxyRequest(c *gin.Context) (err error) { } func (r *receiptsProxyImpl) processRequest(ctx context.Context, rpcRequest rpc.Request, requestID []byte) (resp omniHTTP.Response, err error) { + ctx, span := r.handler.Tracer().Start(ctx, "proxyrequest") + defer func() { + metrics.EndSpanWithErr(span, err) + }() + + mixins.TxSubmitMixin(ctx, r.handler, rpcRequest) + req := r.client.NewRequest() body, err := json.Marshal(rpcRequest) + span.AddEvent("request marshaled", trace.WithAttributes(attribute.String("body", string(body)))) + //nolint: exhaustive switch client.RPCMethod(rpcRequest.Method) { case client.TransactionReceiptByHashMethod: @@ -189,6 +199,8 @@ func (r *receiptsProxyImpl) processRequest(ctx context.Context, rpcRequest rpc.R return nil, fmt.Errorf("could not get response from RPC %s: %w", r.proxyURL, err) } + span.AddEvent("response returned", trace.WithAttributes(attribute.String("body", string(resp.Body())))) + return resp, nil } } diff --git a/services/rfq/relayer/quoter/quoter.go b/services/rfq/relayer/quoter/quoter.go index 3b5ecc8352..2cd3dfcf94 100644 --- a/services/rfq/relayer/quoter/quoter.go +++ b/services/rfq/relayer/quoter/quoter.go @@ -207,6 +207,15 @@ func (m *Manager) ShouldProcess(parentCtx context.Context, quote reldb.QuoteRequ return false, nil } + // check relay amount + maxRelayAmount := m.config.GetMaxRelayAmount(int(quote.Transaction.OriginChainId), quote.Transaction.OriginToken) + if maxRelayAmount != nil { + if quote.Transaction.OriginAmount.Cmp(maxRelayAmount) > 0 { + span.AddEvent("origin amount is greater than max relay amount") + return false, nil + } + } + // all checks have passed return true, nil } @@ -713,7 +722,7 @@ func (m *Manager) getOriginAmount(parentCtx context.Context, input QuoteInput) ( } } - // Finally, clip the quoteAmount by the dest balance + // Clip the quoteAmount by the dest balance if quoteAmount.Cmp(input.DestBalance) > 0 { span.AddEvent("quote amount greater than destination balance", trace.WithAttributes( attribute.String("quote_amount", quoteAmount.String()), @@ -722,6 +731,16 @@ func (m *Manager) getOriginAmount(parentCtx context.Context, input QuoteInput) ( quoteAmount = input.DestBalance } + // Clip the quoteAmount by the maxQuoteAmount + maxQuoteAmount := m.config.GetMaxRelayAmount(input.DestChainID, input.DestTokenAddr) + if maxQuoteAmount != nil && quoteAmount.Cmp(maxQuoteAmount) > 0 { + span.AddEvent("quote amount greater than max quote amount", trace.WithAttributes( + attribute.String("quote_amount", quoteAmount.String()), + attribute.String("max_quote_amount", maxQuoteAmount.String()), + )) + quoteAmount = maxQuoteAmount + } + // Deduct gas cost from the quote amount, if necessary quoteAmount, err = m.deductGasCost(ctx, quoteAmount, input.DestTokenAddr, input.DestChainID) if err != nil { diff --git a/services/rfq/relayer/quoter/quoter_test.go b/services/rfq/relayer/quoter/quoter_test.go index 1d6a52c7de..321d55b189 100644 --- a/services/rfq/relayer/quoter/quoter_test.go +++ b/services/rfq/relayer/quoter/quoter_test.go @@ -136,6 +136,13 @@ func (s *QuoterSuite) TestShouldProcess() { s.False(s.manager.ShouldProcess(s.GetTestContext(), quote)) s.manager.SetRelayPaused(false) s.True(s.manager.ShouldProcess(s.GetTestContext(), quote)) + + // Set max relay amount + originTokenCfg := s.config.Chains[int(s.origin)].Tokens["USDC"] + originTokenCfg.MaxRelayAmount = "900" // less than balance + s.config.Chains[int(s.origin)].Tokens["USDC"] = originTokenCfg + s.manager.SetConfig(s.config) + s.False(s.manager.ShouldProcess(s.GetTestContext(), quote)) } func (s *QuoterSuite) TestIsProfitable() { @@ -173,13 +180,23 @@ func (s *QuoterSuite) TestGetOriginAmount() { originAddr := common.HexToAddress("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48") balance := big.NewInt(1000_000_000) // 1000 USDC - setQuoteParams := func(quotePct, quoteOffset float64, minQuoteAmount, maxBalance string) { - s.config.BaseChainConfig.QuotePct = "ePct + type quoteParams struct { + quotePct float64 + quoteOffset float64 + minQuoteAmount string + maxBalance string + maxQuoteAmount string + } + + setQuoteParams := func(params quoteParams) { + s.config.BaseChainConfig.QuotePct = ¶ms.quotePct destTokenCfg := s.config.Chains[dest].Tokens["USDC"] - destTokenCfg.MinQuoteAmount = minQuoteAmount + destTokenCfg.MinQuoteAmount = params.minQuoteAmount + destTokenCfg.MaxRelayAmount = params.maxQuoteAmount originTokenCfg := s.config.Chains[origin].Tokens["USDC"] - originTokenCfg.QuoteOffsetBps = quoteOffset - originTokenCfg.MaxBalance = &maxBalance + originTokenCfg.QuoteOffsetBps = params.quoteOffset + originTokenCfg.MaxBalance = ¶ms.maxBalance + originTokenCfg.MaxRelayAmount = params.maxQuoteAmount s.config.Chains[dest].Tokens["USDC"] = destTokenCfg s.config.Chains[origin].Tokens["USDC"] = originTokenCfg s.manager.SetConfig(s.config) @@ -201,42 +218,85 @@ func (s *QuoterSuite) TestGetOriginAmount() { s.Equal(expectedAmount, quoteAmount) // Set QuotePct to 50 with MinQuoteAmount of 0; should be 50% of balance. - setQuoteParams(50, 0, "0", "0") + setQuoteParams(quoteParams{ + quotePct: 50, + quoteOffset: 0, + minQuoteAmount: "0", + maxBalance: "0", + }) quoteAmount, err = s.manager.GetOriginAmount(s.GetTestContext(), input) s.NoError(err) expectedAmount = big.NewInt(500_000_000) s.Equal(expectedAmount, quoteAmount) // Set QuotePct to 50 with QuoteOffset of -1%. Should be 1% less than 50% of balance. - setQuoteParams(50, -100, "0", "0") + setQuoteParams(quoteParams{ + quotePct: 50, + quoteOffset: -100, + minQuoteAmount: "0", + maxBalance: "0", + }) quoteAmount, err = s.manager.GetOriginAmount(s.GetTestContext(), input) s.NoError(err) expectedAmount = big.NewInt(495_000_000) s.Equal(expectedAmount, quoteAmount) // Set QuotePct to 25 with MinQuoteAmount of 500; should be 50% of balance. - setQuoteParams(25, 0, "500", "0") + setQuoteParams(quoteParams{ + quotePct: 25, + quoteOffset: 0, + minQuoteAmount: "500", + maxBalance: "0", + }) quoteAmount, err = s.manager.GetOriginAmount(s.GetTestContext(), input) s.NoError(err) expectedAmount = big.NewInt(500_000_000) s.Equal(expectedAmount, quoteAmount) // Set QuotePct to 25 with MinQuoteAmount of 500; should be 50% of balance. - setQuoteParams(25, 0, "500", "0") + setQuoteParams(quoteParams{ + quotePct: 25, + quoteOffset: 0, + minQuoteAmount: "500", + maxBalance: "0", + }) quoteAmount, err = s.manager.GetOriginAmount(s.GetTestContext(), input) s.NoError(err) expectedAmount = big.NewInt(500_000_000) s.Equal(expectedAmount, quoteAmount) // Set QuotePct to 25 with MinQuoteAmount of 1500; should be total balance. - setQuoteParams(25, 0, "1500", "0") + setQuoteParams(quoteParams{ + quotePct: 25, + quoteOffset: 0, + minQuoteAmount: "1500", + maxBalance: "0", + }) quoteAmount, err = s.manager.GetOriginAmount(s.GetTestContext(), input) s.NoError(err) expectedAmount = big.NewInt(1000_000_000) s.Equal(expectedAmount, quoteAmount) + // Set QuotePct to 100 with MinQuoteAmount of 0 and MaxRelayAmount of 500; should be 500. + setQuoteParams(quoteParams{ + quotePct: 100, + quoteOffset: 0, + minQuoteAmount: "0", + maxBalance: "0", + maxQuoteAmount: "500", + }) + quoteAmount, err = s.manager.GetOriginAmount(s.GetTestContext(), input) + s.NoError(err) + expectedAmount = big.NewInt(500_000_000) + s.Equal(expectedAmount, quoteAmount) + // Set QuotePct to 25 with MinQuoteAmount of 1500 and MaxBalance of 1200; should be 200. - setQuoteParams(25, 0, "1500", "1200") + setQuoteParams(quoteParams{ + quotePct: 25, + quoteOffset: 0, + minQuoteAmount: "1500", + maxBalance: "1200", + }) quoteAmount, err = s.manager.GetOriginAmount(s.GetTestContext(), input) s.NoError(err) expectedAmount = big.NewInt(200_000_000) diff --git a/services/rfq/relayer/relconfig/config.go b/services/rfq/relayer/relconfig/config.go index a4449bf8db..220627e13a 100644 --- a/services/rfq/relayer/relconfig/config.go +++ b/services/rfq/relayer/relconfig/config.go @@ -124,6 +124,8 @@ type TokenConfig struct { PriceUSD float64 `yaml:"price_usd"` // MinQuoteAmount is the minimum amount to quote for this token in human-readable units. MinQuoteAmount string `yaml:"min_quote_amount"` + // MaxRelayAmount is the maximum amount to quote and relay for this token in human-readable units. + MaxRelayAmount string `yaml:"max_relay_amount"` // RebalanceMethods are the supported methods for rebalancing. RebalanceMethods []string `yaml:"rebalance_methods"` // MaintenanceBalancePct is the percentage of the total balance under which a rebalance will be triggered. diff --git a/services/rfq/relayer/relconfig/getters.go b/services/rfq/relayer/relconfig/getters.go index 2cb4880712..a3fbb25cc7 100644 --- a/services/rfq/relayer/relconfig/getters.go +++ b/services/rfq/relayer/relconfig/getters.go @@ -746,6 +746,41 @@ func (c Config) GetMinQuoteAmount(chainID int, addr common.Address) *big.Int { return quoteAmountScaled } +var defaultMaxRelayAmount *big.Int // nil + +// GetMaxRelayAmount returns the quote amount for the given chain and address. +// Note that this getter returns the value in native token decimals. +func (c Config) GetMaxRelayAmount(chainID int, addr common.Address) *big.Int { + chainCfg, ok := c.Chains[chainID] + if !ok { + return defaultMaxRelayAmount + } + + var tokenCfg *TokenConfig + for _, cfg := range chainCfg.Tokens { + if common.HexToAddress(cfg.Address).Hex() == addr.Hex() { + cfgCopy := cfg + tokenCfg = &cfgCopy + break + } + } + if tokenCfg == nil { + return defaultMaxRelayAmount + } + quoteAmountFlt, ok := new(big.Float).SetString(tokenCfg.MaxRelayAmount) + if !ok { + return defaultMaxRelayAmount + } + if quoteAmountFlt.Cmp(big.NewFloat(0)) <= 0 { + return defaultMaxRelayAmount + } + + // Scale the minQuoteAmount by the token decimals. + denomDecimalsFactor := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(tokenCfg.Decimals)), nil) + quoteAmountScaled, _ := new(big.Float).Mul(quoteAmountFlt, new(big.Float).SetInt(denomDecimalsFactor)).Int(nil) + return quoteAmountScaled +} + var defaultMinRebalanceAmount = big.NewInt(1000) // GetMinRebalanceAmount returns the min rebalance amount for the given chain and address. diff --git a/services/rfq/relayer/service/handlers.go b/services/rfq/relayer/service/handlers.go index fc1bc1abe8..dbba3dea6e 100644 --- a/services/rfq/relayer/service/handlers.go +++ b/services/rfq/relayer/service/handlers.go @@ -47,7 +47,12 @@ func (r *Relayer) handleBridgeRequestedLog(parentCtx context.Context, req *fastb return nil } - defer unlocker.Unlock() + shouldUnlock := true + defer func() { + if shouldUnlock { + unlocker.Unlock() + } + }() _, err = r.db.GetQuoteRequestByID(ctx, req.TransactionId) // expect no results @@ -120,12 +125,17 @@ func (r *Relayer) handleBridgeRequestedLog(parentCtx context.Context, req *fastb if err != nil { return fmt.Errorf("could not get quote request handler: %w", err) } - // Forward instead of lock since we called lock above. - fwdErr := qr.Forward(ctx, dbReq) - if fwdErr != nil { - logger.Errorf("could not forward to handle seen: %w", fwdErr) - span.AddEvent("could not forward to handle seen") - } + + // Forward in new goroutine and retain the lock. + shouldUnlock = false + go func() { + defer unlocker.Unlock() + fwdErr := qr.Forward(ctx, dbReq) + if fwdErr != nil { + logger.Errorf("could not forward to handle seen: %w", fwdErr) + span.AddEvent(fmt.Sprintf("could not forward to handle seen: %s", fwdErr.Error())) + } + }() return nil } diff --git a/yarn.lock b/yarn.lock index 61c7578a54..bbf0529519 100644 --- a/yarn.lock +++ b/yarn.lock @@ -55,7 +55,7 @@ resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz#63430d04bd8c5e74f8d7d049338f1cd9d4f02069" integrity sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw== -"@adraffy/ens-normalize@^1.8.8": +"@adraffy/ens-normalize@1.11.0", "@adraffy/ens-normalize@^1.8.8": version "1.11.0" resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.11.0.tgz#42cc67c5baa407ac25059fcd7d405cc5ecdb0c33" integrity sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg== @@ -6007,7 +6007,7 @@ dependencies: "@noble/hashes" "1.4.0" -"@noble/curves@^1.4.0": +"@noble/curves@1.6.0", "@noble/curves@^1.4.0", "@noble/curves@~1.6.0": version "1.6.0" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.6.0.tgz#be5296ebcd5a1730fccea4786d420f87abfeb40b" integrity sha512-TlaHRXDehJuRNR9TfZDNQ45mMEd5dwUwmicsafcIX4SsNiqnCHKjE/1alYPd/lDRVhxdhUAlv8uEhMCI5zjIJQ== @@ -7122,7 +7122,7 @@ resolved "https://registry.yarnpkg.com/@safe-global/safe-gateway-typescript-sdk/-/safe-gateway-typescript-sdk-3.22.2.tgz#d4ff9972e58f9344fc95f8d41b2ec6517baa8e79" integrity sha512-Y0yAxRaB98LFp2Dm+ACZqBSdAmI3FlpH/LjxOZ94g/ouuDJecSq0iR26XZ5QDuEL8Rf+L4jBJaoDC08CD0KkJw== -"@scure/base@^1.1.3", "@scure/base@~1.1.0", "@scure/base@~1.1.2", "@scure/base@~1.1.6", "@scure/base@~1.1.8": +"@scure/base@^1.1.3", "@scure/base@~1.1.0", "@scure/base@~1.1.2", "@scure/base@~1.1.6", "@scure/base@~1.1.7", "@scure/base@~1.1.8": version "1.1.9" resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.9.tgz#e5e142fbbfe251091f9c5f1dd4c834ac04c3dbd1" integrity sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg== @@ -7154,6 +7154,15 @@ "@noble/hashes" "~1.4.0" "@scure/base" "~1.1.6" +"@scure/bip32@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.5.0.tgz#dd4a2e1b8a9da60e012e776d954c4186db6328e6" + integrity sha512-8EnFYkqEQdnkuGBVpCzKxyIwDCBLDVj3oiX0EKUFre/tOjL/Hqba1D6n/8RcmaQy4f95qQFrO2A8Sr6ybh4NRw== + dependencies: + "@noble/curves" "~1.6.0" + "@noble/hashes" "~1.5.0" + "@scure/base" "~1.1.7" + "@scure/bip39@1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.1.1.tgz#b54557b2e86214319405db819c4b6a370cf340c5" @@ -9324,7 +9333,7 @@ jest-diff "^25.2.1" pretty-format "^25.2.1" -"@types/js-yaml@^4.0.0": +"@types/js-yaml@^4.0.0", "@types/js-yaml@^4.0.9": version "4.0.9" resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.9.tgz#cd82382c4f902fed9691a2ed79ec68c5898af4c2" integrity sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg== @@ -9469,6 +9478,13 @@ dependencies: undici-types "~6.19.2" +"@types/node@^22.7.5": + version "22.7.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.7.5.tgz#cfde981727a7ab3611a481510b473ae54442b92b" + integrity sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ== + dependencies: + undici-types "~6.19.2" + "@types/normalize-package-data@^2.4.0": version "2.4.4" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz#56e2cc26c397c038fab0e3a917a12d5c5909e901" @@ -9859,7 +9875,7 @@ dependencies: "@types/yargs-parser" "*" -"@types/yargs@^17.0.8": +"@types/yargs@^17.0.33", "@types/yargs@^17.0.8": version "17.0.33" resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.33.tgz#8c32303da83eec050a84b3c7ae7b9f922d13e32d" integrity sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA== @@ -11090,6 +11106,11 @@ abitype@1.0.5: resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.0.5.tgz#29d0daa3eea867ca90f7e4123144c1d1270774b6" integrity sha512-YzDhti7cjlfaBhHutMaboYB21Ha3rXR9QTkNJFzYC4kC8YclaiwPBBBJY8ejFdu2wnJeZCVZSMlQJ7fi8S6hsw== +abitype@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.0.6.tgz#76410903e1d88e34f1362746e2d407513c38565b" + integrity sha512-MMSqYh4+C/aVqI2RQaWqbvI4Kxo5cQV40WQ4QFtDnNzCkqChm8MuENhElmynZlO0qUy/ObkEUaXtKqYnx1Kp3A== + abitype@^0.10.2: version "0.10.3" resolved "https://registry.yarnpkg.com/abitype/-/abitype-0.10.3.tgz#27ce7a7cdb9a80ccd732a3f3cf1ce6ff05266fce" @@ -21583,6 +21604,11 @@ isows@1.0.4: resolved "https://registry.yarnpkg.com/isows/-/isows-1.0.4.tgz#810cd0d90cc4995c26395d2aa4cfa4037ebdf061" integrity sha512-hEzjY+x9u9hPmBom9IIAqdJCwNLax+xrPb51vEPpERoFlIxgmZcHzsT5jKG06nvInKOBGvReAVz80Umed5CczQ== +isows@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/isows/-/isows-1.0.6.tgz#0da29d706fa51551c663c627ace42769850f86e7" + integrity sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw== + isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" @@ -33985,6 +34011,11 @@ typescript@^5.0.4, typescript@^5.2.2, typescript@^5.3.2, typescript@^5.3.3, type resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.2.tgz#d1de67b6bef77c41823f822df8f0b3bcff60a5a0" integrity sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw== +typescript@^5.6.3: + version "5.6.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.3.tgz#5f3449e31c9d94febb17de03cc081dd56d81db5b" + integrity sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw== + typescript@~5.2.2: version "5.2.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78" @@ -35024,6 +35055,21 @@ viem@^2.1.1, viem@^2.13.6, viem@^2.21.6: webauthn-p256 "0.0.5" ws "8.17.1" +viem@^2.21.19: + version "2.21.25" + resolved "https://registry.yarnpkg.com/viem/-/viem-2.21.25.tgz#5e4a7c6a8543396f67ef221ea5d2226321f000b8" + integrity sha512-fQbFLVW5RjC1MwjelmzzDygmc2qMfY17NruAIIdYeiB8diQfhqsczU5zdGw/jTbmNXbKoYnSdgqMb8MFZcbZ1w== + dependencies: + "@adraffy/ens-normalize" "1.11.0" + "@noble/curves" "1.6.0" + "@noble/hashes" "1.5.0" + "@scure/bip32" "1.5.0" + "@scure/bip39" "1.4.0" + abitype "1.0.6" + isows "1.0.6" + webauthn-p256 "0.0.10" + ws "8.18.0" + vite-node@^1.0.2: version "1.6.0" resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-1.6.0.tgz#2c7e61129bfecc759478fa592754fd9704aaba7f" @@ -35891,6 +35937,14 @@ web3@1.7.4: web3-shh "1.7.4" web3-utils "1.7.4" +webauthn-p256@0.0.10: + version "0.0.10" + resolved "https://registry.yarnpkg.com/webauthn-p256/-/webauthn-p256-0.0.10.tgz#877e75abe8348d3e14485932968edf3325fd2fdd" + integrity sha512-EeYD+gmIT80YkSIDb2iWq0lq2zbHo1CxHlQTeJ+KkCILWpVy3zASH3ByD4bopzfk0uCwXxLqKGLqp2W4O28VFA== + dependencies: + "@noble/curves" "^1.4.0" + "@noble/hashes" "^1.4.0" + webauthn-p256@0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/webauthn-p256/-/webauthn-p256-0.0.5.tgz#0baebd2ba8a414b21cc09c0d40f9dd0be96a06bd" @@ -36544,6 +36598,11 @@ ws@8.17.1, ws@~8.17.1: resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== +ws@8.18.0, ws@^8.12.0, ws@^8.13.0, ws@^8.17.1, ws@^8.2.3: + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== + ws@^3.0.0: version "3.3.3" resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz#f1cf84fe2d5e901ebce94efaece785f187a228f2" @@ -36558,11 +36617,6 @@ ws@^7.0.0, ws@^7.3.1, ws@^7.4.6, ws@^7.5.1: resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== -ws@^8.12.0, ws@^8.13.0, ws@^8.17.1, ws@^8.2.3: - version "8.18.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" - integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== - x-default-browser@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/x-default-browser/-/x-default-browser-0.4.0.tgz#70cf0da85da7c0ab5cb0f15a897f2322a6bdd481"