diff --git a/.gitattributes b/.gitattributes index 19f0650d7e..c17ad86ad0 100644 --- a/.gitattributes +++ b/.gitattributes @@ -16,7 +16,7 @@ # ignore ifacemaker files *_generated.go linguist-generated contrib/opbot/generated/* linguist-generated -*.contractinfo.json linguist-generated=true +*.contractinfo.json linguist-generated # svg should be treated as a binary https://git.io/JE2VK @@ -24,3 +24,6 @@ contrib/opbot/generated/* linguist-generated *.sol linguist-language=Solidity .vscode/*.json linguist-language=jsonc + +# foundry deploy data +packages/**/deployments/*.json linguist-generated diff --git a/_config.yml b/_config.yml new file mode 100644 index 0000000000..1c6413e4fd --- /dev/null +++ b/_config.yml @@ -0,0 +1,2 @@ +# jekyll config. Jekyll is used here only for helm. +exclude: [docs] diff --git a/contrib/opbot/botmd/commands.go b/contrib/opbot/botmd/commands.go index bf49c6a98f..6dcc8bc842 100644 --- a/contrib/opbot/botmd/commands.go +++ b/contrib/opbot/botmd/commands.go @@ -320,6 +320,10 @@ func (b *Bot) rfqRefund() *slacker.CommandDefinition { }) if err != nil { log.Printf("error submitting refund: %v\n", err) + _, err := ctx.Response().Reply("error submitting refund") + if err != nil { + log.Println(err) + } return } @@ -331,17 +335,19 @@ func (b *Bot) rfqRefund() *slacker.CommandDefinition { if err != nil || !status.HasTx() { b.logger.Errorf(ctx, "error fetching quote request: %v", err) return fmt.Errorf("error fetching quote request: %w", err) + } else if !status.HasTx() { + return fmt.Errorf("no transaction hash found yet") } return nil }, - retry.WithMaxAttempts(5), - retry.WithMaxAttemptTime(30*time.Second), + retry.WithMaxTotalTime(1*time.Minute), ) + if err != nil { b.logger.Errorf(ctx.Context(), "error fetching quote request: %v", err) _, err := ctx.Response().Reply(fmt.Sprintf("refund submitted with nonce %d", nonce)) if err != nil { - log.Println(err) + b.logger.Errorf(ctx.Context(), "error fetching quote request: %v", err) } return } @@ -351,6 +357,7 @@ func (b *Bot) rfqRefund() *slacker.CommandDefinition { if err != nil { log.Println(err) } + }, } } diff --git a/cspell.json b/cspell.json index 67d9fd645c..059350f412 100644 --- a/cspell.json +++ b/cspell.json @@ -68,6 +68,8 @@ "ofac", "omnirpc", "opaciyt", + "permissionless", + "permissioned", "permissionlessly", "persistor", "popperjs", diff --git a/docs/bridge/CHANGELOG.md b/docs/bridge/CHANGELOG.md index 989c9fd4ee..ccf73ae349 100644 --- a/docs/bridge/CHANGELOG.md +++ b/docs/bridge/CHANGELOG.md @@ -3,6 +3,78 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.5.13](https://github.com/synapsecns/sanguine/compare/@synapsecns/bridge-docs@0.5.12...@synapsecns/bridge-docs@0.5.13) (2024-12-08) + +**Note:** Version bump only for package @synapsecns/bridge-docs + + + + + +## [0.5.12](https://github.com/synapsecns/sanguine/compare/@synapsecns/bridge-docs@0.5.11...@synapsecns/bridge-docs@0.5.12) (2024-12-06) + +**Note:** Version bump only for package @synapsecns/bridge-docs + + + + + +## [0.5.11](https://github.com/synapsecns/sanguine/compare/@synapsecns/bridge-docs@0.5.10...@synapsecns/bridge-docs@0.5.11) (2024-12-02) + +**Note:** Version bump only for package @synapsecns/bridge-docs + + + + + +## [0.5.10](https://github.com/synapsecns/sanguine/compare/@synapsecns/bridge-docs@0.5.9...@synapsecns/bridge-docs@0.5.10) (2024-12-02) + +**Note:** Version bump only for package @synapsecns/bridge-docs + + + + + +## [0.5.9](https://github.com/synapsecns/sanguine/compare/@synapsecns/bridge-docs@0.5.8...@synapsecns/bridge-docs@0.5.9) (2024-12-02) + +**Note:** Version bump only for package @synapsecns/bridge-docs + + + + + +## [0.5.8](https://github.com/synapsecns/sanguine/compare/@synapsecns/bridge-docs@0.5.7...@synapsecns/bridge-docs@0.5.8) (2024-12-01) + +**Note:** Version bump only for package @synapsecns/bridge-docs + + + + + +## [0.5.7](https://github.com/synapsecns/sanguine/compare/@synapsecns/bridge-docs@0.5.6...@synapsecns/bridge-docs@0.5.7) (2024-11-30) + +**Note:** Version bump only for package @synapsecns/bridge-docs + + + + + +## [0.5.6](https://github.com/synapsecns/sanguine/compare/@synapsecns/bridge-docs@0.5.5...@synapsecns/bridge-docs@0.5.6) (2024-11-30) + +**Note:** Version bump only for package @synapsecns/bridge-docs + + + + + +## [0.5.5](https://github.com/synapsecns/sanguine/compare/@synapsecns/bridge-docs@0.5.4...@synapsecns/bridge-docs@0.5.5) (2024-11-19) + +**Note:** Version bump only for package @synapsecns/bridge-docs + + + + + ## [0.5.4](https://github.com/synapsecns/sanguine/compare/@synapsecns/bridge-docs@0.5.3...@synapsecns/bridge-docs@0.5.4) (2024-11-13) **Note:** Version bump only for package @synapsecns/bridge-docs diff --git a/docs/bridge/README.md b/docs/bridge/README.md index d3aa9a52ee..2fb9c0e964 100644 --- a/docs/bridge/README.md +++ b/docs/bridge/README.md @@ -2,6 +2,7 @@ This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator. + ## Generating API Docs diff --git a/docs/bridge/docs/01-About/04-SYN.md b/docs/bridge/docs/01-About/04-SYN.md new file mode 100644 index 0000000000..09bbd9239c --- /dev/null +++ b/docs/bridge/docs/01-About/04-SYN.md @@ -0,0 +1,24 @@ +--- +title: $SYN Token +--- + +# $SYN Token + +$SYN is the governance token for the Synapse Protocol. There are no unlocks, all future $SYN emissions are goverened by the [DAO](/docs/About/DAO). + +Liquidity for the [$SYN](https://coinmarketcap.com/currencies/synapse-2/) token can be found here: + +| Venue | Link | +| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Coinbase | `https://www.coinbase.com/price/synapse` [↗](https://www.coinbase.com/price/synapse) | +| Sushi | `https://www.sushi.com/ethereum/pool/v2/0x4a86c01d67965f8cb3d0aaa2c655705e64097c31` [↗](https://www.sushi.com/ethereum/pool/v2/0x4a86c01d67965f8cb3d0aaa2c655705e64097c31) | +| Revolut | `https://www.revolut.com/crypto/price/syn` [↗](https://www.revolut.com/crypto/price/syn) | +| Binance (Spot) | `https://www.binance.com/en/trade/SYN_USDT?type=spot` [↗](https://www.binance.com/en/trade/SYN_USDT?type=spot) | +| Binance (Perpetuals) | `https://www.binance.com/en/futures/SYNUSDT` [↗](https://www.binance.com/en/futures/SYNUSDT) | +| Bybit (SYN/USDT) | `https://www.bybit.com/trade/usdt/SYNUSDT` [↗](https://www.bybit.com/trade/usdt/SYNUSDT) | +| HTX | `https://www.htx.com/price/syn/` [↗](https://www.htx.com/price/syn/) | +| Kraken | `https://www.kraken.com/prices/synapse` [↗](https://www.kraken.com/prices/synapse) | +| KuCoin | `https://www.kucoin.com/price/SYN` [↗](https://www.kucoin.com/price/SYN) | +
+ +All $SYN token addresses can be found [here](/docs/Contracts/SYN). diff --git a/docs/bridge/docs/01-About/index.md b/docs/bridge/docs/01-About/index.md index a70db958b6..962d9f930e 100644 --- a/docs/bridge/docs/01-About/index.md +++ b/docs/bridge/docs/01-About/index.md @@ -41,7 +41,7 @@ Synapse Router automatically determines the appropriate router for each Bridge t * **[Synapse Router](/docs/Routers/Synapse-Router)** – Returns and executes quotes for supported interchain transactions. * **[CCTP](/docs/Routers/CCTP)** – Native router for USDC transactions. -* **[RFQ](/docs/Routers/RFQ)** – Relayers bid for the right to provide immediate delivery. +* **[RFQ](/docs/RFQ)** – Relayers bid for the right to provide immediate delivery.
diff --git a/docs/bridge/docs/02-Bridge/01-SDK.md b/docs/bridge/docs/02-Bridge/01-SDK.md index d4e1f141fc..290bf7227b 100644 --- a/docs/bridge/docs/02-Bridge/01-SDK.md +++ b/docs/bridge/docs/02-Bridge/01-SDK.md @@ -153,7 +153,7 @@ Use `bridgeQuote` to request a Bridge transaction Returns an array all possible bridge quotes, with the first item in the array being the cheapest route. -Quotes are returned from various Bridge "types" such as [CCTP](/docs/Routers/CCTP) or [RFQ](/docs/Routers/RFQ). More information is available on the [Synapse Router](/docs/Routers/Synapse-Router) page, or [SynapseCNS](https://github.com/synapsecns/sdk-router) Github repository. +Quotes are returned from various Bridge "types" such as [CCTP](/docs/Routers/CCTP) or [RFQ](/docs/RFQ). More information is available on the [Synapse Router](/docs/Routers/Synapse-Router) page, or [SynapseCNS](https://github.com/synapsecns/sdk-router) Github repository. ## Examples diff --git a/docs/bridge/docs/02-Bridge/02-REST-API.md b/docs/bridge/docs/02-Bridge/02-REST-API.md index 647d02d595..fc9ffbe3ce 100644 --- a/docs/bridge/docs/02-Bridge/02-REST-API.md +++ b/docs/bridge/docs/02-Bridge/02-REST-API.md @@ -4,7 +4,16 @@ title: REST API # REST API -Get read-only data from on-chain Synapse contracts, and generate Bridge and Swap quotes, plus additional transaction information. +The Synapse REST API is a read-only API that allows you to integrate the Synapse liquidity network into your application. + +Through HTTP requests, developers can integrate Synapse cross-chain tokens and liquidity transfers dynamically into their applications. Developers can retrieve quotes, as well as generate the relevant call data for Synapse Bridges and Swaps. Example requests can be found below in the [API-docs](#api-docs) section. + + +The Synapse REST API is built on top of the [Synapse Bridge SDK](https://docs.synapseprotocol.com/docs/Bridge/SDK). + + +The API is available at [`https://api.synapseprotocol.com/`](https://api.synapseprotocol.com/). + ## API-docs @@ -17,6 +26,7 @@ Get read-only data from on-chain Synapse contracts, and generate Bridge and Swap | Date | Description | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | 2024‑10‑01 | [https://synapse-rest-api-v2.herokuapp.com/](https://synapse-rest-api-v2.herokuapp.com/) is no longer maintained and has been fully deprecated as of October 2024. | +| 2024‑11‑19 | [https://api.synapseprotocol.com/](https://api.synapseprotocol.com/) the /bridgeTxInfo endpoint has been consolidated into the /bridge endpoint, which now returns call data | ## Support diff --git a/docs/bridge/docs/04-Routers/RFQ/API/get-contract-addresses.api.mdx b/docs/bridge/docs/03-RFQ/10-Quoting/Quoter API/get-contract-addresses.api.mdx similarity index 100% rename from docs/bridge/docs/04-Routers/RFQ/API/get-contract-addresses.api.mdx rename to docs/bridge/docs/03-RFQ/10-Quoting/Quoter API/get-contract-addresses.api.mdx diff --git a/docs/bridge/docs/03-RFQ/10-Quoting/Quoter API/get-open-quote-requests.api.mdx b/docs/bridge/docs/03-RFQ/10-Quoting/Quoter API/get-open-quote-requests.api.mdx new file mode 100644 index 0000000000..919892326f --- /dev/null +++ b/docs/bridge/docs/03-RFQ/10-Quoting/Quoter API/get-open-quote-requests.api.mdx @@ -0,0 +1,244 @@ +--- +id: get-open-quote-requests +title: "Get open quote requests" +description: "Get all open quote requests that are currently in Received or Pending status." +sidebar_label: "Get open quote requests" +hide_title: true +hide_table_of_contents: true +api: eJytU01v2zAM/SsCz8pS7OhbD0MxDNiKfWGAERiMxNpqbUmh6LSG4f8+SE2WtvB22kkA+UQ+Pj7OYCkZdlFc8FDBDYnCvlchkleHMQgppsNISZKSDkUhkzIjM3npJ+W8+kqG3JGsCqxuyVvnW5UEZUzvQANTisEnSlDN8P7qKj+vO375BBo6QktcQL8219FtfhKnkn8Lv779qE5J9Xkc9sRqo74RKRtMUneB1RCYlPN3ATQk09GAuYpMkaCCJOx8C8uyaDDBC3nJWYyxdwZzj+19eu57+euEhsItcojE4p7HMUwoZBuUlQY685bGdOh84+wLhPNCLfEfiIQH8qsV6Ck6LqSaR+dteFyvEti1zjc4hNGvUzkh/k3mBPo7nTERN2gtU0prkupzJOzvyQhcAsiMUxa9gLBNUNVQ3JVgp2Eg6YKFCloS0BBROqhgmz3YFFRz9iBoKJvN6gcvaPK8uaaTPjcCDcezcyATiCHJgGUejwOdHL7ibtCvjTZf7PHfj+KkitCTbGOPzhd1uS8WK8PXsDb8TkMXkuT0PO8x0Q/ulyWHDyPxBFW903BEdrjPatS75XxZUNUzPNCUD8gYilnmI/ZjWc5b7y8vV3Lz4Tssy28PxXQw +sidebar_class_name: "get api-method" +custom_edit_url: null +--- + +import ApiTabs from "@theme/ApiTabs"; +import DiscriminatorTabs from "@theme/DiscriminatorTabs"; +import MethodEndpoint from "@theme/ApiExplorer/MethodEndpoint"; +import SecuritySchemes from "@theme/ApiExplorer/SecuritySchemes"; +import MimeTabs from "@theme/MimeTabs"; +import ParamsItem from "@theme/ParamsItem"; +import ResponseSamples from "@theme/ResponseSamples"; +import SchemaItem from "@theme/SchemaItem"; +import SchemaTabs from "@theme/SchemaTabs"; +import Heading from "@theme/Heading"; +import OperationTabs from "@theme/OperationTabs"; +import TabItem from "@theme/TabItem"; + + + + + + + + + + +Get all open quote requests that are currently in Received or Pending status. + +
+
+ + +
+ + + OK + + +
+ + + Response Headers + +
    +
  • + + + X-Api-Version + + string + +
    +
    + + + API Version Number - See docs for more info + + +
    +
    +
  • +
+
+ + + + +
+ + + Schema + +
+ +
    +
  • +
    + Array [ +
    +
  • + + + + + + + + + + + + + + + +
  • +
    + ] +
    +
  • +
+
+
+ + + + +
+
+
+
+
+
+
+
+ \ No newline at end of file diff --git a/docs/bridge/docs/04-Routers/RFQ/API/get-quotes.api.mdx b/docs/bridge/docs/03-RFQ/10-Quoting/Quoter API/get-quotes.api.mdx similarity index 100% rename from docs/bridge/docs/04-Routers/RFQ/API/get-quotes.api.mdx rename to docs/bridge/docs/03-RFQ/10-Quoting/Quoter API/get-quotes.api.mdx diff --git a/docs/bridge/docs/03-RFQ/10-Quoting/Quoter API/index.md b/docs/bridge/docs/03-RFQ/10-Quoting/Quoter API/index.md new file mode 100644 index 0000000000..58994decf4 --- /dev/null +++ b/docs/bridge/docs/03-RFQ/10-Quoting/Quoter API/index.md @@ -0,0 +1,221 @@ +--- +sidebar_position: 0 +sidebar_label: Quoter API +--- + + +[relay]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#relayv2 +[prove]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#provev2 +[dispute]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#dispute +[claim]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#claimv2 +[cancel]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#cancelv2 +[proof]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#bridgetxdetails +[BridgeRequested]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgerequested +[BridgeTransactionV2]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#bridgetransactionv2 +[BridgeRelayed]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgerelayed +[BridgeProofProvided]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgeproofprovided +[Cancel Delay]: https://rfq-contracts.synapseprotocol.com/contracts/FastBridge.sol/contract.FastBridge.html#refund_delay +[Multicall]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IMulticallTarget.sol/interface.IMulticallTarget.html + +[Quoter API]: /docs/RFQ/Quoting/Quoter%20API/ +[Dispute Period]: /docs/RFQ/Security/#dispute-period +[Quoting]: /docs/RFQ/Quoting +[Bridging]: /docs/RFQ/Bridging +[Relaying]: /docs/RFQ/Relaying +[Proving]: /docs/RFQ/Proving +[Claiming]: /docs/RFQ/Claiming +[Canceling]: /docs/RFQ/Canceling +[Security]: /docs/RFQ/Security +[Exclusivity]: /docs/RFQ/Exclusivity + +[User]: /docs/RFQ/#entities +[Quoter]: /docs/RFQ/#entities +[Prover]: /docs/RFQ/#entities +[Relayer]: /docs/RFQ/#entities +[Guard]: /docs/RFQ/#entities +[Canceler]: /docs/RFQ/#entities + +# Quoter API + +:::info + +This guide is intended for builders who are integrating a quoter or frontend with the Synapse Intent Network (SIN) system. + +If you are interested in running a relayer, please also see [Relaying] and [Canonical Relayer](/docs/RFQ/CanonicalRelayer/) . + +::: + +The implementation of the Quoter API can be found [here](https://github.com/synapsecns/sanguine/tree/master/services/rfq/api). + +Please note that end-users and relayers will not need to run their own version of the API. + + +## Integrating the API + +### Passive Quotes + +**Endpoints for Quoters** + +Authorized quoters can push passive quotes via these endpoints: + +- [`PUT /quotes`](./upsert-quote.api.mdx) - Upsert a passive quote +- [`PUT /bulk_quotes`](./upsert-quotes.api.mdx) - Upsert an array of passive quotes in bulk + +**Endpoints for Integrators / Users** + +To view all current passive quotes, this permissionless endpoint can be used: + +- [`GET /quotes`](./get-quotes.api.mdx) - Get all quotes, can be filtered by different parameters. + + + +### Active Quotes + +Active Quoting is more complicated than passive and requires listening for & responding to individual Requests for Quotes (RFQs). + +**Endpoints for Quoters** + +- [`GET /rfq_stream`](./listen-for-active-rf-qs.api.mdx) - Connect via WebSocket to listen for streamed RFQs +- [`GET /rfq`](./get-open-quote-requests.api.mdx) - Retrieve currently open RFQs. + +**Endpoints for Integrators / Users** + +- [`PUT /rfq`](./initiate-an-active-rfq.api.mdx) - Initiate an RFQ and receive the best available quote. + + ## Websocket API for Quoters + + The websocket API allows quoters to interact with user quote requests once connected to the `GET /rfq_stream` endpoint. + + The websocket API exposes several operations for quoters: + - `ping` - sends a heartbeat to the API server to keep the connection alive (must be sent at least once per minute) + - `subscribe` - subscribes to quote requests for given chain(s) + - `unsubscribe` - unsubscribes to quote requests for given chain(s) + - `send_quote` - responds to a quote request + + The API server may respond with the following operations:` + - `pong` - acknowleges a `ping` message + - `request_quote` - informs quoter of a new user quote request + + All websocket messages follow this format: + ``` + { + op: string, + content: json, + success: bool, + } + ``` + + Quote request content should have the following format: + + ``` + { + data: { + origin_chain_id: number, + dest_chain_id: number, + origin_token_addr: string, + dest_token_addr: string, + origin_amount_exact: string, + expiration_window: number // number of ms since created_at until request should expire + }, + } + ``` + + Quote response content should have the following format: + + ``` + { + request_id: string, + dest_amount: string, + } + ``` + + Subscribe / Unsubscribe content should be an array of chain ids. + + +## API Version Changes + +An http response header "X-Api-Version" will be returned on each call response. + +Any systems that integrate with the API should use this header to detect version changes and perform appropriate follow-up actions & alerts. + +Upon a version change, [versions.go](https://github.com/synapsecns/sanguine/blob/master/services/rfq/api/rest/versions.go) can be referred to for further detail on the version including deprecation alerts, etc. + +Please note, while Synapse may choose to take additional steps to alert & advise on API changes through other communication channels, it will remain the responsibility of the API users & integrators to set up their own detection & notifications of version changes as they use these endpoints. Likewise, it will be their responsibility review the versions.go file, to research & understand how any changes may affect their integration, and to implement any necessary adjustments resulting from the API changes. + +## Authentication & Authorization + +In accordance with [EIP-191](https://eips.ethereum.org/EIPS/eip-191), authorized Quoter API endpoints require a message signed by the relayer's address to accompany each request. The signature should be sent in the `Authorization` header of the request. We provide a client stub/example implementation in go [here](https://pkg.go.dev/github.com/synapsecns/sanguine/services/rfq@v0.13.3/api/client). + +Additional Example in Typescript: + + ```typescript + import { ecsign, toBuffer, bufferToHex, hashPersonalMessage } from 'ethereumjs-util'; + + async function signMessage(privateKey: string) { + const message = Math.floor(Date.now() / 1000).toString(); + const messageHash = hashPersonalMessage(Buffer.from(message)); + var { v, r, s } = ecsign(messageHash, toBuffer(privateKey)); + + v -= 27 + + const signature = Buffer.concat([r, s, Buffer.from([v])]); + + return `${message}:${bufferToHex(signature)}`; + } + ``` + +Once the message has been authenticated, the authorization of the sender/signer will be checked against the assigned roles of the respective FastBridge contract. If `QUOTER_ROLE` is not assigned, the request will be rejected. If you wish to be added as an authorized quoter, contact us. + +::: + +## API Urls + + - Mainnet: `api.synapseprotocol.com` + - Testnet: `rfq-api-testnet.omnirpc.io` + +## Running the API: + +Users and relayers **are not** expected to run their own version of the Quoter API. Rather, they are expected to use a Quoter API that is hosted by the the interface they are quoting for. For example, the Quoter API used by the Synapse bridge interface is hosted at the URL above. + + +### Configuration + +The Quoter API takes in a yaml config that allows the user to specify which contracts, chains and interfaces it should run on. The config is structured like this: + +```yaml +database: + type: mysql # can be other mysql or sqlite + dsn: root:password@hostname:3306)/database?parseTime=true # should be the dsn of your database. If using sqlite, this can be a path +omnirpc_url: https://route-to-my-omnirpc # omnirpc route +bridges: + 1: '0x00......' # FastBridge address on ethereum (chain id: 1) + 10: '0x01....' # FastBridge address on op (chain id: 10) +port: '8081' # port to run your http server on +``` + +### YAML Descriptions + +- `database` - The database settings for the API backend. A database is required to store quotes and other information. Using SQLite with a dsn set to a `/tmp/` directory is recommended for development. + - `type` - the database driver to use, can be `mysql` or `sqlite`. + - `dsn` - the dsn of your database. If using sqlite, this can be a path, if using mysql please see [here](https://dev.mysql.com/doc/connector-odbc/en/connector-odbc-configuration.html) for more information. +- `omnirpc_url` - The omnirpc url to use for querying chain data (no trailing slash). For more information on omnirpc, see [here](/docs/Services/Omnirpc). +- `bridges` - A key value map of chain id to FastBridge contract address. The API will only allow quotes to be posted on these chains. +- `port` - The port to run the http server on. + +### Building from Source + +To build the Quoter API from source, you will need to clone the repository and run the main.go file with the config file. Building from source requires go 1.21 or higher and is generally not recommended for end-users. + +1. `git clone https://github.com/synapsecns/sanguine --recursive` +2. `cd sanguine/services/rfq` +3. `go run main.go --config /path/to/config.yaml` + +**Running with Docker** + +The Quoter API can also be run with docker. To do this, you will need to build the docker image and run it with the config file. + +:::tip +Docker versions should always be pinned in production environments. For a full list of tags, see [here](https://github.com/synapsecns/sanguine/pkgs/container/sanguine%2Frfq-api) +::: + +1. `docker run ghcr.io/synapsecns/sanguine/rfq-api:latest --config /path/to/config` diff --git a/docs/bridge/docs/03-RFQ/10-Quoting/Quoter API/initiate-an-active-rfq.api.mdx b/docs/bridge/docs/03-RFQ/10-Quoting/Quoter API/initiate-an-active-rfq.api.mdx new file mode 100644 index 0000000000..95e3d4cbed --- /dev/null +++ b/docs/bridge/docs/03-RFQ/10-Quoting/Quoter API/initiate-an-active-rfq.api.mdx @@ -0,0 +1,404 @@ +--- +id: initiate-an-active-rfq +title: "Initiate an Active RFQ" +description: "Initiate an Active Request-For-Quote return the best quote available." +sidebar_label: "Initiate an Active RFQ" +hide_title: true +hide_table_of_contents: true +api: eJylVcFu2zAM/RWBZ2cNdvQtGzagGLB1XTsMCIKCsZlErS0pFJ00CPzvg2SndhI3xTqfLOrpiSbfo/eQk89YO9HWQArXRotGIYVGTTLRG1K3X38qNLlikoqNkhWpOXlR68oG3AZ1gfOCPkACTOuKvHyy+Q7SPWTWCBkJr+hcoTMMt1w9+nDVHny2ohLDm2PriEWTD6scZShKXh6wtFVDKDtHkIIX1mYJddLsZyvU5kHnPYQ2QkviF4jYJzIPmOc8SEPPTnPM82GrTW63w1SW9VKbS/m0iMsZtaA3coqVPuboNpkK3BHH0+T9AKZODhE7f6RMwqmYA6NYfo23uTSEI6cWKofIX7iRGXdhXfl/zKZuWtNT4b0nbvXVSqoVl2bKIRWuKH65d9b4Jr+P43Grkh7Pj2+QwIowJ46gP6OJ06PfxF43EjyGT26uVbupvlflnFiN1C8ildvMq4VlVVompc3CQtKT7+kX1sm7pf+GyC8qoevYK0LB9vZ3aCgBX2XZ8d7c2oLQDLc0BnHpIZ02iXmYJVCSrGwOKbgqtNShrCCFK16sIYFQnNtugnx5xtIV1J8IR+U55HZm/fGA0zvwgMHHZ37u4Gc2Hg+6tjvQtaiLndW3V9cTJ54StQacHuKzU4MdUS3sYe5iFhQUmqAlFBEggc1B+BDQznopMQrCYEmvDn84cWdvrv/n76IVjdCzXLkCtYnjg4voiyiNKQRpzBJYWS9hud/P0dM9F3UdwuuKeAfpdJbABlkH4rCqD66HdLqHJ9pBCp+bpEd34dIAL6o4tk7dWSeHE5MsIycXsX1J39zfQQLz9t9X2jwcYdyG/uO26YCNNYwmirE9FGiWFS4DtqEMz1/Dm6d/ +sidebar_class_name: "put api-method" +custom_edit_url: null +--- + +import ApiTabs from "@theme/ApiTabs"; +import DiscriminatorTabs from "@theme/DiscriminatorTabs"; +import MethodEndpoint from "@theme/ApiExplorer/MethodEndpoint"; +import SecuritySchemes from "@theme/ApiExplorer/SecuritySchemes"; +import MimeTabs from "@theme/MimeTabs"; +import ParamsItem from "@theme/ParamsItem"; +import ResponseSamples from "@theme/ResponseSamples"; +import SchemaItem from "@theme/SchemaItem"; +import SchemaTabs from "@theme/SchemaTabs"; +import Heading from "@theme/Heading"; +import OperationTabs from "@theme/OperationTabs"; +import TabItem from "@theme/TabItem"; + + + + + + + + + + +Initiate an Active Request-For-Quote return the best quote available. + + + + + + +
+ +

+ Body +

+ required + +
+
+ + + User quote request + + +
+
    + +
    + + + + data + + object + + +
    + + + + + + + + + + + + + + + + + + + +
    +
    +
    + + + + + + +
+
+
+
+
+ + +
+ + + OK + + +
+ + + Response Headers + +
    +
  • + + + X-Api-Version + + string + +
    +
    + + + API Version Number - See docs for more info + + +
    +
    +
  • +
+
+ + + + +
+ + + Schema + +
+ +
    + + + + + + + + + + + + + +
+
+
+ + + + +
+
+
+
+
+
+
+
diff --git a/docs/bridge/docs/03-RFQ/10-Quoting/Quoter API/listen-for-active-rf-qs.api.mdx b/docs/bridge/docs/03-RFQ/10-Quoting/Quoter API/listen-for-active-rf-qs.api.mdx new file mode 100644 index 0000000000..3d6484b701 --- /dev/null +++ b/docs/bridge/docs/03-RFQ/10-Quoting/Quoter API/listen-for-active-rf-qs.api.mdx @@ -0,0 +1,158 @@ +--- +id: listen-for-active-rf-qs +title: "Listen for Active RFQs" +description: "Establish a WebSocket connection to listen for Active Requests-For-Quote." +sidebar_label: "Listen for Active RFQs" +hide_title: true +hide_table_of_contents: true +api: eJytUktv2zAM/ivCd5aX9epbDu0wYBjapnsARjDIChNrtSVFpLMFhv/7QLfF+gB22kkAyY/fg5qwI/YlZAkposYli2v7wJ1x5hu1m+TvSYxPMZLXESPJ9IGFotmnYtZewonMLR1HYuHqKpXqZkxC72BRiHOKTIx6wsX7C31esm1+BfFdiAdzXZIkn3qGRUduR2VBfa/WOVRfqfACeI1fX380j03zeRxaKqYyGyKzS54XgUMqZELcJ1iw72hwukXOmVCDpYR4wDzPFj5FoSjadTn3wTvlWP3kB95/YBUt7sCoGxzVO2NrMZB0aYcaBxJYZCcdaqzK/viDpZAbYLHoqqeF23nl1lVBeiWAxenJNzBb5MQyuEVNdIOOfHp7iKsbTfBFStNfb//1vI9BCP2WVe5diCpyLL0yPtht8Mzu1qJLLFqdptYxfSn9PGv5OFI5o262FidXgmvVf7Odn34C6mbCPZ314N5T1jxPrh+V/c2t5ufZf7i8wzz/ATWw/8w= +sidebar_class_name: "get api-method" +custom_edit_url: null +--- + +import ApiTabs from "@theme/ApiTabs"; +import DiscriminatorTabs from "@theme/DiscriminatorTabs"; +import MethodEndpoint from "@theme/ApiExplorer/MethodEndpoint"; +import SecuritySchemes from "@theme/ApiExplorer/SecuritySchemes"; +import MimeTabs from "@theme/MimeTabs"; +import ParamsItem from "@theme/ParamsItem"; +import ResponseSamples from "@theme/ResponseSamples"; +import SchemaItem from "@theme/SchemaItem"; +import SchemaTabs from "@theme/SchemaTabs"; +import Heading from "@theme/Heading"; +import OperationTabs from "@theme/OperationTabs"; +import TabItem from "@theme/TabItem"; + + + + + + + + + + +Establish a WebSocket connection to listen for Active Requests-For-Quote. + +
+
+ + +
+ + + Switching Protocols + + +
+ + + Response Headers + +
    +
  • + + + X-Api-Version + + string + +
    +
    + + + API Version Number - See docs for more info + + +
    +
    +
  • +
+
+ + + + +
+ + + Schema + +
+ +
    +
    + + + string + + +
    +
+
+
+
+
+
+
+
+
+
+
+ \ No newline at end of file diff --git a/docs/bridge/docs/04-Routers/RFQ/API/relay-ack.api.mdx b/docs/bridge/docs/03-RFQ/10-Quoting/Quoter API/relay-ack.api.mdx similarity index 100% rename from docs/bridge/docs/04-Routers/RFQ/API/relay-ack.api.mdx rename to docs/bridge/docs/03-RFQ/10-Quoting/Quoter API/relay-ack.api.mdx diff --git a/docs/bridge/docs/03-RFQ/10-Quoting/Quoter API/sidebar.ts b/docs/bridge/docs/03-RFQ/10-Quoting/Quoter API/sidebar.ts new file mode 100644 index 0000000000..1df2f5d223 --- /dev/null +++ b/docs/bridge/docs/03-RFQ/10-Quoting/Quoter API/sidebar.ts @@ -0,0 +1,68 @@ +import type { SidebarsConfig } from "@docusaurus/plugin-content-docs"; + +const sidebar: SidebarsConfig = { + apisidebar: [ + { + type: "category", + label: "ack", + items: [ + { + type: "doc", + id: "04-Routers/RFQ/API/relay-ack", + label: "Relay ack", + className: "api-method put", + }, + ], + }, + { + type: "category", + label: "quotes", + items: [ + { + type: "doc", + id: "04-Routers/RFQ/API/upsert-quotes", + label: "Upsert quotes", + className: "api-method put", + }, + { + type: "doc", + id: "04-Routers/RFQ/API/get-contract-addresses", + label: "Get contract addresses", + className: "api-method get", + }, + { + type: "doc", + id: "04-Routers/RFQ/API/get-open-quote-requests", + label: "Get open quote requests", + className: "api-method get", + }, + { + type: "doc", + id: "04-Routers/RFQ/API/get-quotes", + label: "Get quotes", + className: "api-method get", + }, + { + type: "doc", + id: "04-Routers/RFQ/API/upsert-quote", + label: "Upsert quote", + className: "api-method put", + }, + { + type: "doc", + id: "04-Routers/RFQ/API/handle-user-quote-request", + label: "Initiate an Active RFQ", + className: "api-method put", + }, + { + type: "doc", + id: "04-Routers/RFQ/API/handle-web-socket-connection-for-active-quote-requests", + label: "Active RFQ Listener", + className: "api-method get", + }, + ], + }, + ], +}; + +export default sidebar.apisidebar; diff --git a/docs/bridge/docs/04-Routers/RFQ/API/upsert-quote.api.mdx b/docs/bridge/docs/03-RFQ/10-Quoting/Quoter API/upsert-quote.api.mdx similarity index 100% rename from docs/bridge/docs/04-Routers/RFQ/API/upsert-quote.api.mdx rename to docs/bridge/docs/03-RFQ/10-Quoting/Quoter API/upsert-quote.api.mdx diff --git a/docs/bridge/docs/04-Routers/RFQ/API/upsert-quotes.api.mdx b/docs/bridge/docs/03-RFQ/10-Quoting/Quoter API/upsert-quotes.api.mdx similarity index 100% rename from docs/bridge/docs/04-Routers/RFQ/API/upsert-quotes.api.mdx rename to docs/bridge/docs/03-RFQ/10-Quoting/Quoter API/upsert-quotes.api.mdx diff --git a/docs/bridge/docs/03-RFQ/10-Quoting/index.md b/docs/bridge/docs/03-RFQ/10-Quoting/index.md new file mode 100644 index 0000000000..82b463b16f --- /dev/null +++ b/docs/bridge/docs/03-RFQ/10-Quoting/index.md @@ -0,0 +1,67 @@ +--- +title: Quoting +--- + + +[relay]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#relayv2 +[prove]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#provev2 +[dispute]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#dispute +[claim]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#claimv2 +[cancel]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#cancelv2 +[proof]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#bridgetxdetails +[BridgeRequested]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgerequested +[BridgeTransactionV2]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#bridgetransactionv2 +[BridgeRelayed]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgerelayed +[BridgeProofProvided]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgeproofprovided +[Cancel Delay]: https://rfq-contracts.synapseprotocol.com/contracts/FastBridge.sol/contract.FastBridge.html#refund_delay +[Multicall]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IMulticallTarget.sol/interface.IMulticallTarget.html + +[Quoter API]: /docs/RFQ/Quoting/Quoter%20API/ +[Dispute Period]: /docs/RFQ/Security/#dispute-period +[Quoting]: /docs/RFQ/Quoting +[Bridging]: /docs/RFQ/Bridging +[Relaying]: /docs/RFQ/Relaying +[Proving]: /docs/RFQ/Proving +[Claiming]: /docs/RFQ/Claiming +[Canceling]: /docs/RFQ/Canceling +[Security]: /docs/RFQ/Security +[Exclusivity]: /docs/RFQ/Exclusivity + +[User]: /docs/RFQ/#entities +[Quoter]: /docs/RFQ/#entities +[Prover]: /docs/RFQ/#entities +[Relayer]: /docs/RFQ/#entities +[Guard]: /docs/RFQ/#entities +[Canceler]: /docs/RFQ/#entities + + +The Synapse Intent Network (SIN) systems allows [Quoter] entities (aka market makers / solvers / relayers) to post quotes via an off-chain [Quoter API]. These quotes are matched to `User` bridge inputs to achieve the optimal parameters (eg: the best price) for the [User]'s bridge transaction. + +There are two types of quoting methods supported by the Synapse Intent Network (SIN) system: + +## [Passive Quoting](/docs/RFQ/Quoting/Quoter%20API/#passive-quotes) + +Similar to an order book, Passive Quoting communicates a [Quoter]'s ongoing intention to fulfill any transaction that occurs upon specific routes and meets specific limits, pricing, and fee criteria. + +## [Active Quoting](/docs/RFQ/Quoting/Quoter%20API/#active-quotes) + +In our latest version of the [Quoter API], a new Active Quoting method has been introduced where a [Quoter] can listen and respond to live quote requests individually. + +This supplements the existing Passive Quotes to create a hybrid system, where Active and Passive quoting can be utilized together by Quoters in any desired combination to maximize their efficiency and further improve prices. + +Active quoting is more complicated to implement and maintain, but allow for more granular & customized quotes that can improve efficiency among other benefits. Quoters who prefer a simpler approach are free to use nothing but Passive Quotes, if they choose. + +## + +:::info + +To learn more about how Quoting works, or for integration details, see [Quoter API] docs. + +::: + +Regardless of the method used, these quotes constitute a provisional commitment to fulfill a [User]'s bridge according to the quoted price and other parameters, once it is submitted on-chain. + +To that end, integrators and users can utilize the data from these quotes to construct and submit a [Bridging] transaction on-chain. Once this transaction is finalized on-chain, `User`s can expect to receive their funds on the destination shortly after, as quoted. + +Quoters are responsible for keeping their quotes fresh and accurate. Likewise, they are responsible for completing their part of fulfillment for any transactions which act upon their quotes. To these effects, Quoters should push updates as rapidly as possible in reaction to consequential changes in prices, balances, etc. By default, the [Canonical Relayer](/docs/RFQ/CanonicalRelayer/) continuously updates quotes by checking on-chain balances, in-flight requests, and gas prices - custom implementations should take a similar approach. + diff --git a/docs/bridge/docs/03-RFQ/15-Bridging/index.md b/docs/bridge/docs/03-RFQ/15-Bridging/index.md new file mode 100644 index 0000000000..1bbbbf3767 --- /dev/null +++ b/docs/bridge/docs/03-RFQ/15-Bridging/index.md @@ -0,0 +1,79 @@ +--- +title: Bridging +--- + + +[relay]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#relayv2 +[prove]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#provev2 +[dispute]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#dispute +[claim]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#claimv2 +[cancel]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#cancelv2 +[proof]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#bridgetxdetails +[BridgeRequested]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgerequested +[BridgeTransactionV2]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#bridgetransactionv2 +[BridgeRelayed]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgerelayed +[BridgeProofProvided]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgeproofprovided +[Cancel Delay]: https://rfq-contracts.synapseprotocol.com/contracts/FastBridge.sol/contract.FastBridge.html#refund_delay +[Multicall]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IMulticallTarget.sol/interface.IMulticallTarget.html + +[Quoter API]: /docs/RFQ/Quoting/Quoter%20API/ +[Dispute Period]: /docs/RFQ/Security/#dispute-period +[Quoting]: /docs/RFQ/Quoting +[Bridging]: /docs/RFQ/Bridging +[Relaying]: /docs/RFQ/Relaying +[Proving]: /docs/RFQ/Proving +[Claiming]: /docs/RFQ/Claiming +[Canceling]: /docs/RFQ/Canceling +[Security]: /docs/RFQ/Security +[Exclusivity]: /docs/RFQ/Exclusivity + +[User]: /docs/RFQ/#entities +[Quoter]: /docs/RFQ/#entities +[Prover]: /docs/RFQ/#entities +[Relayer]: /docs/RFQ/#entities +[Guard]: /docs/RFQ/#entities +[Canceler]: /docs/RFQ/#entities + + +Once a quote has been obtained via [Quoting], the details of the quote can be used to construct a bridge transaction for the user to sign and submit to the origin chain. + +Bridges through Synapse Intent Network (SIN) utilize the [Synapse Router](/docs/Routers/Synapse-Router/) - Refer to those docs for more detail. + + +:::info +If you are interested in integrating with Synapse Intent Network (SIN) Bridging, refer to the [Synapse Bridge SDK](/docs/Bridge/SDK). + +Alternatively, you can explore the [Bridge REST API](https://api.synapseprotocol.com/api-docs/). + +::: + + +## Exclusivity + +As of FastBridgeV2, it is possible for integrators to optionally assign temporary exclusive fill rights to certain relayers. + +IE: For a temporary period of time, only the relayer chosen and assigned by the integrator will be able to execute the relay. + +For details on Exclusivity and how to create these types of bridges, see [Exclusivity] + + +## Effects of a bridge Transaction + +If sufficient funds and approvals exist, the bridging funds will be transferred from the [User] to the FastBridge contract. + +The funds will remain with the contract in escrow until: + +- [Claiming] occurs, which transfers the funds to the [Relayer] as reimbursement for completing the relay on the destination. + +or + +- [Canceling] occurs, which returns the funds to the `originSender`. + + +Additionally, a [BridgeRequested] event will be emitted which contains all instructions for the bridge to be completed by Relayers + + + +## Next steps + +Relayers will observe the bridge transaction via the [BridgeRequested] event and proceed to [Relaying] if it meets all of their criteria. diff --git a/docs/bridge/docs/03-RFQ/15-Bridging/quoteId.md b/docs/bridge/docs/03-RFQ/15-Bridging/quoteId.md new file mode 100644 index 0000000000..697d446011 --- /dev/null +++ b/docs/bridge/docs/03-RFQ/15-Bridging/quoteId.md @@ -0,0 +1,41 @@ +--- +title: Quote ID +--- + + +[relay]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#relayv2 +[prove]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#provev2 +[dispute]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#dispute +[claim]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#claimv2 +[cancel]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#cancelv2 +[proof]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#bridgetxdetails +[BridgeRequested]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgerequested +[BridgeTransactionV2]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#bridgetransactionv2 +[BridgeRelayed]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgerelayed +[BridgeProofProvided]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgeproofprovided +[Cancel Delay]: https://rfq-contracts.synapseprotocol.com/contracts/FastBridge.sol/contract.FastBridge.html#refund_delay +[Multicall]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IMulticallTarget.sol/interface.IMulticallTarget.html + +[Quoter API]: /docs/RFQ/Quoting/Quoter%20API/ +[Dispute Period]: /docs/RFQ/Security/#dispute-period +[Quoting]: /docs/RFQ/Quoting +[Bridging]: /docs/RFQ/Bridging +[Relaying]: /docs/RFQ/Relaying +[Proving]: /docs/RFQ/Proving +[Claiming]: /docs/RFQ/Claiming +[Canceling]: /docs/RFQ/Canceling +[Security]: /docs/RFQ/Security +[Exclusivity]: /docs/RFQ/Exclusivity + +[User]: /docs/RFQ/#entities +[Quoter]: /docs/RFQ/#entities +[Prover]: /docs/RFQ/#entities +[Relayer]: /docs/RFQ/#entities +[Guard]: /docs/RFQ/#entities +[Canceler]: /docs/RFQ/#entities + +An optional arbitrary quoteId value can be supplied to FastBridgeV2 bridge transactions as part of the V2 bridge parameters. + +The purpose of this field is to allow a linkage between off-chain quoting systems and on-chain bridges. + +There is currently no on-chain functionality that utilizes this field, it is simply for record keeping and indexing, however this may change in the future. diff --git a/docs/bridge/docs/03-RFQ/20-Relaying/index.md b/docs/bridge/docs/03-RFQ/20-Relaying/index.md new file mode 100644 index 0000000000..9b9114cce8 --- /dev/null +++ b/docs/bridge/docs/03-RFQ/20-Relaying/index.md @@ -0,0 +1,123 @@ +--- +title: Relaying +--- + + +[relay]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#relayv2 +[bridge]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#bridgev2 +[prove]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#provev2 +[dispute]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#dispute +[claim]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#claimv2 +[cancel]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#cancelv2 +[proof]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#bridgetxdetails +[BridgeRequested]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgerequested +[BridgeTransactionV2]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#bridgetransactionv2 +[BridgeRelayed]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgerelayed +[BridgeProofProvided]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgeproofprovided +[Cancel Delay]: https://rfq-contracts.synapseprotocol.com/contracts/FastBridge.sol/contract.FastBridge.html#refund_delay +[Multicall]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IMulticallTarget.sol/interface.IMulticallTarget.html + +[Quoter API]: /docs/RFQ/Quoting/Quoter%20API/ +[Dispute Period]: /docs/RFQ/Security/#dispute-period +[Quoting]: /docs/RFQ/Quoting +[Bridging]: /docs/RFQ/Bridging +[Relaying]: /docs/RFQ/Relaying +[Proving]: /docs/RFQ/Proving +[Claiming]: /docs/RFQ/Claiming +[Canceling]: /docs/RFQ/Canceling +[Security]: /docs/RFQ/Security +[Exclusivity]: /docs/RFQ/Exclusivity + +[User]: /docs/RFQ/#entities +[Quoter]: /docs/RFQ/#entities +[Prover]: /docs/RFQ/#entities +[Relayer]: /docs/RFQ/#entities +[Guard]: /docs/RFQ/#entities +[Canceler]: /docs/RFQ/#entities + +# Relaying + +In the Synapse Intent Network (SIN) System, Relayers fulfill the intent of [User] [bridge] transactions by providing the liquidity and executing the [relay] transaction on the destination chain . + +:::info + +If you are reading this documentation to become a participating Relayer, be sure to read all RFQ sections. + +Also note - Currently, relaying involves actions which require explicit authorization. + +If you are interested in participating as a Relayer, contact us. + +::: + +## Detecting a Bridge Request + +A relay will start by observing a [BridgeRequested] event on an origin chain, which will emit the `request` bytes of an encoded [BridgeTransactionV2] struct, `destChainId`, and other values. + +These are the bridge instructions that the [Relayer] is *relaying* to the FastBridge contract on the indicated destination chain (`destChainId`) - which will then utilize the [Relayer]'s liquidity to complete the bridge. + +## Executing the [relay] function + +To complete the relay, the [Relayer] should provide these same `request` bytes emitted by [BridgeRequested] to the `request` parameter of the [relay] function on the FastBridge contract of the destination chain. + +Additionally , if the `destToken` is the native currency of the chain (indicated by placeholder `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE`) then that amount must also be set as the `msg.value` + +If the `destToken` is some other address, this would indicate that the destination asset to deliver is an ERC20. +Prior to calling [relay], the [Relayer] must have already granted sufficient token approvals to FastBridge to allow transfers of this ERC20. + +If the [Relayer] has sufficient funds and approvals, and the relay has not already been completed by another relayer, FastBridge will then facilitate the delivery of the funds to the [User] and emit a [BridgeRelayed] event. + +### Function Options + +There are two versions of the relay function in FastBridgeV2. Relayers can use whichever best suits their implementation. + +
+ +
+```solidity + function relayV2(bytes memory request, address relayer) external payable; +``` +This version allows an arbitrary `relayer` address to be supplied +
+
+
+```solidity + function relay(bytes memory request) external payable; +``` +This version will auto-assign the executing EOA (`msg.sender`) as the `relayer` +
+
+ +Regardless of the method used, a [BridgeRelayed](https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgedepositrefunded) event will be emitted. + +### Setting the `relayer` parameter +The address which is specified as the `relayer` on the [relay] will have control of the reimbursed funds when the [claim] occurs later. + +Note that Relayers can utilize this feature to be reimbursed on different addresses than they are actually relaying from. This can be useful for advanced relaying setups, but is not a necessity. + +# Exclusivity + +As of FastBridgeV2, it is possible for integrators to optionally assign temporary exclusive fill rights to certain relayers. + +IE: For a temporary period of time, only the relayer chosen and assigned by the integrator will be able to execute the relay. + +For details on Exclusivity and how to relay these types of bridges, see [Exclusivity] + +# Multicalling + +As of FastBridgeV2, it is possible to batch many [relay] transactions together with [Multicall] + +However, the Multicall implementation is limited to non-payable transactions only, so native-gas bridges cannot be included in batches. + +### Permissions + +Although relaying and claiming can be performed permissionlessly, in the current system Relayers will need to also operate a permissioned [Prover] role. + +Note that this allows the use of different EOAs to [relay], [prove], and [claim] - which we recommend doing. + +We also recommend that Relayers operate a [Quoter] to compete on pricing and routes, but this is not a necessity. + +## Next steps + +After a [relay] has been completed, the process is fully complete from the perspective of the [User]. + +Next, the [Relayer] must proceed to [Proving] the relay so that they can be reimbursed for the liquidity they provided. diff --git a/docs/bridge/docs/03-RFQ/20-Relaying/riskFactors.md b/docs/bridge/docs/03-RFQ/20-Relaying/riskFactors.md new file mode 100644 index 0000000000..e2ec3e132d --- /dev/null +++ b/docs/bridge/docs/03-RFQ/20-Relaying/riskFactors.md @@ -0,0 +1,91 @@ +--- +title: Risk Factors +--- + + +[relay]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#relayv2 +[prove]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#provev2 +[dispute]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#dispute +[claim]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#claimv2 +[cancel]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#cancelv2 +[proof]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#bridgetxdetails +[BridgeRequested]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgerequested +[BridgeTransactionV2]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#bridgetransactionv2 +[BridgeRelayed]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgerelayed +[BridgeProofProvided]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgeproofprovided +[Cancel Delay]: https://rfq-contracts.synapseprotocol.com/contracts/FastBridge.sol/contract.FastBridge.html#refund_delay +[Multicall]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IMulticallTarget.sol/interface.IMulticallTarget.html + +[Quoter API]: /docs/RFQ/Quoting/Quoter%20API/ +[Dispute Period]: /docs/RFQ/Security/#dispute-period +[Quoting]: /docs/RFQ/Quoting +[Bridging]: /docs/RFQ/Bridging +[Relaying]: /docs/RFQ/Relaying +[Proving]: /docs/RFQ/Proving +[Claiming]: /docs/RFQ/Claiming +[Canceling]: /docs/RFQ/Canceling +[Security]: /docs/RFQ/Security +[Exclusivity]: /docs/RFQ/Exclusivity + +[User]: /docs/RFQ/#entities +[Quoter]: /docs/RFQ/#entities +[Prover]: /docs/RFQ/#entities +[Relayer]: /docs/RFQ/#entities +[Guard]: /docs/RFQ/#entities +[Canceler]: /docs/RFQ/#entities + + +:::warning + +As is the case with many cross-chain systems, there are inherent risks involved with providing liquidity as a [Relayer]. + +Although the system is designed to minimize these risks, it is ultimately the sole responsibility and liability of the [Relayer] to fully understand and manage the risks involved with participating as a [Relayer] in the Synapse Intent Network (SIN) system. + +::: + +### Missing Relays + +If a bridge transaction occurs that acts upon a quote issued by a particular [Relayer], and that [Relayer] fails to complete the [relay] transaction - the bridge deposit can be come "stuck" if there is no one to complete it. + +Although funds are safe in these instances and will eventually either be cancelled & refunded or completed late, these are adverse UX incidents nonetheless. + +All participating Relayers must ensure they are able to complete all bridge transactions that act upon their quotes and will be asked to quickly rectify any shortfalls. + +This may include completing the [relay] at an unexpectedly unfavorable price - within a reasonable threshold. For example, if the gas price sharply increased beyond what was anticipated. + +Refer to the [RFQ Indexer](/docs/Services/RFQ-Indexer-API) for endpoints that can aid with tracking missing relays. + +### Missing Proofs / Claims + +Currently, it is the responsibility of the [Relayer] to track and submit timely [prove] transactions for their own finalized [relay] transactions. +Failure to do so can result in a delayed reimbursement at best, or an eventual loss of funds at worst. + +Likewise, Relayers are expected to track and submit their own [claim] transactions once their proofs are past the [Dispute Period]. +Missing claims are less urgent than missing proofs, but the funds can remain unclaimed in escrow indefinitely if the responsible [Relayer] loses track of them. + +Refer to the [RFQ Indexer](/docs/Services/RFQ-Indexer-API) for endpoints that can aid with tracking missing proofs and claims. + +### Invalid Relays + +If a [relay] occurs for an incorrect output amount (or any other incorrect [BridgeTransactionV2] data) and is submitted via [prove] as the fulfillment a bridge transaction, the [proof] will be disputed by a [Guard]. + +In other terms, although a [relay] transaction may have truly taken place and delivered funds from the [Relayer] to the [User], if the [relay] request parameter does not exactly match the encoded [BridgeTransactionV2] data on the bridge transaction, then it cannot be accepted as the completion of that bridge. + +The incorrect [relay] in this example cannot be reversed or reimbursed and would effectively be a loss of funds for the [Relayer] who performed it. + +Reorganizations of the bridge transaction on the origin chain are the most likely vector for this scenario. + +### Pricing Failures + +Relayers should never assume that all bridges are profitable or properly priced, even those that came from the Synapse Frontend, and instead should independently verify using trusted mechanisms and oracles. + +Similarly, it is important to remember that bridge is a permissionless function. As with any similar system, bad actors *can* and *will* submit exploitative bridges to see if any Relayers make the fatal mistake of filling them. + +Failure to implement appropriate pricing protections could result in a loss of Relayer funds. + +### Self-Monitoring + +If you are a participating [Relayer], it is highly recommended that you monitor proofs, disputes, and other critical functions that involve your quotes and transactions. + +Refer to the [RFQ Indexer](/docs/Services/RFQ-Indexer-API) for endpoints that can aid with monitoring and alerting. + diff --git a/docs/bridge/docs/03-RFQ/30-Proving/index.md b/docs/bridge/docs/03-RFQ/30-Proving/index.md new file mode 100644 index 0000000000..6a91d8759f --- /dev/null +++ b/docs/bridge/docs/03-RFQ/30-Proving/index.md @@ -0,0 +1,83 @@ +--- +title: Proving +--- + + +[relay]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#relayv2 +[prove]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#provev2 +[dispute]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#dispute +[claim]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#claimv2 +[cancel]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#cancelv2 +[proof]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#bridgetxdetails +[BridgeRequested]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgerequested +[BridgeTransactionV2]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#bridgetransactionv2 +[BridgeRelayed]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgerelayed +[BridgeProofProvided]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgeproofprovided +[Cancel Delay]: https://rfq-contracts.synapseprotocol.com/contracts/FastBridge.sol/contract.FastBridge.html#refund_delay +[Multicall]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IMulticallTarget.sol/interface.IMulticallTarget.html + +[Quoter API]: /docs/RFQ/Quoting/Quoter%20API/ +[Dispute Period]: /docs/RFQ/Security/#dispute-period +[Quoting]: /docs/RFQ/Quoting +[Bridging]: /docs/RFQ/Bridging +[Relaying]: /docs/RFQ/Relaying +[Proving]: /docs/RFQ/Proving +[Claiming]: /docs/RFQ/Claiming +[Canceling]: /docs/RFQ/Canceling +[Security]: /docs/RFQ/Security +[Exclusivity]: /docs/RFQ/Exclusivity + +[User]: /docs/RFQ/#entities +[Quoter]: /docs/RFQ/#entities +[Prover]: /docs/RFQ/#entities +[Relayer]: /docs/RFQ/#entities +[Guard]: /docs/RFQ/#entities +[Canceler]: /docs/RFQ/#entities + +After [Relaying] successfully and observing that the [relay] transaction has finalized on the destination chain, [prove] transactions are executed by authorized Provers (who are typically also the [Relayer]) + +Through these transactions, the Prover is asserting that the indicated `relayer` completed a [relay] and can rightfully [claim] the escrowed bridge funds as a reimbursement. + +Each [prove] transaction sets the [proof] data for the bridge and initiates a [Dispute Period]. + +### Function Options + +There are two versions of the prove function in FastBridgeV2. Relayers can use whichever best suits their implementation. + +
+
+```solidity + function proveV2(bytes32 transactionId, bytes32 destTxHash, address relayer) external; +``` +This version allows an arbitrary `relayer` address to be supplied +
+ +
+
+```solidity + function prove(bytes memory request, bytes32 destTxHash) external; +``` +This version will auto-assign the executing EOA (`msg.sender`) as the `relayer` +
+
+ +Regardless of the method used, a [BridgeProofProvided](https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgeproofprovided) event will be emitted. + +Note that during the [Dispute Period], the `relayer` indicated on this function will be required to match the `relayer` on the actual [relay]. + +:::warning + +prove should not be called until the Relayer is confident that the relay transaction is finalized and will not be reorganized. + +::: + +### Multicalling + +As of FastBridgeV2, it is possible to batch many [prove] transactions together with [Multicall] + + +## Next steps + +Following the [prove] transaction, a [Dispute Period] begins - after which the [Relayer] may proceed to [Claiming] + +Although disputes are unlikely, monitoring should occur during the [Dispute Period] to verify there are no issues. diff --git a/docs/bridge/docs/03-RFQ/40-Claiming/index.md b/docs/bridge/docs/03-RFQ/40-Claiming/index.md new file mode 100644 index 0000000000..221251e33f --- /dev/null +++ b/docs/bridge/docs/03-RFQ/40-Claiming/index.md @@ -0,0 +1,73 @@ +--- +title: Claiming +--- + + +[relay]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#relayv2 +[prove]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#provev2 +[dispute]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#dispute +[claim]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#claimv2 +[cancel]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#cancelv2 +[proof]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#bridgetxdetails +[BridgeRequested]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgerequested +[BridgeTransactionV2]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#bridgetransactionv2 +[BridgeRelayed]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgerelayed +[BridgeProofProvided]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgeproofprovided +[Cancel Delay]: https://rfq-contracts.synapseprotocol.com/contracts/FastBridge.sol/contract.FastBridge.html#refund_delay +[Multicall]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IMulticallTarget.sol/interface.IMulticallTarget.html + +[Quoter API]: /docs/RFQ/Quoting/Quoter%20API/ +[Dispute Period]: /docs/RFQ/Security/#dispute-period +[Quoting]: /docs/RFQ/Quoting +[Bridging]: /docs/RFQ/Bridging +[Relaying]: /docs/RFQ/Relaying +[Proving]: /docs/RFQ/Proving +[Claiming]: /docs/RFQ/Claiming +[Canceling]: /docs/RFQ/Canceling +[Security]: /docs/RFQ/Security +[Exclusivity]: /docs/RFQ/Exclusivity + +[User]: /docs/RFQ/#entities +[Quoter]: /docs/RFQ/#entities +[Prover]: /docs/RFQ/#entities +[Relayer]: /docs/RFQ/#entities +[Guard]: /docs/RFQ/#entities +[Canceler]: /docs/RFQ/#entities + +After [Proving] and waiting for the [Dispute Period] to end, Relayers can then execute a [claim] transaction which will release the funds which have been escrowed since the original bridge. + +The funds will be transferred to the rightful [Relayer] as a reimbursement for the liquidity they provided on the [relay]. + + +### Function Options + +There are two versions of the claim function in FastBridgeV2. Relayers can use whichever best suits their implementation. + +
+
+```solidity + function claimV2(bytes memory request) external; +``` +This version can only be executed by the `relayer` on the proof. Allows an arbitrary `to` address as the recipient of funds. +
+
+
+```solidity + function claim(bytes memory request, address to) external; +``` + + +This version can be executed permissionlessly, and will transfer the funds only to the `relayer` address on the proof. +
+
+ +Regardless of the method used, a [BridgeDepositClaimed](https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgedepositclaimed) event will be emitted. + +### Multicalling + +As of FastBridgeV2, it is possible to batch many [claim] transactions together with [Multicall] + + +## Next steps + +Following the [claim] transaction, the bridge deposit funds have been reimbursed to the [Relayer] and are ready to be used for another relay. The bridge cycle is fully complete. diff --git a/docs/bridge/docs/03-RFQ/50-Canceling/index.md b/docs/bridge/docs/03-RFQ/50-Canceling/index.md new file mode 100644 index 0000000000..a973287c4a --- /dev/null +++ b/docs/bridge/docs/03-RFQ/50-Canceling/index.md @@ -0,0 +1,51 @@ +--- +title: Canceling +--- + + +[relay]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#relayv2 +[prove]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#provev2 +[dispute]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#dispute +[claim]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#claimv2 +[cancel]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#cancelv2 +[proof]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#bridgetxdetails +[BridgeRequested]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgerequested +[BridgeTransactionV2]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#bridgetransactionv2 +[BridgeRelayed]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgerelayed +[BridgeProofProvided]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgeproofprovided +[Cancel Delay]: https://rfq-contracts.synapseprotocol.com/contracts/FastBridge.sol/contract.FastBridge.html#refund_delay +[Multicall]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IMulticallTarget.sol/interface.IMulticallTarget.html + +[Quoter API]: /docs/RFQ/Quoting/Quoter%20API/ +[Dispute Period]: /docs/RFQ/Security/#dispute-period +[Quoting]: /docs/RFQ/Quoting +[Bridging]: /docs/RFQ/Bridging +[Relaying]: /docs/RFQ/Relaying +[Proving]: /docs/RFQ/Proving +[Claiming]: /docs/RFQ/Claiming +[Canceling]: /docs/RFQ/Canceling +[Security]: /docs/RFQ/Security +[Exclusivity]: /docs/RFQ/Exclusivity + +[User]: /docs/RFQ/#entities +[Quoter]: /docs/RFQ/#entities +[Prover]: /docs/RFQ/#entities +[Relayer]: /docs/RFQ/#entities +[Guard]: /docs/RFQ/#entities +[Canceler]: /docs/RFQ/#entities + +Due to various factors, it is possible but uncommon for a bridge to go unfilled, where it is indefinitely not relayed. + +In these situations, the [User]'s funds will remain safely in escrow on the FastBridge contract until a [cancel] function is executed. This function can be executed permissionlessly after a [Cancel Delay] period has passed. + +A permissioned [Canceler] entity can also execute the [cancel] function prior to the cancellation delay, but only after the relay expiration [deadline](https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgeparams) indicated the BridgeParams of the bridge request. This allows cancellations to occur more rapidly when appropriate, at the discretion of the [Canceler]. + +If there is already a [proof] on file for the transaction, then cancellation will be prevented. (IE: Cannot attempt to [cancel] a transaction which appears to have already been successfully completed) + +### Effects of a cancel transaction + +Regardless of the manner of cancellation, the [User]'s escrowed funds will be transferred from FastBridge back to the `originSender` of the original bridge transaction. + +Additionally, a [BridgeDepositRefunded](https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgedepositrefunded) event will be emitted. + +No further action is possible with the bridge after a cancellation and it can be considered closed. diff --git a/docs/bridge/docs/03-RFQ/60-Security/index.md b/docs/bridge/docs/03-RFQ/60-Security/index.md new file mode 100644 index 0000000000..f2c0e57c8d --- /dev/null +++ b/docs/bridge/docs/03-RFQ/60-Security/index.md @@ -0,0 +1,80 @@ +--- +title: Security +--- + + +[relay]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#relayv2 +[prove]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#provev2 +[dispute]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#dispute +[claim]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#claimv2 +[cancel]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#cancelv2 +[proof]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#bridgetxdetails +[BridgeRequested]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgerequested +[BridgeTransactionV2]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#bridgetransactionv2 +[BridgeRelayed]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgerelayed +[BridgeProofProvided]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgeproofprovided +[Cancel Delay]: https://rfq-contracts.synapseprotocol.com/contracts/FastBridge.sol/contract.FastBridge.html#refund_delay +[Multicall]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IMulticallTarget.sol/interface.IMulticallTarget.html + +[Quoter API]: /docs/RFQ/Quoting/Quoter%20API/ +[Dispute Period]: /docs/RFQ/Security/#dispute-period +[Quoting]: /docs/RFQ/Quoting +[Bridging]: /docs/RFQ/Bridging +[Relaying]: /docs/RFQ/Relaying +[Proving]: /docs/RFQ/Proving +[Claiming]: /docs/RFQ/Claiming +[Canceling]: /docs/RFQ/Canceling +[Security]: /docs/RFQ/Security +[Exclusivity]: /docs/RFQ/Exclusivity + +[User]: /docs/RFQ/#entities +[Quoter]: /docs/RFQ/#entities +[Prover]: /docs/RFQ/#entities +[Relayer]: /docs/RFQ/#entities +[Guard]: /docs/RFQ/#entities +[Canceler]: /docs/RFQ/#entities + + +Synapse Intent Network (SIN) is an optimistic cross-chain system. This means that any ambiguous actions in the system are assumed to be accurate and honest by default unless they are challenged/disputed within a short timeframe. + + +### Proofs + +With this system in particular, [prove] transactions are the focal point of this optimistic mechanism - whereby a [Relayer] is asserting that they completed a [relay] and can rightfully [claim] the escrowed bridge funds as a reimbursement. + +Each [prove] transaction sets the [proof] data for the bridge and initiates a dispute period. + + +### Dispute Period + +After a [prove] transaction is posted and the [proof] data is set, a window of time called the [Dispute Period](https://rfq-contracts.synapseprotocol.com/contracts/FastBridgeV2.sol/contract.FastBridgeV2.html#dispute_period) begins. + +During this time, the prove/proof is eligible to be disputed by [Guard] entities. + +After the [Dispute Period](https://rfq-contracts.synapseprotocol.com/contracts/FastBridgeV2.sol/contract.FastBridgeV2.html#dispute_period) has passed without any disputes, the funds in escrow from the original bridge transaction can be released via a [claim] transaction, which will reimburse the rightful [Relayer]. + + +### Guards + +During the dispute period, off-chain [Guard] entities provide the security function of evaluating the [relay] asserted by the [prove]: + +- Does the asserted [relay] transaction exist in a finalized state on the destination chain? + +- Do all [BridgeTransactionV2] parameters of the destination [relay] match the origin bridge? + +- Is the `relayer` asserted by the [prove] the same as the `relayer` of the [relay]? + +If any discrepancies are found, the [Guard] will execute a [dispute]. + + +### Effects of a [dispute] Transaction + +When a [dispute] is executed, it effectively negates/erases the [proof] which was asserted by the disputed [prove] transaction. + +This allows for a new corrected [prove] to be submitted and the process begins again. + +If a [relay] truly did occur for the disputed [prove], but it was not for the correct bridge parameters, this constitutes an [invalid relay](/docs/RFQ/Relaying/riskFactors#invalid-relays). + +Additionally, a [BridgeProofDisputed](https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgeproofdisputed) event will be emitted. + +This event can be useful for monitoring / alerting. diff --git a/docs/bridge/docs/04-Routers/RFQ/dashboard.png b/docs/bridge/docs/03-RFQ/70-CanonicalRelayer/dashboard.png similarity index 100% rename from docs/bridge/docs/04-Routers/RFQ/dashboard.png rename to docs/bridge/docs/03-RFQ/70-CanonicalRelayer/dashboard.png diff --git a/docs/bridge/docs/04-Routers/RFQ/01-Relayer.md b/docs/bridge/docs/03-RFQ/70-CanonicalRelayer/index.md similarity index 70% rename from docs/bridge/docs/04-Routers/RFQ/01-Relayer.md rename to docs/bridge/docs/03-RFQ/70-CanonicalRelayer/index.md index 23e8c04e67..ff32ac1efc 100644 --- a/docs/bridge/docs/04-Routers/RFQ/01-Relayer.md +++ b/docs/bridge/docs/03-RFQ/70-CanonicalRelayer/index.md @@ -1,23 +1,66 @@ --- -sidebar_position: 0 -sidebar_label: Relayer +sidebar_label: Canonical Relayer --- -# RFQ Relayer + +[relay]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#relayv2 +[prove]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#provev2 +[dispute]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#dispute +[claim]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#claimv2 +[cancel]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#cancelv2 +[proof]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#bridgetxdetails +[BridgeRequested]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgerequested +[BridgeTransactionV2]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#bridgetransactionv2 +[BridgeRelayed]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgerelayed +[BridgeProofProvided]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgeproofprovided +[Cancel Delay]: https://rfq-contracts.synapseprotocol.com/contracts/FastBridge.sol/contract.FastBridge.html#refund_delay +[Multicall]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IMulticallTarget.sol/interface.IMulticallTarget.html + +[Quoter API]: /docs/RFQ/Quoting/Quoter%20API/ +[Dispute Period]: /docs/RFQ/Security/#dispute-period +[Quoting]: /docs/RFQ/Quoting +[Bridging]: /docs/RFQ/Bridging +[Relaying]: /docs/RFQ/Relaying +[Proving]: /docs/RFQ/Proving +[Claiming]: /docs/RFQ/Claiming +[Canceling]: /docs/RFQ/Canceling +[Security]: /docs/RFQ/Security +[Exclusivity]: /docs/RFQ/Exclusivity + +[User]: /docs/RFQ/#entities +[Quoter]: /docs/RFQ/#entities +[Prover]: /docs/RFQ/#entities +[Relayer]: /docs/RFQ/#entities +[Guard]: /docs/RFQ/#entities +[Canceler]: /docs/RFQ/#entities + +# Canonical Relayer + +:::info + +Relaying itself is permissionless as of FastBridgeV2 - but other necessary supporting functions of this Relayer (Quoting, Proving) are permissioned. + +If you are interested in participating as a Relayer, contact us. -:::note +::: -Relayers must be whitelisted in order to fulfill bridge requests. +The Canonical Relayer is the official relayer app built and operated by Synapse which performs all necessary automated functions that are necessary for a Relayer participating in the Synapse Intent Network (SIN) network. -::: +These functions include: -A Go application which coordinates on-chain events and stored message states to relay user funds. Relayers are easily observable. +- [Quoting] +- [Relaying] +- [Proving] +- [Claiming] +- Rebalancing -The canonical RFQ Relayer handles **three event loops**: quoting routes, approving relays, and rebalancing funds. +## Stack -## Quote +The canonical RFQ Relayer is primarily built in Go with a MySQL backend. It also utilizes other tools and services built by Synapse, such as the [Submitter](/docs/Services/Submitter). -Continuously track and update route quotes based on changes to available and in-flight balances via [API](API). The quote should update each time the available balance or other parameters change. +## Quoting + +Continuously track and update route quotes via the [Quoter API](/docs/RFQ/Quoting/Quoter%20API/) based on changes to balances, prices, and fees. | Param | Description |---------|- @@ -27,24 +70,27 @@ Continuously track and update route quotes based on changes to available and in- -## Relay +## Relaying -Listen to on-chain events and database updates to move [`BridgeRequest`](https://vercel-rfq-docs.vercel.app/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgeparams) objects through the following states: +Listen to on-chain events and database updates to move [`BridgeRequest`](https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#bridgeparams) objects through the following states: | State | Description |----------------------|- -| `Seen` | [`BridgeRequested`](https://vercel-rfq-docs.vercel.app/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgerequested) event stored in the db. -| `WillNotProcess` | [`BridgeRequested`](https://vercel-rfq-docs.vercel.app/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgerequested) event is invalid +| `Seen` | [`BridgeRequested`](https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#bridgerequested) event stored in the db. +| `WillNotProcess` | [`BridgeRequested`](https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#bridgerequested) event is invalid | `NotEnoughInventory` | Retry later -| `CommittedPending` | All checks pass, waiting for transaction to finalize -| `CommittedConfirmed` | Transaction is finalized on-chain -| `RelayPending` | Called [`relay`](https://vercel-rfq-docs.vercel.app/contracts/FastBridge.sol/contract.FastBridge.html#relay) -| `RelayComplete` | Relay completed on-chain -| `ProvePosting` | Called [`prove`](https://vercel-rfq-docs.vercel.app/contracts/FastBridge.sol/contract.FastBridge.html#prove) -| `ClaimPending` | Called [`claim`](https://vercel-rfq-docs.vercel.app/contracts/FastBridge.sol/contract.FastBridge.html#claim) -| `ClaimComplete` | Dispute period expired, funds received. +| `CommittedPending` | All checks pass. Inventory is committed to the fill. Awaiting [finalization](https://medium.com/@theblockchains/finality-in-blockchain-d287a087a9b9) of the BridgeRequested transction. +| `CommittedConfirmed` | BridgeRequested transaction is [finalized](https://medium.com/@theblockchains/finality-in-blockchain-d287a087a9b9) on-chain. If conditions are still right, a relay will occur. +| `RelayPending` | A [`relay`](https://rfq-contracts.synapseprotocol.com/contracts/FastBridgeV2.sol/contract.FastBridgeV2.html#relay) tx has been submitted to the chain. +| `RelayComplete` | The submitted `relay` transaction has successfully landed on-chain. +| `ProvePosting` | Called [`prove`](https://rfq-contracts.synapseprotocol.com/contracts/FastBridgeV2.sol/contract.FastBridgeV2.html#prove) tx has been submitted to the chain. +| `ProvePosted` | The submitted `prove` transaction has successfully landed on-chain. +| `ClaimPending` | The `Dispute Period` has passed and the relayer has submitted a [`claim`](https://rfq-contracts.synapseprotocol.com/contracts/FastBridgeV2.sol/contract.FastBridgeV2.html#claim) tx to the chain. +| `ClaimComplete` | The submitted `claim` transaction has successfully landed on-chain and the funds from the original `BridgeRequested` tx have been released to the relayer - less any fees. +| `RelayRaceLost` | Another Relayer has completed the relay. + -## Rebalance +## Rebalancing For cases where flows are mono-directional, the Relayer provides an interface for rebalancing funds. @@ -84,7 +130,7 @@ At a high level, the rebalancer checks inventory on Scroll versus other chains, ::: -## Configure +## Configuration RFQ Relayer requires a YAML configuration file path to be provided at run time. @@ -103,7 +149,7 @@ RFQ Relayer requires a YAML configuration file path to be provided at run time. * `type`: Database driver to use, can be `mysql` or `sqlite`. * `dsn`: 'Data Source Name'. If using SQLite, this can be a path. For MySQL see [here](https://dev.mysql.com/doc/connector-odbc/en/connector-odbc-configuration.html) for more information. * `screener_api_url` (optional): [Screener API](https://github.com/synapsecns/sanguine/tree/master/contrib/screener-api#screening-api) -* `rfq_url`: [Mainnet & Testnet addresses](API/#api-urls) +* `rfq_url`: [Mainnet & Testnet addresses](/docs/RFQ/Quoting/Quoter%20API/#api-urls) * `omnirpc_url`: [Running an OmniRPC instance](/docs/Services/Omnirpc) * `rebalance_interval`: How often to rebalance. Can use `s` (seconds), `m` (minutes), or `h` (hours) * `relayer_api_port`: Controls the relayer. Should be private or secured. @@ -272,7 +318,7 @@ Clone the Sanguine repository, then run the main.go file along with the path to ## sendChainGas -Boolean flag available to Bridge users. When `sendChainGas` is `true`, the amount to send is specified as `chainGasAmount` in the FastBridge contract on the destination chain. +Boolean flag available to Bridge users. When `sendChainGas` is `true`, the amount to send is specified as `chainGasAmount` in the FastBridgeV2 contract on the destination chain. :::note diff --git a/docs/bridge/docs/03-RFQ/75-Exclusivity/index.md b/docs/bridge/docs/03-RFQ/75-Exclusivity/index.md new file mode 100644 index 0000000000..8d1ce59fa0 --- /dev/null +++ b/docs/bridge/docs/03-RFQ/75-Exclusivity/index.md @@ -0,0 +1,71 @@ +--- +title: Exclusivity +--- + + +[relay]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#relayv2 +[prove]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#provev2 +[dispute]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#dispute +[claim]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#claimv2 +[cancel]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#cancelv2 +[proof]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#bridgetxdetails +[BridgeRequested]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgerequested +[BridgeTransactionV2]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#bridgetransactionv2 +[BridgeRelayed]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgerelayed +[BridgeProofProvided]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgeproofprovided +[Cancel Delay]: https://rfq-contracts.synapseprotocol.com/contracts/FastBridge.sol/contract.FastBridge.html#refund_delay +[Multicall]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IMulticallTarget.sol/interface.IMulticallTarget.html + +[Quoter API]: /docs/RFQ/Quoting/Quoter%20API/ +[Dispute Period]: /docs/RFQ/Security/#dispute-period +[Quoting]: /docs/RFQ/Quoting +[Bridging]: /docs/RFQ/Bridging +[Relaying]: /docs/RFQ/Relaying +[Proving]: /docs/RFQ/Proving +[Claiming]: /docs/RFQ/Claiming +[Canceling]: /docs/RFQ/Canceling +[Security]: /docs/RFQ/Security +[Exclusivity]: /docs/RFQ/Exclusivity + +[User]: /docs/RFQ/#entities +[Quoter]: /docs/RFQ/#entities +[Prover]: /docs/RFQ/#entities +[Relayer]: /docs/RFQ/#entities +[Guard]: /docs/RFQ/#entities +[Canceler]: /docs/RFQ/#entities + +Starting with FastBridgeV2, specific Relayers can optionally be granted temporary exclusive rights to be the first to complete a [relay] for a given bridge. + + +## Benefits of Exclusivity + +Utilizing exclusivity minimizes the wasteful aspects of competing relayers "racing" on the open-market to be the first to fill, allowing them to offer more efficient operations & pricing. + + +## How to Decide Exclusivity + +Exclusivity settings are an optional tool offered to integrators and users - the functionality is available to be used in any way that suits them. For example, exclusivity can be used to implement an off-chain auction system, or to distribute relays to a private liquidity network. + + +## How to Set Exclusivity + +Exclusivity can be activated and configured per bridge. Users and Integrators simply need to determine the following values off-chain and assign them as part of [BridgeParamsV2](https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#bridgeparamsv2) when the bridge deposit transaction is constructed. + +| Parameter | Description | +|----------------------------|-------------------------------------------------------------------------------------------------------| +| quoteRelayer | The address of the Relayer who will have temporarily exclusive fill rights. | +| quoteExclusivitySeconds | The duration in seconds for which the `quoteRelayer` will have exclusive fill rights. The countdown begins when the bridge lands on the origin chain and is based on the transaction's block timestamp. | + +Note: The seconds setting can be as granular and fine-tuned as desired. However, for simple implementations, thirty seconds is more than enough time for most relays. + + +## Relaying an Exclusive Fill + +Exclusivity is enforced by the FastBridge contract according to two values that will be on the [BridgeTransactionV2] struct emitted on the [BridgeRequested] event. + +| Parameter | Description | +|----------------------|-------------------------------------------------------------------------------------------------------| +| exclusivityRelayer | This will be set to the address of the [Relayer] who has been granted first-fill rights. | +| exclusivityEndTime | Once the Block timestamp exceeds this value, anyone can complete the relay if it has not yet been completed. | + +Relayers who are *not* the assigned `exclusivityRelayer` can wait until the block timestamp of the destination has surpased the `exclusivityEndTime` and then attempt the relay for themselves, if it was not filled in time by the assignee. diff --git a/docs/bridge/docs/03-RFQ/90-Misc/index.md b/docs/bridge/docs/03-RFQ/90-Misc/index.md new file mode 100644 index 0000000000..f8b5100440 --- /dev/null +++ b/docs/bridge/docs/03-RFQ/90-Misc/index.md @@ -0,0 +1,68 @@ +--- +title: Miscellaneous +--- + + +[relay]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#relayv2 +[prove]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#provev2 +[dispute]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#dispute +[claim]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#claimv2 +[cancel]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#cancelv2 +[proof]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#bridgetxdetails +[BridgeRequested]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgerequested +[BridgeTransactionV2]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#bridgetransactionv2 +[BridgeRelayed]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgerelayed +[BridgeProofProvided]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgeproofprovided +[Cancel Delay]: https://rfq-contracts.synapseprotocol.com/contracts/FastBridge.sol/contract.FastBridge.html#refund_delay +[Multicall]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IMulticallTarget.sol/interface.IMulticallTarget.html + +[Quoter API]: /docs/RFQ/Quoting/Quoter%20API/ +[Dispute Period]: /docs/RFQ/Security/#dispute-period +[Quoting]: /docs/RFQ/Quoting +[Bridging]: /docs/RFQ/Bridging +[Relaying]: /docs/RFQ/Relaying +[Proving]: /docs/RFQ/Proving +[Claiming]: /docs/RFQ/Claiming +[Canceling]: /docs/RFQ/Canceling +[Security]: /docs/RFQ/Security +[Exclusivity]: /docs/RFQ/Exclusivity + +[User]: /docs/RFQ/#entities +[Quoter]: /docs/RFQ/#entities +[Prover]: /docs/RFQ/#entities +[Relayer]: /docs/RFQ/#entities +[Guard]: /docs/RFQ/#entities +[Canceler]: /docs/RFQ/#entities + +## FastBridge Contract Utilities + + +### Call from Origin Chain: +
+#### [canClaim](https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#canclaim) + +Checks if the [Dispute Period] has passed and the funds are now claimable + +##### [bridgeStatuses](https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#bridgestatuses) + +Returns the current [BridgeStatus](https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#bridgestatus) of the indicated `transactionId` + +##### [bridgeProofs](https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#bridgeproofs) + +Returns the details of a bridge proof, including the asserted `relayer` + +#### [getBridgeTransaction](https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#getbridgetransactionv2) + +Decodes request bytes (such as those emitted from a [BridgeRequested] event) into a [BridgeTransactionV2] struct. +
+ +### Call from Destination Chain: +
+##### [bridgeRelays](https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#bridgerelays) + +Checks if the indicated `transactionId` has already been relayed +
+ +## RFQ 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. diff --git a/docs/bridge/docs/03-RFQ/index.md b/docs/bridge/docs/03-RFQ/index.md new file mode 100644 index 0000000000..cef0db07a9 --- /dev/null +++ b/docs/bridge/docs/03-RFQ/index.md @@ -0,0 +1,210 @@ +--- +title: Synapse Intent Network +--- + +import { RFQFlow } from '@site/src/components/RFQFlow' + + +[relay]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#relayv2 +[prove]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#provev2 +[dispute]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#dispute +[claim]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#claimv2 +[cancel]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#cancelv2 +[proof]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#bridgetxdetails +[BridgeRequested]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgerequested +[BridgeTransactionV2]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridgeV2.sol/interface.IFastBridgeV2.html#bridgetransactionv2 +[BridgeRelayed]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgerelayed +[BridgeProofProvided]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgeproofprovided +[Cancel Delay]: https://rfq-contracts.synapseprotocol.com/contracts/FastBridge.sol/contract.FastBridge.html#refund_delay +[Multicall]: https://rfq-contracts.synapseprotocol.com/contracts/interfaces/IMulticallTarget.sol/interface.IMulticallTarget.html + +[Quoter API]: /docs/RFQ/Quoting/Quoter%20API/ +[Dispute Period]: /docs/RFQ/Security/#dispute-period +[Quoting]: /docs/RFQ/Quoting +[Bridging]: /docs/RFQ/Bridging +[Relaying]: /docs/RFQ/Relaying +[Proving]: /docs/RFQ/Proving +[Claiming]: /docs/RFQ/Claiming +[Canceling]: /docs/RFQ/Canceling +[Security]: /docs/RFQ/Security +[Exclusivity]: /docs/RFQ/Exclusivity + +[User]: /docs/RFQ/#entities +[Quoter]: /docs/RFQ/#entities +[Prover]: /docs/RFQ/#entities +[Relayer]: /docs/RFQ/#entities +[Guard]: /docs/RFQ/#entities +[Canceler]: /docs/RFQ/#entities + +# Synapse Intent Network + +Synapse Intent Network (SIN) is an RFQ (Request-For-Quote) based intent centric bridging system that connects bridging users to a network of relayers. + +These relayers compete to provide the optimal bridge execution (eg: the best price) for the user's specific request. + +

Request-For-Quote End-to-End Flow

+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
OrderUser inputs their desired order into a bridge interface.
QuoteQuotes from Relayers are evaluated and resolved to the optimal choice to match the user's input. +

The resolved quote is used to construct a bridge transaction for the user to sign.
RequestUser signs and submits the bridge transaction on-chain. +

Their assets are deposited into a Bridge contract on the `originChain`.
RelayA [Relayer] completes the user's bridge on the `destChain` by delivering the user's desired funds. +

At this point, the bridge is complete from the user's perspective.
ProveThe [Relayer] submits an optimistic proof of their completed relay to the `originChain`.
GuardAfter a proof is asserted for each bridge, a brief optimistic dispute period is initiated. +

During this period, [Guard] entities will verify that the proof is valid and accurate.
ClaimAfter the dispute period has passed, the [Relayer] may claim the user's originally deposited funds. +

This reimburses the [Relayer] for the funds which they delivered in the earlier [relay] step.
+
+ +## Entities + +Users (Permissionless Role) +
+ Uses a bridge interface to submit their intent to the RFQ system for a quote, and then to the chain for fulfillment. +

+ +Quoters (Permissioned Role) +
+ Posts Passive and/or Active Quotes via the [Quoter API] to be matched against user bridge inputs. +
+ Note: In practice, each participating Quoter typically acts as their own Relayer and Prover. + +

+ +Relayers (Permissionless Role) +
+ Observes bridge deposits and submits [relay] transactions to fulfill them. +
+ Also submits `claim` transactions to be reimbursed for their relays. +
+ Note: In practice, each participating Relayer typically acts as their own Quoter and Prover. +

+ +Provers (Permissioned Role) +
+ Observes [relay]s and submits [prove] transactions to initiate relayer reimbursements. +
+ Note: In practice, each participating Prover typically acts as their own Quoter and Relayer. +

+ +Guards (Permissioned Role) +
+ Validates proofs during the [Dispute Period] and submits [dispute] transactions for any discrepancies found. +
+ Currently, Synapse itself is the sole Guard operator of the Synapse Intent Network (SIN) system. +

+ +Cancelers (Permissioned Role) +
+ Able to manually pre-emptively cancel bridge requests which have been deposited, have not yet been relayed, and are past their relay deadline. +
+ Currently, Synapse itself is the sole Canceler operator of the Synapse Intent Network (SIN) system. +
+ Note: Incomplete bridge requests can also be canceled permissionlessly (without any involvement from a Canceler) after a longer cancellation window has passed. +
+ + + +## Architecture + +### Quoter API +The RESTful [Quoter API] allows Quoters to post Passive and/or Active Quotes. +These quotes are then used to create transactions for Users to sign and submit. + +### FastBridge Smart Contracts +[Synapse FastBridge contracts](/docs/Contracts/RFQ) facilitate and enforce all of the on-chain functionality and security of the system. + +This includes the bridge, deposit, escrow, and eventual release of user funds on the origin chain. + +Associated [relay], [prove], [claim], [dispute], and [cancel] actions are also facilitated by FastBridge contracts. + +Additionally, the FastBridge contracts manage all relevant permissions and on-chain configuration settings of the system. + +FastBridge contracts will be deployed on all supported chains. + + +## Flow Summary + +#### [Quoting] +
+A [User] inputs their desired bridge information into an bridge interface such as [Synapse](https://synapseprotocol.com/?fromChainId=1). + +Quotes to complete the bridge are collected from [Quoters]. The best of these is resolved and presented to the user as a bridge transaction to sign & submit on-chain. +
+ +#### [Bridging] +
+Once the [User] has signed and submitted their deposit on-chain via a bridge transaction, the bridging funds will be transferred to a FastBridge contract and held in escrow. + +
+ +#### [Relaying] + +
+Relayers who observe the bridge deposit event can permissionlessly complete the bridge by calling [relay] on the FastBridge contract of the destination chain. Typically the Relayer is the same as the Quoter. + +This will transfer the bridge destination funds from the [Relayer] to the [User], with FastBridge acting as intermediary. + +At this point, the process is complete from the [User]'s perspective. However, the [Relayer] needs to be reimbursed for the funds they delivered. +
+ +#### [Proving] + +
+ +After the relay, a [Prover] (typically also the Relayer) will submit a [prove] transaction with the relay transaction's details to the origin chain's FastBridge contract. + +This asserts that the [relay] for the bridge was completed according to the specifications of the [User], and that the [Relayer] can rightfully [claim] the escrowed funds. + +A [Dispute Period] begins for the transaction before the [claim] can occur. + +
+ +#### [Dispute Period] + +
+During the [Dispute Period], [Guard] entities will verify that the proof's assertion is accurate. + +IE: They will confirm that the [relay] being asserted was completed exactly as specified by the user. + +If any discrepancies are found, the guards will [dispute] the proof +
+ +#### [Claiming] + +
+Once the [Dispute Period] has passed without incident, a [claim] transaction can be executed by the [Relayer] on the origin chain. + +This will release the deposit funds from escrow and deliver them to the rightful [Relayer] as a reimbursement for the liquidity they provided on the [relay]. +
diff --git a/docs/bridge/docs/04-Routers/RFQ/API/index.md b/docs/bridge/docs/04-Routers/RFQ/API/index.md deleted file mode 100644 index 8abe0acb1e..0000000000 --- a/docs/bridge/docs/04-Routers/RFQ/API/index.md +++ /dev/null @@ -1,95 +0,0 @@ ---- -sidebar_position: 0 -sidebar_label: API ---- - -# RFQ API - -:::note - -This guide is mostly meant for developers who are working on building their own quoter or frontend for rfq. If you are just looking to run a relayer, please see [Relayer](../Relayer). If you are looking to integrate rfq, please see the API docs below the dropdown. - -::: - -The RFQ API is an off-chain service that allows market makers to post quotes for certain bridge routes & tokens. Users can then read these quotes and take the liquidity by submitting a transaction on chain through the [Fast Bridge Contract](https://vercel-rfq-docs.vercel.app/contracts/FastBridge.sol/contract.FastBridge.html). - -Solvers are responsible for keeping quotes fresh and implementations of the RFQ relayer should update quotes as often as possible. By default, the canonical [relayer](../Relayer) continuously updates quotes by checking on-chain balances and in-flight requests and other implementations should take a similar approach. - -The implementation of the rfq api can be found [here](https://github.com/synapsecns/sanguine/tree/master/services/rfq/api). Please note that end-users and solvers will not need to run their own version of the API. - -## Integrating the API - -The RFQ API is a RESTful API that allows users to post quotes and read quotes. The API is incredibly simple and only has one endpoint with two methods: - -- [`PUT /quotes`](./upsert-quote.api.mdx) (authenticated) - Upsert a quote -- [`GET /quotes`](./get-quotes.api.mdx) (unauthenticated) - Get all quotes, can be filtered by different parameters. - -Only Solvers should be writing to the API, end-users need only read from the `/quotes` endpoint. - -**API Version Changes** - -An http response header "X-Api-Version" will be returned on each call response. - -Any systems that integrate with the API should use this header to detect version changes and perform appropriate follow-up actions & alerts. - -Upon a version change, [versions.go](https://github.com/synapsecns/sanguine/blob/master/services/rfq/api/rest/versions.go) can be referred to for further detail on the version including deprecation alerts, etc. - -Please note, while Synapse may choose to take additional steps to alert & advise on API changes through other communication channels, it will remain the responsibility of the API users & integrators to set up their own detection & notifications of version changes as they use these endpoints. Likewise, it will be their responsibility review the versions.go file, to research & understand how any changes may affect their integration, and to implement any necessary adjustments resulting from the API changes. - -**Authentication** - -In accordance with [EIP-191](https://eips.ethereum.org/EIPS/eip-191), the RFQ API requires a signature to be sent with each request. The signature should be generated by the user's wallet and should be a valid signature of the message `rfq-api` with the user's private key. The signature should be sent in the `Authorization` header of the request. We provide a client stub/example implementation in go [here](https://pkg.go.dev/github.com/synapsecns/sanguine/services/rfq@v0.13.3/api/client). - -:::note - -The RFQ API expects the signatures to have V values as 0/1 rather than 27/28. The fastest way to fix this is to modify V by subtracting 27 - -::: - -### API Urls - - - Mainnet: `api.synapseprotocol.com/quotes` - - Testnet: `rfq-api-testnet.omnirpc.io` - - - -## Running the API: - -Users and relayers **are not** expected to run their own version of the RFQ API. The API is a service that should be run by Quoters and interfaces that allow Solvers to post quotes. The RFQ API takes in a yaml config that allows the user to specify which contracts, chains and interfaces it should run on. The config is structured like this: - -```yaml -database: - type: mysql # can be other mysql or sqlite - dsn: root:password@hostname:3306)/database?parseTime=true # should be the dsn of your database. If using sqlite, this can be a path -omnirpc_url: https://route-to-my-omnirpc # omnirpc route -bridges: - 1: '0x00......' # FastBridge address on ethereum (chain id: 1) - 10: '0x01....' # FastBridge address on op (chain id: 10) -port: '8081' # port to run your http server on -``` - -Yaml settings: - -- `database` - The database settings for the API backend. A database is required to store quotes and other information. Using SQLite with a dsn set to a `/tmp/` directory is recommended for development. - - `type` - the database driver to use, can be `mysql` or `sqlite`. - - `dsn` - the dsn of your database. If using sqlite, this can be a path, if using mysql please see [here](https://dev.mysql.com/doc/connector-odbc/en/connector-odbc-configuration.html) for more information. -- `omnirpc_url` - The omnirpc url to use for querying chain data (no trailing slash). For more information on omnirpc, see [here](/docs/Services/Omnirpc). -- `bridges` - A key value map of chain id to FastBridge contract address. The API will only allow quotes to be posted on these chains. -- `port` - The port to run the http server on. - -**Building From Source:** - -To build the RFQ API from source, you will need to clone the repository and run the main.go file with the config file. Building from source requires go 1.21 or higher and is generally not recommended for end-users. - -1. `git clone https://github.com/synapsecns/sanguine --recursive` -2. `cd sanguine/services/rfq` -3. `go run main.go --config /path/to/config.yaml` - -**Running with Docker** - -The RFQ API can also be run with docker. To do this, you will need to build the docker image and run it with the config file. - -:::tip -Docker versions should always be pinned in production environments. For a full list of tags, see [here](https://github.com/synapsecns/sanguine/pkgs/container/sanguine%2Frfq-api) -::: - -1. `docker run ghcr.io/synapsecns/sanguine/rfq-api:latest --config /path/to/config` diff --git a/docs/bridge/docs/04-Routers/RFQ/index.md b/docs/bridge/docs/04-Routers/RFQ/index.md deleted file mode 100644 index b2a498f803..0000000000 --- a/docs/bridge/docs/04-Routers/RFQ/index.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -sidebar_label: RFQ ---- - -import { RFQFlow } from '@site/src/components/RFQFlow' - -# RFQ Router - -A [Synapse Router](../Synapse-Router) bridge module which matches on-chain user requests against bridge quotes posted by decentralized [Relayers](Relayer). - -
- -
User assets are sent to a Bridge contract, and held until a Solver executes their quote on the `destChain`.
-
- -## Architecture - -[Synapse Fast Bridge contracts](/docs/Contracts/RFQ) coordinate decentralized Solvers to match user requests against the best quote for a given route, and secure user funds while their transaction is fulfilled. - - - -| Agents | Description -|---------|- -| Quoters | Quote distribution services run through traditional [APIs](API), or protocols like libp2p, irc, or dht. -| Solvers | Posts, then fulfills, route quotes through a [Relayer](Relayer), when matched by the Fast Bridge contract against a user request. -| Users | Uses a route quote to form a bridge request which is matched on-chain by the solver who posted the quote. -| Guards | Raises a dispute if errors or fraudulent activity are detected. - -## Behavior - -After receiving a [`BridgeRequest`](https://vercel-rfq-docs.vercel.app/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgeparams) (broadcast as a [`BridgeRequested`](https://vercel-rfq-docs.vercel.app/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgerequested) event), a Solver executes the transaction by calling [`relay`](https://vercel-rfq-docs.vercel.app/contracts/FastBridge.sol/contract.FastBridge.html#relay) on the Bridge contract. - -The Bridge relays the requested funds ([`msg.value`](https://ethereum.stackexchange.com/questions/43362/what-is-msg-value) in the case of ETH) from Solver to User, allowing the Solver that accepted the bridge to call [`prove`](https://vercel-rfq-docs.vercel.app/contracts/FastBridge.sol/contract.FastBridge.html#prove) on the Bridge contract, and receive their funds at the end of the optimistic period - -| `#` | State | Description -|-----|-------------|- -| `0` | `Null` | Bridge transaction does not exist yet on origin chain -| `1` | `Requested` | [`BridgeRequested`](https://vercel-rfq-docs.vercel.app/contracts/interfaces/IFastBridge.sol/interface.IFastBridge.html#bridgerequested) event broadcast. Waiting for Relayer -| `2` | `Proved` | Relayer called [`relay`](https://vercel-rfq-docs.vercel.app/contracts/FastBridge.sol/contract.FastBridge.html#relay), and [`prove`](https://vercel-rfq-docs.vercel.app/contracts/FastBridge.sol/contract.FastBridge.html#prove), and is waiting for the optimistic period to end. -| `3` | `Claimed` | Relayer called [`claim`](https://vercel-rfq-docs.vercel.app/contracts/FastBridge.sol/contract.FastBridge.html#claim) and received their funds. -| `4` | `Refunded` | Relayer did not claim within the optimistic period, or a dispute was decided in favor of the user. - - - - - - - - - - - - - - - - - -## Dispute Period and Guards - -The RFQ system includes an optimistic dispute window in which Guard contracts may initiate a dispute if they detect errors or fraudulent activity, such as incorrect fill amounts or proofs submitted by the wrong relayer. - -In a successful dispute, the relayer loses their claimable funds. This design is intended to enforce honest behavior while also protecting honest relayers in cases of blockchain reorgs. - -## 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/docs/05-Contracts/06-RFQ.md b/docs/bridge/docs/05-Contracts/06-RFQ.md index 3e9691a1d2..2f6668dede 100644 --- a/docs/bridge/docs/05-Contracts/06-RFQ.md +++ b/docs/bridge/docs/05-Contracts/06-RFQ.md @@ -12,8 +12,30 @@ The canonical list is hosted within the SynapseCNS on [Github](https://github.co RFQ transactions primarily interact with the Router Address, which is responsible for routing transactions with the proper meta data to the RFQ contract on the relevant chain. +## FastBridge Version 2 + +**Source code**: [SynapseCNS (Github)](https://github.com/synapsecns/sanguine/tree/master/packages/contracts-rfq)\ +**Generated docs**: [Contract docs](https://rfq-contracts.synapseprotocol.com/contracts/FastBridgeV2.sol/contract.FastBridgeV2.html)\ +**RFQ Router Address**: `0x00cD000000003f7F682BE4813200893d4e690000` + + +| Chain | Address | +| -------- | ------- | +| Arbitrum | `0xAABBCC` [↗](https://arbiscan.io/address/0xAABBCC) | +| Base | `0xAABBCC` [↗](https://basescan.org/address/0xAABBCC) | +| Ethereum | `0xAABBCC` [↗](https://etherscan.io/address/0xAABBCC) | +| Optimism | `0xAABBCC` [↗](https://optimistic.etherscan.io/address/0xAABBCC) | +| Scroll | `0xAABBCC` [↗](https://scrollscan.com/address/0xAABBCC) | +| Linea | `0xAABBCC` [↗](https://lineascan.build/address/0xAABBCC) | +| BNB Chain| `0xAABBCC` [↗](https://bscscan.com/address/0xAABBCC) | +| Blast | `0xAABBCC` [↗](https://blastscan.io/address/0xAABBCC) | +| World | `0xAABBCC` [↗](https://worldscan.org/address/0xAABBCC) | + + +## FastBridge Version 1 + **Source code**: [SynapseCNS (Github)](https://github.com/synapsecns/sanguine/tree/master/packages/contracts-rfq)\ -**Generated docs**: [RFQ docs](https://vercel-rfq-docs.vercel.app/contracts/FastBridge.sol/contract.FastBridge.html)\ +**Generated docs**: [Contract docs](https://rfq-contracts.synapseprotocol.com/contracts/FastBridge.sol/contract.FastBridge.html)\ **RFQ Router Address**: `0x00cD000000003f7F682BE4813200893d4e690000` | Chain | Address | @@ -26,3 +48,4 @@ RFQ transactions primarily interact with the Router Address, which is responsibl | Linea | `0x34F52752975222d5994C206cE08C1d5B329f24dD` [↗](https://lineascan.build/address/0x34F52752975222d5994C206cE08C1d5B329f24dD) | | BNB Chain| `0x5523D3c98809DdDB82C686E152F5C58B1B0fB59E` [↗](https://bscscan.com/address/0x5523D3c98809DdDB82C686E152F5C58B1B0fB59E) | | Blast | `0x34F52752975222d5994C206cE08C1d5B329f24dD` [↗](https://blastscan.io/address/0x34F52752975222d5994C206cE08C1d5B329f24dD) | +| World | `0x05c62156c7c47e76223a560210ea648de5e6b53b` [↗](https://worldscan.org/address/0x05c62156c7c47e76223a560210ea648de5e6b53b) | diff --git a/docs/bridge/docs/05-Contracts/09-SYN.md b/docs/bridge/docs/05-Contracts/09-SYN.md new file mode 100644 index 0000000000..e5a8d3e78c --- /dev/null +++ b/docs/bridge/docs/05-Contracts/09-SYN.md @@ -0,0 +1,32 @@ +--- +title: $SYN Token +--- + +:::note This list may be incomplete + +The canonical list is hosted within the SynapseCNS on [Github](https://github.com/synapsecns/synapse-contracts). + +::: + +# $SYN + +| Chain | Address | +| --------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| Arbitrum | `0x080f6aed32fc474dd5717105dba5ea57268f46eb` [↗](https://arbiscan.io/address/0x080f6aed32fc474dd5717105dba5ea57268f46eb) | +| Aurora | `0xd80d8688b02B3FD3afb81cDb124F188BB5aD0445` [↗](https://explorer.mainnet.aurora.dev/address/0xd80d8688b02B3FD3afb81cDb124F188BB5aD0445) | +| Avalanche | `0x1f1E7c893855525b303f99bDF5c3c05Be09ca251` [↗](https://snowscan.xyz/address/0x1f1E7c893855525b303f99bDF5c3c05Be09ca251) | +| Base | `0x432036208d2717394d2614d6697c46DF3Ed69540` [↗](https://basescan.org/address/0x432036208d2717394d2614d6697c46DF3Ed69540) | +| Blast | `0x9592f08387134e218327E6E8423400eb845EdE0E` [↗](https://blastscan.io/address/0x9592f08387134e218327E6E8423400eb845EdE0E) | +| Boba | `0xb554A55358fF0382Fb21F0a478C3546d1106Be8c` [↗](https://blockexplorer.boba.network/address/0xb554A55358fF0382Fb21F0a478C3546d1106Be8c) | +| BSC | `0xa4080f1778e69467e905b8d6f72f6e441f9e9484` [↗](https://bscscan.com/address/0xa4080f1778e69467e905b8d6f72f6e441f9e9484) | +| Canto | `0x555982d2E211745b96736665e19D9308B615F78e` [↗](https://canto.dex.guru/address/0x555982d2E211745b96736665e19D9308B615F78e) | +| Cronos | `0xFD0F80899983b8D46152aa1717D76cba71a31616` [↗](https://cronos.org/explorer/address/0xFD0F80899983b8D46152aa1717D76cba71a31616) | +| DFK Chain | `0xB6b5C854a8f71939556d4f3a2e5829F7FcC1bf2A` [↗](https://dfkchain.com/address/0xB6b5C854a8f71939556d4f3a2e5829F7FcC1bf2A) | +| Ethereum | `0x0f2D719407FdBeFF09D87557AbB7232601FD9F29` [↗](https://etherscan.io/address/0x0f2D719407FdBeFF09D87557AbB7232601FD9F29) | +| Fantom | `0xE55e19Fb4F2D85af758950957714292DAC1e25B2` [↗](https://ftmscan.com/address/0xE55e19Fb4F2D85af758950957714292DAC1e25B2) | +| Harmony | `0xE55e19Fb4F2D85af758950957714292DAC1e25B2` [↗](https://explorer.harmony.one/address/0xE55e19Fb4F2D85af758950957714292DAC1e25B2) | +| Metis | `0x67c10c397dd0ba417329543c1a40eb48aaa7cd00` [↗](https://andromeda-explorer.metis.io/address/0x67c10c397dd0ba417329543c1a40eb48aaa7cd00) | +| Moonbeam | `0xF44938b0125A6662f9536281aD2CD6c499F22004` [↗](https://moonbeam.moonscan.io/address/0xF44938b0125A6662f9536281aD2CD6c499F22004) | +| Moonriver | `0xd80d8688b02B3FD3afb81cDb124F188BB5aD0445` [↗](https://moonriver.moonscan.io/address/0xd80d8688b02B3FD3afb81cDb124F188BB5aD0445) | +| Optimism | `0x5A5fFf6F753d7C11A56A52FE47a177a87e431655` [↗](https://optimistic.etherscan.io/address/0x5A5fFf6F753d7C11A56A52FE47a177a87e431655) | +| Polygon | `0xf8f9efc0db77d8881500bb06ff5d6abc3070e695` [↗](https://polygonscan.com/address/0xf8f9efc0db77d8881500bb06ff5d6abc3070e695) | diff --git a/docs/bridge/docusaurus.config.ts b/docs/bridge/docusaurus.config.ts index 15c56367b2..2c64ebfbe3 100644 --- a/docs/bridge/docusaurus.config.ts +++ b/docs/bridge/docusaurus.config.ts @@ -15,7 +15,7 @@ const options = { // the referenced when running CLI commands specPath: '../../services/rfq/api/docs/swagger.yaml', // path to OpenAPI spec, URLs supported baseUrl: 'https://rfq-api.omnirpc.io/', - outputDir: 'docs/rfq/API', // output directory for generated files + outputDir: 'docs/04-Routers/RFQ/API', // output directory for generated files sidebarOptions: { // optional, instructs plugin to generate sidebar.js groupPathsBy: 'tag', // group sidebar items by operation "tag" diff --git a/docs/bridge/package.json b/docs/bridge/package.json index ff0fa5988d..de0c5ebf15 100644 --- a/docs/bridge/package.json +++ b/docs/bridge/package.json @@ -1,6 +1,6 @@ { "name": "@synapsecns/bridge-docs", - "version": "0.5.4", + "version": "0.5.13", "private": true, "scripts": { "docusaurus": "docusaurus", @@ -36,7 +36,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.8.2", + "@synapsecns/synapse-constants": "^1.8.5", "clsx": "^2.0.0", "docusaurus-plugin-openapi-docs": "^4.0.1", "docusaurus-theme-openapi-docs": "^4.0.1", diff --git a/packages/contracts-rfq/.solhintignore b/packages/contracts-rfq/.solhintignore index f2e4fd2a54..30d665167e 100644 --- a/packages/contracts-rfq/.solhintignore +++ b/packages/contracts-rfq/.solhintignore @@ -3,4 +3,4 @@ contracts/interfaces/IFastBridge.sol contracts/legacy/**/*.sol script/FastBridge.s.sol test/FastBridge.t.sol -test/FastBridgeMock.sol +test/mocks/FastBridgeMock.sol diff --git a/packages/contracts-rfq/CHANGELOG.md b/packages/contracts-rfq/CHANGELOG.md index 84f3d98166..3c258f337c 100644 --- a/packages/contracts-rfq/CHANGELOG.md +++ b/packages/contracts-rfq/CHANGELOG.md @@ -3,6 +3,76 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.14.7](https://github.com/synapsecns/sanguine/compare/@synapsecns/contracts-rfq@0.14.6...@synapsecns/contracts-rfq@0.14.7) (2024-12-05) + +**Note:** Version bump only for package @synapsecns/contracts-rfq + + + + + +## [0.14.6](https://github.com/synapsecns/sanguine/compare/@synapsecns/contracts-rfq@0.14.5...@synapsecns/contracts-rfq@0.14.6) (2024-11-28) + + +### Bug Fixes + +* **contracts-rfq:** `TokenZapV1` native gas token behaviour [SLT-389] ([#3418](https://github.com/synapsecns/sanguine/issues/3418)) ([ee3705a](https://github.com/synapsecns/sanguine/commit/ee3705a1fabf52746a933964d6d52ba1ca2379d6)) + + + + + +## [0.14.5](https://github.com/synapsecns/sanguine/compare/@synapsecns/contracts-rfq@0.14.4...@synapsecns/contracts-rfq@0.14.5) (2024-11-25) + +**Note:** Version bump only for package @synapsecns/contracts-rfq + + + + + +## [0.14.4](https://github.com/synapsecns/sanguine/compare/@synapsecns/contracts-rfq@0.14.3...@synapsecns/contracts-rfq@0.14.4) (2024-11-24) + +**Note:** Version bump only for package @synapsecns/contracts-rfq + + + + + +## [0.14.3](https://github.com/synapsecns/sanguine/compare/@synapsecns/contracts-rfq@0.14.2...@synapsecns/contracts-rfq@0.14.3) (2024-11-23) + +**Note:** Version bump only for package @synapsecns/contracts-rfq + + + + + +## [0.14.2](https://github.com/synapsecns/sanguine/compare/@synapsecns/contracts-rfq@0.14.1...@synapsecns/contracts-rfq@0.14.2) (2024-11-22) + +**Note:** Version bump only for package @synapsecns/contracts-rfq + + + + + +## [0.14.1](https://github.com/synapsecns/sanguine/compare/@synapsecns/contracts-rfq@0.14.0...@synapsecns/contracts-rfq@0.14.1) (2024-11-22) + +**Note:** Version bump only for package @synapsecns/contracts-rfq + + + + + +# [0.14.0](https://github.com/synapsecns/sanguine/compare/@synapsecns/contracts-rfq@0.13.0...@synapsecns/contracts-rfq@0.14.0) (2024-11-22) + + +### Features + +* **contracts-rfq:** Token Zap [SLT-389] ([#3352](https://github.com/synapsecns/sanguine/issues/3352)) ([743e859](https://github.com/synapsecns/sanguine/commit/743e859e3274ed449c6410441bd664ff2aaf9740)), closes [#3382](https://github.com/synapsecns/sanguine/issues/3382) + + + + + # [0.13.0](https://github.com/synapsecns/sanguine/compare/@synapsecns/contracts-rfq@0.12.1...@synapsecns/contracts-rfq@0.13.0) (2024-11-18) diff --git a/packages/contracts-rfq/contracts/Admin.sol b/packages/contracts-rfq/contracts/Admin.sol index ffb352b28a..b6617d262c 100644 --- a/packages/contracts-rfq/contracts/Admin.sol +++ b/packages/contracts-rfq/contracts/Admin.sol @@ -3,8 +3,8 @@ pragma solidity ^0.8.20; import {AccessControlEnumerable} from "@openzeppelin/contracts/access/extensions/AccessControlEnumerable.sol"; -import {UniversalTokenLib} from "./libs/UniversalToken.sol"; import {IAdmin} from "./interfaces/IAdmin.sol"; +import {UniversalTokenLib} from "./libs/UniversalToken.sol"; contract Admin is IAdmin, AccessControlEnumerable { using UniversalTokenLib for address; diff --git a/packages/contracts-rfq/contracts/AdminV2.sol b/packages/contracts-rfq/contracts/AdminV2.sol index 5a8cf447ad..64806426c3 100644 --- a/packages/contracts-rfq/contracts/AdminV2.sol +++ b/packages/contracts-rfq/contracts/AdminV2.sol @@ -1,77 +1,80 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; +// ════════════════════════════════════════════════ INTERFACES ═════════════════════════════════════════════════════ + import {IAdminV2} from "./interfaces/IAdminV2.sol"; import {IAdminV2Errors} from "./interfaces/IAdminV2Errors.sol"; +// ═════════════════════════════════════════════ EXTERNAL IMPORTS ══════════════════════════════════════════════════ + import {AccessControlEnumerable} from "@openzeppelin/contracts/access/extensions/AccessControlEnumerable.sol"; -import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +/// @title AdminV2 +/// @notice Provides administrative functions and controls for managing the FastBridgeV2 contract, +/// including access control and configuration settings. contract AdminV2 is AccessControlEnumerable, IAdminV2, IAdminV2Errors { using SafeERC20 for IERC20; - /// @notice Address reserved for native gas token (ETH on Ethereum and most L2s, AVAX on Avalanche, etc) + /// @notice The address reserved for the native gas token (ETH on Ethereum and most L2s, AVAX on Avalanche, etc.). address public constant NATIVE_GAS_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; - /// @notice Role identifier for Quoter API's off-chain authentication. + /// @notice The role identifier for the Quoter API's off-chain authentication. /// @dev Only addresses with this role can post FastBridge quotes to the API. bytes32 public constant QUOTER_ROLE = keccak256("QUOTER_ROLE"); - /// @notice Role identifier for Prover's on-chain authentication in FastBridge. + /// @notice The role identifier for the Prover's on-chain authentication in FastBridge. /// @dev Only addresses with this role can provide proofs that a FastBridge request has been relayed. bytes32 public constant PROVER_ROLE = keccak256("PROVER_ROLE"); - /// @notice Role identifier for Guard's on-chain authentication in FastBridge. + /// @notice The role identifier for the Guard's on-chain authentication in FastBridge. /// @dev Only addresses with this role can dispute submitted relay proofs during the dispute period. bytes32 public constant GUARD_ROLE = keccak256("GUARD_ROLE"); - /// @notice Role identifier for Canceler's on-chain authentication in FastBridge. + /// @notice The role identifier for the Canceler's on-chain authentication in FastBridge. /// @dev Only addresses with this role can cancel a FastBridge transaction without the cancel delay. bytes32 public constant CANCELER_ROLE = keccak256("CANCELER_ROLE"); - /// @notice Role identifier for Governor's on-chain administrative authority. + /// @notice The role identifier for the Governor's on-chain administrative authority. /// @dev Only addresses with this role can perform administrative tasks within the contract. bytes32 public constant GOVERNOR_ROLE = keccak256("GOVERNOR_ROLE"); - /// @notice Denominator for fee rates, represents 100%. + /// @notice The denominator for fee rates, representing 100%. uint256 public constant FEE_BPS = 1e6; - /// @notice Maximum protocol fee rate: 1% on origin amount. + /// @notice The maximum protocol fee rate: 1% of the origin amount. uint256 public constant FEE_RATE_MAX = 0.01e6; - /// @notice Minimum cancel delay that can be set by the governor. + /// @notice The minimum cancel delay that can be set by the governor. uint256 public constant MIN_CANCEL_DELAY = 1 hours; - /// @notice Default cancel delay set during the contract deployment. + /// @notice The default cancel delay set during contract deployment. uint256 public constant DEFAULT_CANCEL_DELAY = 1 days; - /// @notice Protocol fee rate taken on origin amount deposited in origin chain + /// @notice The protocol fee rate taken on the origin amount deposited in the origin chain. uint256 public protocolFeeRate; - /// @notice Protocol fee amounts accumulated + /// @notice The accumulated protocol fee amounts. mapping(address => uint256) public protocolFees; - /// @notice Delay for a transaction after which it could be permisionlessly cancelled + /// @notice The delay period after which a transaction can be permissionlessly cancelled. uint256 public cancelDelay; - /// @notice This is deprecated and should not be used. + /// @notice This variable is deprecated and should not be used. /// @dev Use ZapNative V2 requests instead. uint256 public immutable chainGasAmount = 0; - constructor(address _owner) { - _grantRole(DEFAULT_ADMIN_ROLE, _owner); + constructor(address defaultAdmin) { + _grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin); _setCancelDelay(DEFAULT_CANCEL_DELAY); } - /// @notice Allows the contract governor to set the cancel delay. The cancel delay is the time after the transaction - /// deadline after which it can be permissionlessly cancelled, if it hasn't been proven by any of the Relayers. + /// @inheritdoc IAdminV2 function setCancelDelay(uint256 newCancelDelay) external onlyRole(GOVERNOR_ROLE) { _setCancelDelay(newCancelDelay); } - /// @notice Allows the contract governor to set the protocol fee rate. The protocol fee is taken from the origin - /// amount only for completed and claimed transactions. - /// @dev The protocol fee is abstracted away from the relayers, they always operate using the amounts after fees: - /// what they see as the origin amount emitted in the log is what they get credited with. + /// @inheritdoc IAdminV2 function setProtocolFeeRate(uint256 newFeeRate) external onlyRole(GOVERNOR_ROLE) { if (newFeeRate > FEE_RATE_MAX) revert FeeRateAboveMax(); uint256 oldFeeRate = protocolFeeRate; @@ -79,14 +82,15 @@ contract AdminV2 is AccessControlEnumerable, IAdminV2, IAdminV2Errors { emit FeeRateUpdated(oldFeeRate, newFeeRate); } - /// @notice Allows the contract governor to sweep the accumulated protocol fees in the contract. + /// @inheritdoc IAdminV2 function sweepProtocolFees(address token, address recipient) external onlyRole(GOVERNOR_ROLE) { + // Early exit if no accumulated fees. uint256 feeAmount = protocolFees[token]; - if (feeAmount == 0) return; // skip if no accumulated fees - + if (feeAmount == 0) return; + // Reset the accumulated fees first. protocolFees[token] = 0; emit FeesSwept(token, recipient, feeAmount); - /// Sweep the fees as the last transaction action + // Sweep the fees as the last transaction action. if (token == NATIVE_GAS_TOKEN) { Address.sendValue(payable(recipient), feeAmount); } else { @@ -94,7 +98,8 @@ contract AdminV2 is AccessControlEnumerable, IAdminV2, IAdminV2Errors { } } - /// @notice Internal function to set the cancel delay. Security checks are performed outside of this function. + /// @notice Internal logic to set the cancel delay. Security checks are performed outside of this function. + /// @dev This function is marked as private to prevent child contracts from calling it directly. function _setCancelDelay(uint256 newCancelDelay) private { if (newCancelDelay < MIN_CANCEL_DELAY) revert CancelDelayBelowMin(); uint256 oldCancelDelay = cancelDelay; diff --git a/packages/contracts-rfq/contracts/FastBridge.sol b/packages/contracts-rfq/contracts/FastBridge.sol index 68966aa0e0..2386f8500f 100644 --- a/packages/contracts-rfq/contracts/FastBridge.sol +++ b/packages/contracts-rfq/contracts/FastBridge.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.20; -import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "./libs/Errors.sol"; import {UniversalTokenLib} from "./libs/UniversalToken.sol"; diff --git a/packages/contracts-rfq/contracts/FastBridgeV2.sol b/packages/contracts-rfq/contracts/FastBridgeV2.sol index a5aa64851a..f5e9cbd363 100644 --- a/packages/contracts-rfq/contracts/FastBridgeV2.sol +++ b/packages/contracts-rfq/contracts/FastBridgeV2.sol @@ -1,53 +1,71 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.24; -import {BridgeTransactionV2Lib} from "./libs/BridgeTransactionV2.sol"; +// ════════════════════════════════════════════════ INTERFACES ═════════════════════════════════════════════════════ -import {AdminV2} from "./AdminV2.sol"; import {IFastBridge} from "./interfaces/IFastBridge.sol"; import {IFastBridgeV2} from "./interfaces/IFastBridgeV2.sol"; import {IFastBridgeV2Errors} from "./interfaces/IFastBridgeV2Errors.sol"; import {IZapRecipient} from "./interfaces/IZapRecipient.sol"; +// ═════════════════════════════════════════════ INTERNAL IMPORTS ══════════════════════════════════════════════════ + +import {AdminV2} from "./AdminV2.sol"; +import {BridgeTransactionV2Lib} from "./libs/BridgeTransactionV2.sol"; import {MulticallTarget} from "./utils/MulticallTarget.sol"; -import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +// ═════════════════════════════════════════════ EXTERNAL IMPORTS ══════════════════════════════════════════════════ + +import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; -/// @notice FastBridgeV2 is a contract for bridging tokens across chains. +/// @title FastBridgeV2 +/// @notice Core component of the SynapseRFQ protocol, enabling Relayers (Solvers) to fulfill bridge requests. +/// Supports ERC20 and native gas tokens, along with the Zap feature for executing actions on the destination chain. +/// Users interact with the off-chain Quoter API to obtain a current quote for a bridge transaction. +/// They then submit the bridge request with the quote to this contract, depositing their assets in escrow. +/// Relayers can fulfill requests by relaying them to the destination chain and must prove fulfillment to claim funds. +/// Guards monitor proofs and can dispute discrepancies. +/// Users can reclaim funds by cancelling their requests if it has not been fulfilled within the specified deadline. contract FastBridgeV2 is AdminV2, MulticallTarget, IFastBridgeV2, IFastBridgeV2Errors { using BridgeTransactionV2Lib for bytes; using SafeERC20 for IERC20; - /// @notice Dispute period for relayed transactions + /// @notice The duration of the dispute period for relayed transactions. uint256 public constant DISPUTE_PERIOD = 30 minutes; - /// @notice Minimum deadline period to relay a requested bridge transaction + /// @notice The minimum required time between transaction request and deadline. uint256 public constant MIN_DEADLINE_PERIOD = 30 minutes; - /// @notice Maximum length of accepted zapData + /// @notice The maximum allowed length for zapData. uint256 public constant MAX_ZAP_DATA_LENGTH = 2 ** 16 - 1; - /// @notice Status of the bridge tx on origin chain + /// @notice Maps transaction IDs to bridge details (status, destination chain ID, proof timestamp, and relayer). + /// Note: this is only stored for transactions having local chain as the origin chain. mapping(bytes32 => BridgeTxDetails) public bridgeTxDetails; - /// @notice Relay details on destination chain + /// @notice Maps transaction IDs to relay details (block number, block timestamp, and relayer). + /// Note: this is only stored for transactions having local chain as the destination chain. mapping(bytes32 => BridgeRelay) public bridgeRelayDetails; - /// @notice Unique bridge nonces tracked per originSender + /// @notice Maps sender addresses to their unique bridge nonce. mapping(address => uint256) public senderNonces; - /// @notice This is deprecated and should not be used. - /// @dev Replaced by senderNonces + /// @notice This variable is deprecated and should not be used. + /// @dev Replaced by senderNonces. uint256 public immutable nonce = 0; - /// @notice the block the contract was deployed at + /// @notice The block number at which this contract was deployed. uint256 public immutable deployBlock; - constructor(address _owner) AdminV2(_owner) { + /// @notice Initializes the FastBridgeV2 contract with the provided default admin, + /// sets the default cancel delay, and records the deploy block number. + constructor(address defaultAdmin) AdminV2(defaultAdmin) { deployBlock = block.number; } + // ══════════════════════════════════════ EXTERNAL MUTABLE (USER FACING) ═══════════════════════════════════════════ + /// @inheritdoc IFastBridge function bridge(BridgeParams memory params) external payable { - bridge({ + bridgeV2({ params: params, paramsV2: BridgeParamsV2({ quoteRelayer: address(0), @@ -59,38 +77,49 @@ contract FastBridgeV2 is AdminV2, MulticallTarget, IFastBridgeV2, IFastBridgeV2E }); } + /// Note: this function is deprecated and will be removed in a future version. + /// @dev Replaced by `cancel`. + /// @inheritdoc IFastBridge + function refund(bytes calldata request) external { + cancelV2(request); + } + + // ══════════════════════════════════════ EXTERNAL MUTABLE (AGENT FACING) ══════════════════════════════════════════ + /// @inheritdoc IFastBridge function relay(bytes calldata request) external payable { - // relay override will validate the request - relay({request: request, relayer: msg.sender}); + // `relay` override will validate the request. + relayV2({request: request, relayer: msg.sender}); } /// @inheritdoc IFastBridge function prove(bytes calldata request, bytes32 destTxHash) external { request.validateV2(); - prove({transactionId: keccak256(request), destTxHash: destTxHash, relayer: msg.sender}); + proveV2({transactionId: keccak256(request), destTxHash: destTxHash, relayer: msg.sender}); } /// @inheritdoc IFastBridgeV2 - function claim(bytes calldata request) external { - // claim override will validate the request + function claimV2(bytes calldata request) external { + // `claim` override will validate the request. claim({request: request, to: address(0)}); } /// @inheritdoc IFastBridge function dispute(bytes32 transactionId) external onlyRole(GUARD_ROLE) { + // Aggregate the read operations from the same storage slot. BridgeTxDetails storage $ = bridgeTxDetails[transactionId]; - // Aggregate the read operations from the same storage slot address disputedRelayer = $.proofRelayer; BridgeStatus status = $.status; uint56 proofBlockTimestamp = $.proofBlockTimestamp; - // Can only dispute a RELAYER_PROVED transaction within the dispute period + + // Can only dispute a RELAYER_PROVED transaction within the dispute period. if (status != BridgeStatus.RELAYER_PROVED) revert StatusIncorrect(); if (_timeSince(proofBlockTimestamp) > DISPUTE_PERIOD) { revert DisputePeriodPassed(); } - // Update status to REQUESTED and delete the disputed proof details - // Note: these are storage writes + + // Update status to REQUESTED and delete the disputed proof details. + // Note: these are storage writes. $.status = BridgeStatus.REQUESTED; $.proofRelayer = address(0); $.proofBlockTimestamp = 0; @@ -98,18 +127,15 @@ contract FastBridgeV2 is AdminV2, MulticallTarget, IFastBridgeV2, IFastBridgeV2E emit BridgeProofDisputed(transactionId, disputedRelayer); } - /// Note: this function is deprecated and will be removed in a future version. - /// @inheritdoc IFastBridge - function refund(bytes calldata request) external { - cancel(request); - } + // ══════════════════════════════════════════════ EXTERNAL VIEWS ═══════════════════════════════════════════════════ /// @inheritdoc IFastBridge function canClaim(bytes32 transactionId, address relayer) external view returns (bool) { + // The correct relayer can only claim a RELAYER_PROVED transaction after the dispute period. BridgeTxDetails storage $ = bridgeTxDetails[transactionId]; - // The correct relayer can only claim a RELAYER_PROVED transaction after the dispute period if ($.status != BridgeStatus.RELAYER_PROVED) revert StatusIncorrect(); if ($.proofRelayer != relayer) revert SenderIncorrect(); + return _timeSince($.proofBlockTimestamp) > DISPUTE_PERIOD; } @@ -119,9 +145,9 @@ contract FastBridgeV2 is AdminV2, MulticallTarget, IFastBridgeV2, IFastBridgeV2E /// - `zapData` is ignored /// In order to process all kinds of requests use getBridgeTransactionV2 instead. function getBridgeTransaction(bytes calldata request) external view returns (BridgeTransaction memory) { - // Try decoding into V2 struct first. This will revert if V1 struct is passed + // 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 zapData field, as it was not present in V1 + // Note: we entirely ignore the zapData field, as it was not present in V1. return BridgeTransaction({ originChainId: txV2.originChainId, destChainId: txV2.destChainId, @@ -137,7 +163,7 @@ contract FastBridgeV2 is AdminV2, MulticallTarget, IFastBridgeV2, IFastBridgeV2E nonce: txV2.nonce }); } catch { - // Fallback to V1 struct + // Fallback to V1 struct. return abi.decode(request, (BridgeTransaction)); } } @@ -148,28 +174,31 @@ contract FastBridgeV2 is AdminV2, MulticallTarget, IFastBridgeV2, IFastBridgeV2E return BridgeTransactionV2Lib.decodeV2(request); } + // ═══════════════════════════════════════ PUBLIC MUTABLE (USER FACING) ════════════════════════════════════════════ + /// @inheritdoc IFastBridgeV2 - function bridge(BridgeParams memory params, BridgeParamsV2 memory paramsV2) public payable { + function bridgeV2(BridgeParams memory params, BridgeParamsV2 memory paramsV2) public payable { + // If relayer exclusivity is not intended for this bridge, set exclusivityEndTime to static zero. + // Otherwise, set exclusivity to expire at the current block ts offset by quoteExclusivitySeconds. int256 exclusivityEndTime = 0; - // if relayer exclusivity is not intended for this bridge, set exclusivityEndTime to static zero - // otherwise, set exclusivity to expire at the current block ts offset by quoteExclusivitySeconds if (paramsV2.quoteRelayer != address(0)) { exclusivityEndTime = int256(block.timestamp) + paramsV2.quoteExclusivitySeconds; } _validateBridgeParams(params, paramsV2, exclusivityEndTime); - // transfer tokens to bridge contract - /// @dev use returned originAmount in request in case of transfer fees + // Transfer tokens to bridge contract. We use the actual transferred amount in case of transfer fees. uint256 originAmount = _takeBridgedUserAsset(params.originToken, params.originAmount); - // track amount of origin token owed to protocol - uint256 originFeeAmount; + // Track the amount of origin token owed to protocol. + uint256 originFeeAmount = 0; if (protocolFeeRate > 0) { originFeeAmount = (originAmount * protocolFeeRate) / FEE_BPS; - originAmount -= originFeeAmount; // remove from amount used in request as not relevant for relayers + // The Relayer filling this request will be paid the originAmount after fees. + // Note: the protocol fees will be accumulated only when the Relayer claims the origin collateral. + originAmount -= originFeeAmount; } - // set status to requested + // Hash the bridge request and set the initial status to REQUESTED. bytes memory request = BridgeTransactionV2Lib.encodeV2( BridgeTransactionV2({ originChainId: uint32(block.chainid), @@ -182,16 +211,17 @@ contract FastBridgeV2 is AdminV2, MulticallTarget, IFastBridgeV2, IFastBridgeV2E destAmount: params.destAmount, originFeeAmount: originFeeAmount, deadline: params.deadline, - nonce: senderNonces[params.sender]++, // increment nonce on every bridge + // Increment the sender's nonce on every bridge. + nonce: senderNonces[params.sender]++, exclusivityRelayer: paramsV2.quoteRelayer, - // We checked exclusivityEndTime to be in range [0 .. params.deadline] above, so can safely cast + // We checked exclusivityEndTime to be in range [0 .. params.deadline] above, so can safely cast. exclusivityEndTime: uint256(exclusivityEndTime), zapNative: paramsV2.zapNative, zapData: paramsV2.zapData }) ); bytes32 transactionId = keccak256(request); - // Note: the tx status will be updated throughout the tx lifecycle, while destChainId is set once here + // Note: the tx status will be updated throughout the tx lifecycle, while destChainId is set once here. bridgeTxDetails[transactionId].status = BridgeStatus.REQUESTED; bridgeTxDetails[transactionId].destChainId = params.dstChainId; @@ -210,21 +240,62 @@ contract FastBridgeV2 is AdminV2, MulticallTarget, IFastBridgeV2, IFastBridgeV2E } /// @inheritdoc IFastBridgeV2 - function relay(bytes calldata request, address relayer) public payable { + function cancelV2(bytes calldata request) public { + // Decode the request and check that it could be cancelled. + request.validateV2(); + bytes32 transactionId = keccak256(request); + + // Can only cancel a REQUESTED transaction after its deadline expires. + BridgeTxDetails storage $ = bridgeTxDetails[transactionId]; + if ($.status != BridgeStatus.REQUESTED) revert StatusIncorrect(); + + // Permissionless cancel is only allowed after `cancelDelay` on top of the deadline. + uint256 deadline = request.deadline(); + if (!hasRole(CANCELER_ROLE, msg.sender)) deadline += cancelDelay; + if (block.timestamp <= deadline) revert DeadlineNotExceeded(); + + // Update status to REFUNDED. + // Note: this is a storage write. + $.status = BridgeStatus.REFUNDED; + + // Return the full amount (collateral + protocol fees) to the original sender. + // The protocol fees are only accumulated when the transaction is claimed, so we don't need to update them here. + address to = request.originSender(); + address token = request.originToken(); + uint256 amount = request.originAmount() + request.originFeeAmount(); + + // Emit the event before any external calls. + emit BridgeDepositRefunded(transactionId, to, token, amount); + + // Return the funds to the original sender as last transaction action. + if (token == NATIVE_GAS_TOKEN) { + Address.sendValue(payable(to), amount); + } else { + IERC20(token).safeTransfer(to, amount); + } + } + + // ═══════════════════════════════════════ PUBLIC MUTABLE (AGENT FACING) ═══════════════════════════════════════════ + + /// @inheritdoc IFastBridgeV2 + function relayV2(bytes calldata request, address relayer) public payable { + // Decode the request and check that it could be relayed. request.validateV2(); bytes32 transactionId = keccak256(request); _validateRelayParams(request, 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 trigger Zap if requested + // Mark the bridge request as relayed by saving the relayer and the block details. + bridgeRelayDetails[transactionId].blockNumber = uint48(block.number); + bridgeRelayDetails[transactionId].blockTimestamp = uint48(block.timestamp); + bridgeRelayDetails[transactionId].relayer = relayer; + + // Transfer tokens to recipient on destination chain and trigger Zap if requested. address to = request.destRecipient(); address token = request.destToken(); uint256 amount = request.destAmount(); uint256 zapNative = request.zapNative(); - // Emit the event before any external calls + // Emit the event before any external calls. emit BridgeRelayed({ transactionId: transactionId, relayer: relayer, @@ -240,13 +311,13 @@ contract FastBridgeV2 is AdminV2, MulticallTarget, IFastBridgeV2, IFastBridgeV2E // 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 (token == NATIVE_GAS_TOKEN) { - // For the native gas token, additional zapNative is not allowed + // For the native gas token, additional zapNative is not allowed. if (zapNative != 0) revert ZapNativeNotSupported(); - // Check that the correct msg.value was sent + // Check that the correct msg.value was sent. if (msg.value != amount) revert MsgValueIncorrect(); - // Don't do a native transfer yet: we will handle it alongside the Zap below + // Don't do a native transfer yet: we will handle it alongside the Zap below. } else { - // For ERC20s, we check that the correct msg.value was sent + // For ERC20s, we check that the correct msg.value was sent. if (msg.value != zapNative) revert MsgValueIncorrect(); // We need to transfer the tokens from the Relayer to the recipient first before performing an // optional post-transfer Zap. @@ -263,10 +334,9 @@ contract FastBridgeV2 is AdminV2, MulticallTarget, IFastBridgeV2, IFastBridgeV2E bytes calldata zapData = request.zapData(); if (zapData.length != 0) { // Zap Data is present: Zap has been requested by the recipient. Trigger it forwarding the full msg.value. - + _triggerZapWithChecks({recipient: to, token: token, amount: amount, zapData: zapData}); // 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. - _triggerZapWithChecks({recipient: to, token: token, amount: amount, zapData: zapData}); } else if (msg.value != 0) { // Zap Data is missing, but msg.value was sent. This could happen in two different cases: // - Relay with the native gas token is happening. @@ -277,13 +347,13 @@ contract FastBridgeV2 is AdminV2, MulticallTarget, IFastBridgeV2, IFastBridgeV2E } /// @inheritdoc IFastBridgeV2 - function prove(bytes32 transactionId, bytes32 destTxHash, address relayer) public onlyRole(PROVER_ROLE) { + function proveV2(bytes32 transactionId, bytes32 destTxHash, address relayer) public onlyRole(PROVER_ROLE) { + // Can only prove a REQUESTED transaction. BridgeTxDetails storage $ = bridgeTxDetails[transactionId]; - - // Can only prove a REQUESTED transaction if ($.status != BridgeStatus.REQUESTED) revert StatusIncorrect(); - // Update status to RELAYER_PROVED and store the proof details - // Note: these are storage writes + + // Update status to RELAYER_PROVED and store the proof details. + // Note: these are storage writes. $.status = BridgeStatus.RELAYER_PROVED; $.proofBlockTimestamp = uint56(block.timestamp); $.proofRelayer = relayer; @@ -293,69 +363,41 @@ contract FastBridgeV2 is AdminV2, MulticallTarget, IFastBridgeV2, IFastBridgeV2E /// @inheritdoc IFastBridge function claim(bytes calldata request, address to) public { + // Decode the request and check that it could be claimed. request.validateV2(); bytes32 transactionId = keccak256(request); + + // Aggregate the read operations from the same storage slot. BridgeTxDetails storage $ = bridgeTxDetails[transactionId]; - // Aggregate the read operations from the same storage slot address proofRelayer = $.proofRelayer; BridgeStatus status = $.status; uint56 proofBlockTimestamp = $.proofBlockTimestamp; - // Can only claim a RELAYER_PROVED transaction after the dispute period + // Can only claim a RELAYER_PROVED transaction after the dispute period. if (status != BridgeStatus.RELAYER_PROVED) revert StatusIncorrect(); - if (_timeSince(proofBlockTimestamp) <= DISPUTE_PERIOD) { - revert DisputePeriodNotPassed(); - } - + if (_timeSince(proofBlockTimestamp) <= DISPUTE_PERIOD) revert DisputePeriodNotPassed(); if (to == address(0)) { - // Anyone could claim the funds to the proven relayer on their behalf + // Anyone could claim the funds to the proven relayer on their behalf. to = proofRelayer; } else if (proofRelayer != msg.sender) { - // Only the proven relayer could specify an address to claim the funds to + // Only the proven relayer could specify an address to claim the funds to. revert SenderIncorrect(); } - // Update status to RELAYER_CLAIMED and transfer the origin collateral to the specified claim address - // Note: this is a storage write + // Update status to RELAYER_CLAIMED and transfer the origin collateral to the specified claim address. + // Note: this is a storage write. $.status = BridgeStatus.RELAYER_CLAIMED; + // Accumulate protocol fees if origin fee amount exists. address token = request.originToken(); uint256 amount = request.originAmount(); - // Update protocol fees if origin fee amount exists uint256 originFeeAmount = request.originFeeAmount(); if (originFeeAmount > 0) protocolFees[token] += originFeeAmount; - // Emit the event before any external calls - emit BridgeDepositClaimed(transactionId, proofRelayer, to, token, amount); - // Complete the relayer claim as the last transaction action - if (token == NATIVE_GAS_TOKEN) { - Address.sendValue(payable(to), amount); - } else { - IERC20(token).safeTransfer(to, amount); - } - } - /// @inheritdoc IFastBridgeV2 - function cancel(bytes calldata request) public { - request.validateV2(); - bytes32 transactionId = keccak256(request); - BridgeTxDetails storage $ = bridgeTxDetails[transactionId]; - // Can only cancel a REQUESTED transaction after its deadline expires - if ($.status != BridgeStatus.REQUESTED) revert StatusIncorrect(); - uint256 deadline = request.deadline(); - // Permissionless cancel is only allowed after `cancelDelay` on top of the deadline - if (!hasRole(CANCELER_ROLE, msg.sender)) deadline += cancelDelay; - if (block.timestamp <= deadline) revert DeadlineNotExceeded(); - // Update status to REFUNDED and return the full amount (collateral + protocol fees) to the original sender. - // The protocol fees are only updated when the transaction is claimed, so we don't need to update them here. - // Note: this is a storage write - $.status = BridgeStatus.REFUNDED; + // Emit the event before any external calls. + emit BridgeDepositClaimed(transactionId, proofRelayer, to, token, amount); - address to = request.originSender(); - address token = request.originToken(); - uint256 amount = request.originAmount() + request.originFeeAmount(); - // Emit the event before any external calls - emit BridgeDepositRefunded(transactionId, to, token, amount); - // Complete the user cancel as the last transaction action + // Complete the relayer claim as the last transaction action. if (token == NATIVE_GAS_TOKEN) { Address.sendValue(payable(to), amount); } else { @@ -363,6 +405,8 @@ contract FastBridgeV2 is AdminV2, MulticallTarget, IFastBridgeV2, IFastBridgeV2E } } + // ═══════════════════════════════════════════════ PUBLIC VIEWS ════════════════════════════════════════════════════ + /// @inheritdoc IFastBridgeV2 function bridgeStatuses(bytes32 transactionId) public view returns (BridgeStatus status) { return bridgeTxDetails[transactionId].status; @@ -371,20 +415,21 @@ contract FastBridgeV2 is AdminV2, MulticallTarget, IFastBridgeV2, IFastBridgeV2E /// @inheritdoc IFastBridgeV2 function bridgeProofs(bytes32 transactionId) public view returns (uint96 timestamp, address relayer) { BridgeTxDetails storage $ = bridgeTxDetails[transactionId]; - timestamp = $.proofBlockTimestamp; relayer = $.proofRelayer; } /// @inheritdoc IFastBridgeV2 function bridgeRelays(bytes32 transactionId) public view returns (bool) { - // has this transactionId been relayed? + // This transaction has been relayed if the relayer address is recorded. return bridgeRelayDetails[transactionId].relayer != address(0); } - /// @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 transferred back to the user - /// via the cancel function should no one complete the relay. + // ═════════════════════════════════════════════ INTERNAL METHODS ══════════════════════════════════════════════════ + + /// @notice Takes the bridged asset from the user into FastBridgeV2 custody. The asset will later be + /// claimed by the relayer who completed the relay on the destination chain, or returned to the user + /// via the cancel function if no relay is completed. function _takeBridgedUserAsset(address token, uint256 amount) internal returns (uint256 amountTaken) { if (token == NATIVE_GAS_TOKEN) { // For the native gas token, we just need to check that the supplied msg.value is correct. @@ -395,50 +440,52 @@ contract FastBridgeV2 is AdminV2, MulticallTarget, IFastBridgeV2, IFastBridgeV2E // 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. if (msg.value != 0) revert MsgValueIncorrect(); - // Throw an explicit error if the provided token address is not a contract + // Throw an explicit error if the provided token address is not a contract. if (token.code.length == 0) revert TokenNotContract(); + + // Use the balance difference as the amount taken in case of fee on transfer tokens. 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; } } - /// @notice Calls the Recipient's hook function with the specified zapData and performs - /// all the necessary checks for the returned value. + /// @notice Calls the recipient's hook function with the specified zapData and validates + /// the returned value. function _triggerZapWithChecks(address recipient, address token, uint256 amount, bytes calldata zapData) internal { - // This will bubble any revert messages from the hook function + // Call the recipient's hook function with the specified zapData, bubbling any revert messages. bytes memory returnData = Address.functionCallWithValue({ target: recipient, data: abi.encodeCall(IZapRecipient.zap, (token, amount, zapData)), - // Note: see `relay()` for reasoning behind passing msg.value + // Note: see `relay()` for reasoning behind passing msg.value. value: msg.value }); - // Explicit revert if no return data at all + + // Explicit revert if no return data at all. if (returnData.length == 0) revert RecipientNoReturnValue(); - // Check that exactly a single return value was returned + // Check that exactly a single return value was returned. if (returnData.length != 32) revert RecipientIncorrectReturnValue(); - // Return value should be abi-encoded hook function selector + // Return value should be abi-encoded hook function selector. if (bytes32(returnData) != bytes32(IZapRecipient.zap.selector)) { revert RecipientIncorrectReturnValue(); } } - /// @notice Calculates time since proof submitted - /// @dev proof.timestamp stores casted uint56(block.timestamp) block timestamps for gas optimization - /// _timeSince(proof) can accomodate rollover case when block.timestamp > type(uint56).max but - /// proof.timestamp < type(uint56).max via unchecked statement - /// @param proofBlockTimestamp The bridge proof block timestamp - /// @return delta Time delta since proof submitted + /// @notice Calculates the time elapsed since a proof was submitted. + /// @dev The proof.timestamp stores block timestamps as uint56 for gas optimization. + /// _timeSince(proof) handles timestamp rollover when block.timestamp > type(uint56).max but + /// proof.timestamp < type(uint56).max via an unchecked statement. + /// @param proofBlockTimestamp The block timestamp when the proof was submitted. + /// @return delta The time elapsed since proof submission. function _timeSince(uint56 proofBlockTimestamp) internal view returns (uint256 delta) { unchecked { delta = uint56(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. + /// @notice Validates all parameters required for a bridge transaction. + /// @dev This function's complexity cannot be reduced due to the number of required checks, + /// so we disable the code-complexity rule. // solhint-disable-next-line code-complexity function _validateBridgeParams( BridgeParams memory params, @@ -448,32 +495,34 @@ contract FastBridgeV2 is AdminV2, MulticallTarget, IFastBridgeV2, IFastBridgeV2E internal view { - // Check V1 (legacy) params + // 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 + + // Check V2 params. if (paramsV2.zapData.length > MAX_ZAP_DATA_LENGTH) revert ZapDataLengthAboveMax(); if (paramsV2.zapNative != 0 && params.destToken == NATIVE_GAS_TOKEN) { revert ZapNativeNotSupported(); } - // exclusivityEndTime must be in range [0 .. params.deadline] + + // 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. + /// @notice Validates all parameters required for a relay transaction. function _validateRelayParams(bytes calldata request, bytes32 transactionId, address relayer) internal view { if (relayer == address(0)) revert ZeroAddress(); - // Check if the transaction has already been relayed + // Check that the transaction has not been relayed yet and is for the current chain. if (bridgeRelays(transactionId)) revert TransactionRelayed(); if (request.destChainId() != block.chainid) revert ChainIncorrect(); - // Check the deadline for relay to happen + // Check that the deadline for relay to happen has not passed yet. if (block.timestamp > request.deadline()) revert DeadlineExceeded(); - // Check the exclusivity period, if it is still ongoing + // Check the exclusivity period, if it was specified and is still ongoing. address exclRelayer = request.exclusivityRelayer(); if (exclRelayer != address(0) && exclRelayer != relayer && block.timestamp <= request.exclusivityEndTime()) { revert ExclusivityPeriodNotPassed(); diff --git a/packages/contracts-rfq/contracts/interfaces/IAdminV2.sol b/packages/contracts-rfq/contracts/interfaces/IAdminV2.sol index 1a30434161..90115d2f49 100644 --- a/packages/contracts-rfq/contracts/interfaces/IAdminV2.sol +++ b/packages/contracts-rfq/contracts/interfaces/IAdminV2.sol @@ -6,9 +6,16 @@ interface IAdminV2 { event FeeRateUpdated(uint256 oldFeeRate, uint256 newFeeRate); event FeesSwept(address token, address recipient, uint256 amount); + /// @notice Allows the governor to set the cancel delay. The cancel delay is the time period after the transaction + /// deadline during which a transaction can be permissionlessly cancelled if it hasn't been proven by any Relayer. function setCancelDelay(uint256 newCancelDelay) external; + /// @notice Allows the governor to set the protocol fee rate. The protocol fee is taken from the origin + /// amount and is only applied to completed and claimed transactions. + /// @dev The protocol fee is abstracted away from the relayers; they always operate using the amounts after fees. + /// The origin amount they see in the emitted log is what they get credited with. function setProtocolFeeRate(uint256 newFeeRate) external; + /// @notice Allows the governor to withdraw the accumulated protocol fees from the contract. function sweepProtocolFees(address token, address recipient) external; } diff --git a/packages/contracts-rfq/contracts/interfaces/IFastBridgeV2.sol b/packages/contracts-rfq/contracts/interfaces/IFastBridgeV2.sol index 258369b191..141b3808a7 100644 --- a/packages/contracts-rfq/contracts/interfaces/IFastBridgeV2.sol +++ b/packages/contracts-rfq/contracts/interfaces/IFastBridgeV2.sol @@ -5,7 +5,7 @@ import {IFastBridge} from "./IFastBridge.sol"; interface IFastBridgeV2 is IFastBridge { enum BridgeStatus { - NULL, // doesn't exist yet + NULL, // Doesn't exist yet. REQUESTED, RELAYER_PROVED, RELAYER_CLAIMED, @@ -26,8 +26,7 @@ interface IFastBridgeV2 is IFastBridge { } /// @notice New params introduced in the FastBridgeV2. - /// We are passing fields from the older BridgeParams struct outside of this struct - /// for backwards compatibility. + /// We are passing fields from the older BridgeParams struct outside of this struct 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: zapNative > 0 can NOT be used with destToken = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE (native token) @@ -71,28 +70,28 @@ interface IFastBridgeV2 is IFastBridge { /// 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, can be left empty) - function bridge(BridgeParams memory params, BridgeParamsV2 memory paramsV2) external payable; + function bridgeV2(BridgeParams memory params, BridgeParamsV2 memory paramsV2) external payable; /// @notice Relays destination side of bridge transaction by off-chain relayer /// @param request The encoded bridge transaction to relay on destination chain /// @param relayer The address of the relaying entity which should have control of the origin funds when claimed - function relay(bytes memory request, address relayer) external payable; + function relayV2(bytes memory request, address relayer) external payable; /// @notice Provides proof on origin side that relayer provided funds on destination side of bridge transaction /// @param transactionId The transaction id associated with the encoded bridge transaction to prove /// @param destTxHash The destination tx hash proving bridge transaction was relayed /// @param relayer The address of the relaying entity which should have control of the origin funds when claimed - function prove(bytes32 transactionId, bytes32 destTxHash, address relayer) external; + function proveV2(bytes32 transactionId, bytes32 destTxHash, address relayer) external; /// @notice Completes bridge transaction on origin chain by claiming originally deposited capital. /// @notice Can only send funds to the relayer address on the proof. /// @param request The encoded bridge transaction to claim on origin chain - function claim(bytes memory request) external; + function claimV2(bytes memory request) external; /// @notice Cancels an outstanding bridge transaction in case optimistic bridging failed and returns the full amount /// to the original sender. /// @param request The encoded bridge transaction to refund - function cancel(bytes memory request) external; + function cancelV2(bytes memory request) external; /// @notice Checks if a transaction has been relayed /// @param transactionId The ID of the transaction to check diff --git a/packages/contracts-rfq/contracts/interfaces/IMulticallTarget.sol b/packages/contracts-rfq/contracts/interfaces/IMulticallTarget.sol index 8902983c72..f162e0d160 100644 --- a/packages/contracts-rfq/contracts/interfaces/IMulticallTarget.sol +++ b/packages/contracts-rfq/contracts/interfaces/IMulticallTarget.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.4; -/// @notice Interface for a contract that can be called multiple times by the same caller. Inspired by MulticallV3: +/// @notice Interface for a contract that supports multiple calls from the same caller. Inspired by MulticallV3: /// https://github.com/mds1/multicall/blob/master/src/Multicall3.sol interface IMulticallTarget { struct Result { @@ -9,7 +9,23 @@ interface IMulticallTarget { bytes returnData; } + /// @notice Executes multiple calls to this contract in a single transaction while preserving msg.sender. + /// Return data from the calls is discarded. + /// @dev This method is non-payable, so only calls with msg.value of 0 can be batched. + /// If ignoreReverts is set to true, reverted calls will be skipped. + /// Otherwise, the entire batch will revert with the original revert reason. + /// @param data List of ABI-encoded calldata for the calls to execute + /// @param ignoreReverts Whether to skip calls that revert function multicallNoResults(bytes[] calldata data, bool ignoreReverts) external; + + /// @notice Executes multiple calls to this contract in a single transaction while preserving msg.sender. + /// Return data from each call is preserved. + /// @dev This method is non-payable, so only calls with msg.value of 0 can be batched. + /// If ignoreReverts is set to true, reverted calls will be skipped. + /// Otherwise, the entire batch will revert with the original revert reason. + /// @param data List of ABI-encoded calldata for the calls to execute + /// @param ignoreReverts Whether to skip calls that revert + /// @return results List of results from the calls, each containing (success, returnData) function multicallWithResults( bytes[] calldata data, bool ignoreReverts diff --git a/packages/contracts-rfq/contracts/interfaces/IZapRecipient.sol b/packages/contracts-rfq/contracts/interfaces/IZapRecipient.sol index 8746a9c16e..2300854ce4 100644 --- a/packages/contracts-rfq/contracts/interfaces/IZapRecipient.sol +++ b/packages/contracts-rfq/contracts/interfaces/IZapRecipient.sol @@ -1,6 +1,15 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.4; +/// @notice Interface for contracts that can perform Zap operations. Such contracts could be used as Recipients +/// in a FastBridge transaction that includes a Zap operation. The Zap Data should include instructions on how +/// exactly the Zap operation should be executed, which would typically include the target address and calldata +/// to use. The exact implementation of the Zap Data encoding is up to the Recipient contract. interface IZapRecipient { + /// @notice Performs a Zap operation with the given token and amount according to the provided Zap data. + /// @param token The address of the token being used for the Zap operation. + /// @param amount The amount of tokens to be used. + /// @param zapData The encoded data specifying how the Zap operation should be executed. + /// @return The function selector to indicate successful execution. function zap(address token, uint256 amount, bytes memory zapData) external payable returns (bytes4); } diff --git a/packages/contracts-rfq/contracts/libs/BridgeTransactionV2.sol b/packages/contracts-rfq/contracts/libs/BridgeTransactionV2.sol index 2bdca5ef8b..dc16cb8b57 100644 --- a/packages/contracts-rfq/contracts/libs/BridgeTransactionV2.sol +++ b/packages/contracts-rfq/contracts/libs/BridgeTransactionV2.sol @@ -46,7 +46,7 @@ library BridgeTransactionV2Lib { error BridgeTransactionV2__InvalidEncodedTx(); error BridgeTransactionV2__UnsupportedVersion(uint16 version); - /// @notice Validates the encoded transaction to be a tightly packed encoded payload for BridgeTransactionV2. + /// @notice Validates that the encoded transaction is a tightly packed encoded payload for BridgeTransactionV2. /// @dev Checks the minimum length and the version, use this function before decoding any of the fields. function validateV2(bytes calldata encodedTx) internal pure { // Check the minimum length: must at least include all static fields. diff --git a/packages/contracts-rfq/contracts/libs/UniversalToken.sol b/packages/contracts-rfq/contracts/libs/UniversalToken.sol index c57bf141e0..935cfb1cd2 100644 --- a/packages/contracts-rfq/contracts/libs/UniversalToken.sol +++ b/packages/contracts-rfq/contracts/libs/UniversalToken.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.20; import {TokenNotContract} from "./Errors.sol"; -import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; library UniversalTokenLib { using SafeERC20 for IERC20; diff --git a/packages/contracts-rfq/contracts/libs/ZapDataV1.sol b/packages/contracts-rfq/contracts/libs/ZapDataV1.sol new file mode 100644 index 0000000000..0b7c13a9d1 --- /dev/null +++ b/packages/contracts-rfq/contracts/libs/ZapDataV1.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +// solhint-disable no-inline-assembly +library ZapDataV1 { + /// @notice Version of the Zap Data struct. + uint16 internal constant VERSION = 1; + + /// @notice Value that indicates the amount is not present in the target function's payload. + uint16 internal constant AMOUNT_NOT_PRESENT = 0xFFFF; + + // Offsets of the fields in the packed ZapData struct + // uint16 version [000 .. 002) + // uint16 amountPosition [002 .. 004) + // address target [004 .. 024) + // bytes payload [024 .. ***) + + // forgefmt: disable-start + uint256 private constant OFFSET_AMOUNT_POSITION = 2; + uint256 private constant OFFSET_TARGET = 4; + uint256 private constant OFFSET_PAYLOAD = 24; + // forgefmt: disable-end + + error ZapDataV1__InvalidEncoding(); + error ZapDataV1__TargetZeroAddress(); + error ZapDataV1__UnsupportedVersion(uint16 version); + + /// @notice Validates that encodedZapData is a tightly packed encoded payload for ZapData struct. + /// @dev Checks that all the required fields are present and the version is correct. + function validateV1(bytes calldata encodedZapData) internal pure { + // Check the minimum length: must at least include all static fields. + if (encodedZapData.length < OFFSET_PAYLOAD) revert ZapDataV1__InvalidEncoding(); + // Once we validated the length, we can be sure that the version field is present. + uint16 version_ = version(encodedZapData); + if (version_ != VERSION) revert ZapDataV1__UnsupportedVersion(version_); + } + + /// @notice Encodes the ZapData struct by tightly packing the fields. + /// Note: we don't know the exact amount of tokens that will be used for the Zap at the time of encoding, + /// so we provide the reference index where the token amount is encoded within `payload_`. This allows us to + /// hot-swap the token amount in the payload, when the Zap is performed. + /// @dev `abi.decode` will not work as a result of the tightly packed fields. Use `decodeZapData` instead. + /// @param amountPosition_ Position (start index) where the token amount is encoded within `payload_`. + /// This will usually be `4 + 32 * n`, where `n` is the position of the token amount in + /// the list of parameters of the target function (starting from 0). + /// Or `AMOUNT_NOT_PRESENT` if the token amount is not encoded within `payload_`. + /// @param target_ Address of the target contract. + /// @param payload_ ABI-encoded calldata to be used for the `target_` contract call. + /// If the target function has the token amount as an argument, any placeholder amount value + /// can be used for the original ABI encoding of `payload_`. The placeholder amount will + /// be replaced with the actual amount, when the Zap Data is decoded. + function encodeV1( + uint16 amountPosition_, + address target_, + bytes memory payload_ + ) + internal + pure + returns (bytes memory encodedZapData) + { + if (target_ == address(0)) revert ZapDataV1__TargetZeroAddress(); + // Amount is encoded in [amountPosition_ .. amountPosition_ + 32), which should be within the payload. + if (amountPosition_ != AMOUNT_NOT_PRESENT && (uint256(amountPosition_) + 32 > payload_.length)) { + revert ZapDataV1__InvalidEncoding(); + } + return abi.encodePacked(VERSION, amountPosition_, target_, payload_); + } + + /// @notice Extracts the version from the encoded Zap Data. + function version(bytes calldata encodedZapData) internal pure returns (uint16 version_) { + // Load 32 bytes from the start and shift it 240 bits to the right to get the highest 16 bits. + assembly { + version_ := shr(240, calldataload(encodedZapData.offset)) + } + } + + /// @notice Extracts the target address from the encoded Zap Data. + function target(bytes calldata encodedZapData) internal pure returns (address target_) { + // Load 32 bytes from the offset and shift it 96 bits to the right to get the highest 160 bits. + assembly { + target_ := shr(96, calldataload(add(encodedZapData.offset, OFFSET_TARGET))) + } + } + + /// @notice Extracts the payload from the encoded Zap Data. Replaces the token amount with the provided value, + /// if it was present in the original data (if amountPosition is not AMOUNT_NOT_PRESENT). + /// @dev This payload will be used as a calldata for the target contract. + function payload(bytes calldata encodedZapData, uint256 amount) internal pure returns (bytes memory) { + // The original payload is located at encodedZapData[OFFSET_PAYLOAD:]. + uint16 amountPosition = _amountPosition(encodedZapData); + // If the amount was not present in the original payload, return the payload as is. + if (amountPosition == AMOUNT_NOT_PRESENT) { + return encodedZapData[OFFSET_PAYLOAD:]; + } + // Calculate the start and end indexes of the amount in ZapData from its position within the payload. + // Note: we use inclusive start and exclusive end indexes for easier slicing of the ZapData. + uint256 amountStartIndexIncl = OFFSET_PAYLOAD + amountPosition; + uint256 amountEndIndexExcl = amountStartIndexIncl + 32; + // Check that the amount is within the ZapData. + if (amountEndIndexExcl > encodedZapData.length) revert ZapDataV1__InvalidEncoding(); + // Otherwise we need to replace the amount in the payload with the provided value. + return abi.encodePacked( + // Copy the original payload up to the amount + encodedZapData[OFFSET_PAYLOAD:amountStartIndexIncl], + // Replace the originally encoded amount with the provided value + amount, + // Copy the rest of the payload after the amount + encodedZapData[amountEndIndexExcl:] + ); + } + + /// @notice Extracts the amount position from the encoded Zap Data. + function _amountPosition(bytes calldata encodedZapData) private pure returns (uint16 amountPosition) { + // Load 32 bytes from the offset and shift it 240 bits to the right to get the highest 16 bits. + assembly { + amountPosition := shr(240, calldataload(add(encodedZapData.offset, OFFSET_AMOUNT_POSITION))) + } + } +} diff --git a/packages/contracts-rfq/contracts/utils/MulticallTarget.sol b/packages/contracts-rfq/contracts/utils/MulticallTarget.sol index 51493c10e2..6d7bbbd1c2 100644 --- a/packages/contracts-rfq/contracts/utils/MulticallTarget.sol +++ b/packages/contracts-rfq/contracts/utils/MulticallTarget.sol @@ -4,18 +4,12 @@ pragma solidity ^0.8.4; import {IMulticallTarget} from "../interfaces/IMulticallTarget.sol"; // solhint-disable avoid-low-level-calls -/// @notice Template for a contract that supports batched calls (preserving the msg.sender). -/// Only calls with zero msg.value could be batched. +/// @notice Abstract contract template that supports batched calls while preserving msg.sender. +/// Only calls with msg.value of 0 can be batched. abstract contract MulticallTarget is IMulticallTarget { error MulticallTarget__UndeterminedRevert(); - /// @notice Perform a batched call to this contract, preserving the msg.sender. - /// The return data from each call is discarded. - /// @dev The method is non-payable, so only calls with `msg.value == 0` could be batched. - /// It's possible to ignore the reverts from the calls by setting the `ignoreReverts` flag. - /// Otherwise, the whole batch call will be reverted with the original revert reason. - /// @param data List of abi-encoded calldata for the calls to perform. - /// @param ignoreReverts Whether to ignore the revert errors from the calls. + /// @inheritdoc IMulticallTarget function multicallNoResults(bytes[] calldata data, bool ignoreReverts) external { for (uint256 i = 0; i < data.length; ++i) { // We perform a delegate call to ourself to preserve the msg.sender. This is identical to `msg.sender` @@ -29,14 +23,7 @@ abstract contract MulticallTarget is IMulticallTarget { } } - /// @notice Perform a batched call to this contract, preserving the msg.sender. - /// The return data from each call is preserved. - /// @dev The method is non-payable, so only calls with `msg.value == 0` could be batched. - /// It's possible to ignore the reverts from the calls by setting the `ignoreReverts` flag. - /// Otherwise, the whole batch call will be reverted with the original revert reason. - /// @param data List of abi-encoded calldata for the calls to perform. - /// @param ignoreReverts Whether to ignore the revert errors from the calls. - /// @return results List of results from the calls: `(success, returnData)`. + /// @inheritdoc IMulticallTarget function multicallWithResults( bytes[] calldata data, bool ignoreReverts diff --git a/packages/contracts-rfq/contracts/zaps/TokenZapV1.sol b/packages/contracts-rfq/contracts/zaps/TokenZapV1.sol new file mode 100644 index 0000000000..bd58d8f391 --- /dev/null +++ b/packages/contracts-rfq/contracts/zaps/TokenZapV1.sol @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +// ════════════════════════════════════════════════ INTERFACES ═════════════════════════════════════════════════════ + +import {IZapRecipient} from "../interfaces/IZapRecipient.sol"; + +// ═════════════════════════════════════════════ INTERNAL IMPORTS ══════════════════════════════════════════════════ + +import {ZapDataV1} from "../libs/ZapDataV1.sol"; + +// ═════════════════════════════════════════════ EXTERNAL IMPORTS ══════════════════════════════════════════════════ + +import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +/// @title TokenZapV1 +/// @notice Facilitates atomic token operations known as "Zaps", allowing the execution of predefined actions +/// on behalf of users, such as deposits or swaps. Supports ERC20 tokens and native gas tokens (e.g., ETH). +/// @dev Tokens must be transferred to the contract before execution, native tokens could be provided as `msg.value`. +/// This contract is stateless and does not hold assets between Zaps; leftover tokens can be claimed by anyone. +/// Ensure that Zaps fully utilize tokens or revert to prevent the loss of funds. +contract TokenZapV1 is IZapRecipient { + using SafeERC20 for IERC20; + using ZapDataV1 for bytes; + + address public constant NATIVE_GAS_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + error TokenZapV1__PayloadLengthAboveMax(); + error TokenZapV1__TargetZeroAddress(); + + /// @notice Allows the contract to receive ETH. + /// @dev Leftover ETH can be claimed by anyone. Ensure the full balance is spent during Zaps. + receive() external payable {} + + /// @notice Performs a Zap action using the specified token and amount. This amount must have previously been + /// transferred to this contract (could also be supplied as msg.value if the token is a native gas token). + /// Zap action will be performed forwarding full `msg.value` for ERC20s or `amount` for native gas tokens. + /// Note: all funds remaining after the Zap action is performed can be claimed by anyone. + /// Make sure to spend the full balance during the Zaps and avoid sending extra funds if a single Zap is performed. + /// @dev The provided ZapData contains the target address and calldata for the Zap action, and must be + /// encoded using the encodeZapData function. Native gas token transfers could be done by using empty `payload`, + /// this is the only case where target could be an EOA. + /// @param token Address of the token to be used for the Zap action. + /// @param amount Amount of the token to be used for the Zap action. + /// @param zapData Encoded Zap Data containing the target address and calldata for the Zap action. + /// @return selector Selector of this function to signal the caller about the success of the Zap action. + function zap(address token, uint256 amount, bytes calldata zapData) external payable returns (bytes4) { + // Validate the ZapData format and extract the target address. + zapData.validateV1(); + address target = zapData.target(); + if (target == address(0)) revert TokenZapV1__TargetZeroAddress(); + // Note: we don't check the amount that was transferred to TokenZapV1 (or msg.value for native gas tokens). + // Transferring more than `amount` will lead to remaining funds in TokenZapV1, which can be claimed by anyone. + // Ensure that you send the exact amount for a single Zap or spend the full balance for multiple `zap()` calls. + uint256 msgValue = msg.value; + if (token == NATIVE_GAS_TOKEN) { + // For native gas tokens, we forward the requested amount to the target contract during the Zap action. + // Similar to ERC20s, we allow using pre-transferred native tokens for the Zap. + msgValue = amount; + // No approval is needed since native tokens don't use allowances. + // Note: balance check is performed within `Address.sendValue` or `Address.functionCallWithValue` below. + } else { + // For ERC20 tokens, grant unlimited approval to the target if the current allowance is insufficient. + // This is safe since the contract doesn't custody tokens between zaps. + if (IERC20(token).allowance(address(this), target) < amount) { + IERC20(token).forceApprove(target, type(uint256).max); + } + // Note: balance check is omitted as the target contract will revert if there are insufficient funds. + } + // Construct the payload for the target contract call with the Zap action. + // The payload is modified to replace the placeholder amount with the actual amount. + bytes memory payload = zapData.payload(amount); + if (payload.length == 0 && token == NATIVE_GAS_TOKEN) { + // Zap Action in a form of native gas token transfer to the target is requested. + // Note: we avoid using `functionCallWithValue` because the target might be an EOA. This will + // revert with a generic custom error should the target contract revert on incoming transfer. + Address.sendValue({recipient: payable(target), amount: msgValue}); + } else { + // Perform the Zap action, forwarding the requested native value to the target contract. + // Note: this will bubble up any revert from the target contract, and revert if target is EOA. + Address.functionCallWithValue({target: target, data: payload, value: msgValue}); + } + // Return function selector to indicate successful execution + return this.zap.selector; + } + + /// @notice Encodes the ZapData for a Zap action. + /// @dev At the time of encoding, we don't know the exact amount of tokens that will be used for the Zap, + /// as we don't have a quote for performing a Zap. Therefore, a placeholder value for the amount must be used + /// when ABI-encoding the payload. A reference index where the actual amount is encoded within the payload + /// must be provided in order to replace the placeholder with the actual amount when the Zap is performed. + /// @param target Address of the target contract. + /// @param payload ABI-encoded calldata to be used for the `target` contract call. + /// If the target function has the token amount as an argument, any placeholder amount value + /// can be used for the original ABI encoding of `payload`. The placeholder amount will + /// be replaced with the actual amount when the Zap Data is decoded. + /// @param amountPosition Position (start index) where the token amount is encoded within `payload`. + /// This will usually be `4 + 32 * n`, where `n` is the position of the token amount in + /// the list of parameters of the target function (starting from 0). + /// Any value greater than or equal to `payload.length` can be used if the token amount is + /// not an argument of the target function. + function encodeZapData( + address target, + bytes memory payload, + uint256 amountPosition + ) + external + pure + returns (bytes memory) + { + if (payload.length > ZapDataV1.AMOUNT_NOT_PRESENT) { + revert TokenZapV1__PayloadLengthAboveMax(); + } + // External integrations do not need to understand the specific `AMOUNT_NOT_PRESENT` semantics. + // Therefore, they can specify any value greater than or equal to `payload.length` to indicate + // that the amount is not present in the payload. + if (amountPosition >= payload.length) { + amountPosition = ZapDataV1.AMOUNT_NOT_PRESENT; + } + // At this point, we have checked that both `amountPosition` and `payload.length` fit in uint16. + return ZapDataV1.encodeV1(uint16(amountPosition), target, payload); + } + + /// @notice Decodes the ZapData for a Zap action. Replaces the placeholder amount with the actual amount, + /// if it was present in the original `payload`. Otherwise, returns the original `payload` as is. + /// @param zapData Encoded Zap Data containing the target address and calldata for the Zap action. + /// @param amount Actual amount of the token to be used for the Zap action. + function decodeZapData( + bytes calldata zapData, + uint256 amount + ) + public + pure + returns (address target, bytes memory payload) + { + zapData.validateV1(); + target = zapData.target(); + payload = zapData.payload(amount); + } +} diff --git a/packages/contracts-rfq/foundry.toml b/packages/contracts-rfq/foundry.toml index 5bca86282a..48f7927e13 100644 --- a/packages/contracts-rfq/foundry.toml +++ b/packages/contracts-rfq/foundry.toml @@ -2,6 +2,7 @@ # 2024-01-01 block_timestamp = 1_704_067_200 evm_version = "paris" +optimizer_runs = 1_000_000 src = 'contracts' out = 'out' libs = ["lib", "node_modules"] @@ -16,6 +17,7 @@ ignore = ["contracts/legacy/**/*.sol"] line_length = 120 multiline_func_header = 'all' number_underscore = 'thousands' +sort_imports = true [rpc_endpoints] arbitrum = "${ARBITRUM_RPC}" diff --git a/packages/contracts-rfq/package.json b/packages/contracts-rfq/package.json index 9e6300ca22..17fe284464 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.13.0", + "version": "0.14.7", "description": "FastBridge contracts.", "private": true, "files": [ @@ -26,7 +26,7 @@ "lint": "forge fmt && npm run solhint", "lint:check": "forge fmt --check && npm run solhint:check", "ci:lint": "npm run lint:check", - "build:go": "./flatten.sh contracts/*.sol test/*.sol", + "build:go": "./flatten.sh contracts/*.sol test/harnesses/*.sol test/mocks/*.sol", "solhint": "solhint '{contracts,script,test}/**/*.sol' --fix --noPrompt --max-warnings 3", "solhint:check": "solhint '{contracts,script,test}/**/*.sol' --max-warnings 3" } diff --git a/packages/contracts-rfq/test/FastBridge.t.sol b/packages/contracts-rfq/test/FastBridge.t.sol index 0e7804e49a..7fad47dc27 100644 --- a/packages/contracts-rfq/test/FastBridge.t.sol +++ b/packages/contracts-rfq/test/FastBridge.t.sol @@ -10,7 +10,7 @@ import "../contracts/interfaces/IFastBridge.sol"; import "../contracts/libs/Errors.sol"; import "../contracts/libs/UniversalToken.sol"; -import "./MockERC20.sol"; +import "./mocks/MockERC20.sol"; import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol"; diff --git a/packages/contracts-rfq/test/FastBridgeV2.Dst.Base.t.sol b/packages/contracts-rfq/test/FastBridgeV2.Dst.Base.t.sol index 42dac15820..93a7605c4a 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.Dst.Base.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.Dst.Base.t.sol @@ -50,6 +50,6 @@ contract FastBridgeV2DstBaseTest is FastBridgeV2Test { { bytes memory request = BridgeTransactionV2Lib.encodeV2(bridgeTx); vm.prank({msgSender: caller, txOrigin: caller}); - fastBridge.relay{value: msgValue}(request, relayer); + fastBridge.relayV2{value: msgValue}(request, relayer); } } diff --git a/packages/contracts-rfq/test/FastBridgeV2.Dst.t.sol b/packages/contracts-rfq/test/FastBridgeV2.Dst.t.sol index 6028e751d2..7a4148ca8c 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.Dst.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.Dst.t.sol @@ -453,19 +453,19 @@ contract FastBridgeV2DstTest is FastBridgeV2DstBaseTest { // V1 doesn't have any version field expectRevertUnsupportedVersion(0); vm.prank({msgSender: relayerA, txOrigin: relayerA}); - fastBridge.relay(mockRequestV1, relayerB); + fastBridge.relayV2(mockRequestV1, relayerB); } function test_relay_withRelayerAddress_revert_invalidRequestV2() public { expectRevertInvalidEncodedTx(); vm.prank({msgSender: relayerA, txOrigin: relayerA}); - fastBridge.relay(invalidRequestV2, relayerB); + fastBridge.relayV2(invalidRequestV2, relayerB); } function test_relay_withRelayerAddress_revert_requestV3() public { expectRevertUnsupportedVersion(3); vm.prank({msgSender: relayerA, txOrigin: relayerA}); - fastBridge.relay(mockRequestV3, relayerB); + fastBridge.relayV2(mockRequestV3, relayerB); } function test_relay_withRelayerAddress_revert_chainIncorrect() public { diff --git a/packages/contracts-rfq/test/FastBridgeV2.Src.Base.t.sol b/packages/contracts-rfq/test/FastBridgeV2.Src.Base.t.sol index 2d655aa49d..54853731ef 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.Src.Base.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.Src.Base.t.sol @@ -68,12 +68,12 @@ abstract contract FastBridgeV2SrcBaseTest is FastBridgeV2Test { public { vm.prank({msgSender: caller, txOrigin: caller}); - fastBridge.bridge{value: msgValue}(params, paramsV2); + fastBridge.bridgeV2{value: msgValue}(params, paramsV2); } function prove(address caller, bytes32 transactionId, bytes32 destTxHash, address relayer) public { vm.prank({msgSender: caller, txOrigin: caller}); - fastBridge.prove(transactionId, destTxHash, relayer); + fastBridge.proveV2(transactionId, destTxHash, relayer); } function prove(address caller, IFastBridgeV2.BridgeTransactionV2 memory bridgeTx, bytes32 destTxHash) public { @@ -83,7 +83,7 @@ abstract contract FastBridgeV2SrcBaseTest is FastBridgeV2Test { function claim(address caller, IFastBridgeV2.BridgeTransactionV2 memory bridgeTx) public { vm.prank({msgSender: caller, txOrigin: caller}); - fastBridge.claim(BridgeTransactionV2Lib.encodeV2(bridgeTx)); + fastBridge.claimV2(BridgeTransactionV2Lib.encodeV2(bridgeTx)); } function claim(address caller, IFastBridgeV2.BridgeTransactionV2 memory bridgeTx, address to) public { @@ -98,7 +98,7 @@ abstract contract FastBridgeV2SrcBaseTest is FastBridgeV2Test { function cancel(address caller, IFastBridgeV2.BridgeTransactionV2 memory bridgeTx) public virtual { vm.prank({msgSender: caller, txOrigin: caller}); - fastBridge.cancel(BridgeTransactionV2Lib.encodeV2(bridgeTx)); + fastBridge.cancelV2(BridgeTransactionV2Lib.encodeV2(bridgeTx)); } function test_nonce() public view { diff --git a/packages/contracts-rfq/test/FastBridgeV2.Src.RefundV1.t.sol b/packages/contracts-rfq/test/FastBridgeV2.Src.RefundV1.t.sol index 3b26f83c55..44443a3920 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.Src.RefundV1.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.Src.RefundV1.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import {FastBridgeV2SrcTest, BridgeTransactionV2Lib, IFastBridgeV2} from "./FastBridgeV2.Src.t.sol"; +import {BridgeTransactionV2Lib, FastBridgeV2SrcTest, IFastBridgeV2} from "./FastBridgeV2.Src.t.sol"; // solhint-disable func-name-mixedcase, ordering contract FastBridgeV2SrcRefundV1Test is FastBridgeV2SrcTest { diff --git a/packages/contracts-rfq/test/FastBridgeV2.Src.t.sol b/packages/contracts-rfq/test/FastBridgeV2.Src.t.sol index 539fe94c4d..5b3db7e3a9 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.Src.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.Src.t.sol @@ -998,19 +998,19 @@ contract FastBridgeV2SrcTest is FastBridgeV2SrcBaseTest { // V1 doesn't have any version field expectRevertUnsupportedVersion(0); vm.prank({msgSender: relayerA, txOrigin: relayerA}); - fastBridge.claim(mockRequestV1); + fastBridge.claimV2(mockRequestV1); } function test_claim_revert_invalidRequestV2() public { expectRevertInvalidEncodedTx(); vm.prank({msgSender: relayerA, txOrigin: relayerA}); - fastBridge.claim(invalidRequestV2); + fastBridge.claimV2(invalidRequestV2); } function test_claim_revert_requestV3() public { expectRevertUnsupportedVersion(3); vm.prank({msgSender: relayerA, txOrigin: relayerA}); - fastBridge.claim(mockRequestV3); + fastBridge.claimV2(mockRequestV3); } function test_claim_toDiffAddress_revert_requestV1() public { @@ -1036,18 +1036,18 @@ contract FastBridgeV2SrcTest is FastBridgeV2SrcBaseTest { // V1 doesn't have any version field expectRevertUnsupportedVersion(0); vm.prank({msgSender: relayerA, txOrigin: relayerA}); - fastBridge.cancel(mockRequestV1); + fastBridge.cancelV2(mockRequestV1); } function test_cancel_revert_invalidRequestV2() public { expectRevertInvalidEncodedTx(); vm.prank({msgSender: relayerA, txOrigin: relayerA}); - fastBridge.cancel(invalidRequestV2); + fastBridge.cancelV2(invalidRequestV2); } function test_cancel_revert_requestV3() public { expectRevertUnsupportedVersion(3); vm.prank({msgSender: relayerA, txOrigin: relayerA}); - fastBridge.cancel(mockRequestV3); + fastBridge.cancelV2(mockRequestV3); } } diff --git a/packages/contracts-rfq/test/FastBridgeV2.t.sol b/packages/contracts-rfq/test/FastBridgeV2.t.sol index 706f05d681..04bfeb7a36 100644 --- a/packages/contracts-rfq/test/FastBridgeV2.t.sol +++ b/packages/contracts-rfq/test/FastBridgeV2.t.sol @@ -8,14 +8,14 @@ import {IFastBridge} from "../contracts/interfaces/IFastBridge.sol"; // solhint-disable-next-line no-unused-import import {IFastBridgeV2} from "../contracts/interfaces/IFastBridgeV2.sol"; -import {IFastBridgeV2Errors} from "../contracts/interfaces/IFastBridgeV2Errors.sol"; import {FastBridgeV2} from "../contracts/FastBridgeV2.sol"; +import {IFastBridgeV2Errors} from "../contracts/interfaces/IFastBridgeV2Errors.sol"; -import {MockERC20} from "./MockERC20.sol"; +import {MockERC20} from "./mocks/MockERC20.sol"; import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol"; import {Test} from "forge-std/Test.sol"; -import {stdStorage, StdStorage} from "forge-std/Test.sol"; +import {StdStorage, stdStorage} from "forge-std/Test.sol"; // solhint-disable no-empty-blocks, max-states-count, ordering abstract contract FastBridgeV2Test is Test, IFastBridgeV2Errors { diff --git a/packages/contracts-rfq/test/MulticallTarget.t.sol b/packages/contracts-rfq/test/MulticallTarget.t.sol index 76141272c8..909c63463d 100644 --- a/packages/contracts-rfq/test/MulticallTarget.t.sol +++ b/packages/contracts-rfq/test/MulticallTarget.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.4; import {IMulticallTarget} from "../contracts/interfaces/IMulticallTarget.sol"; -import {MulticallTargetHarness, MulticallTarget} from "./harnesses/MulticallTargetHarness.sol"; +import {MulticallTarget, MulticallTargetHarness} from "./harnesses/MulticallTargetHarness.sol"; import {Test} from "forge-std/Test.sol"; diff --git a/packages/contracts-rfq/test/UniversalTokenLibHarness.sol b/packages/contracts-rfq/test/harnesses/UniversalTokenLibHarness.sol similarity index 93% rename from packages/contracts-rfq/test/UniversalTokenLibHarness.sol rename to packages/contracts-rfq/test/harnesses/UniversalTokenLibHarness.sol index 5e628b5fa2..0b2d2bc234 100644 --- a/packages/contracts-rfq/test/UniversalTokenLibHarness.sol +++ b/packages/contracts-rfq/test/harnesses/UniversalTokenLibHarness.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.17; -import {UniversalTokenLib} from "../contracts/libs/UniversalToken.sol"; +import {UniversalTokenLib} from "../../contracts/libs/UniversalToken.sol"; // solhint-disable no-empty-blocks, ordering contract UniversalTokenLibHarness { diff --git a/packages/contracts-rfq/test/harnesses/ZapDataV1Harness.sol b/packages/contracts-rfq/test/harnesses/ZapDataV1Harness.sol new file mode 100644 index 0000000000..b1b5cef18e --- /dev/null +++ b/packages/contracts-rfq/test/harnesses/ZapDataV1Harness.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import {ZapDataV1} from "../../contracts/libs/ZapDataV1.sol"; + +contract ZapDataV1Harness { + function validateV1(bytes calldata encodedZapData) public pure { + ZapDataV1.validateV1(encodedZapData); + } + + function encodeV1( + uint16 amountPosition_, + address target_, + bytes memory payload_ + ) + public + pure + returns (bytes memory encodedZapData) + { + return ZapDataV1.encodeV1(amountPosition_, target_, payload_); + } + + function version(bytes calldata encodedZapData) public pure returns (uint16) { + return ZapDataV1.version(encodedZapData); + } + + function target(bytes calldata encodedZapData) public pure returns (address) { + return ZapDataV1.target(encodedZapData); + } + + function payload(bytes calldata encodedZapData, uint256 amount) public pure returns (bytes memory) { + return ZapDataV1.payload(encodedZapData, amount); + } +} diff --git a/packages/contracts-rfq/test/integration/FastBridge.MulticallTarget.t.sol b/packages/contracts-rfq/test/integration/FastBridge.MulticallTarget.t.sol index 53c597136e..182b25f050 100644 --- a/packages/contracts-rfq/test/integration/FastBridge.MulticallTarget.t.sol +++ b/packages/contracts-rfq/test/integration/FastBridge.MulticallTarget.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.20; import {FastBridge} from "../../contracts/FastBridge.sol"; -import {MulticallTargetIntegrationTest, IFastBridge} from "./MulticallTarget.t.sol"; +import {IFastBridge, MulticallTargetIntegrationTest} from "./MulticallTarget.t.sol"; contract FastBridgeMulticallTargetTest is MulticallTargetIntegrationTest { function deployAndConfigureFastBridge() public override returns (address) { diff --git a/packages/contracts-rfq/test/integration/FastBridgeV2.MulticallTarget.t.sol b/packages/contracts-rfq/test/integration/FastBridgeV2.MulticallTarget.t.sol index f58dceb310..4a23e98169 100644 --- a/packages/contracts-rfq/test/integration/FastBridgeV2.MulticallTarget.t.sol +++ b/packages/contracts-rfq/test/integration/FastBridgeV2.MulticallTarget.t.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.24; import {FastBridgeV2, IFastBridgeV2} from "../../contracts/FastBridgeV2.sol"; import {BridgeTransactionV2Lib} from "../../contracts/libs/BridgeTransactionV2.sol"; -import {MulticallTargetIntegrationTest, IFastBridge} from "./MulticallTarget.t.sol"; +import {IFastBridge, MulticallTargetIntegrationTest} from "./MulticallTarget.t.sol"; contract FastBridgeV2MulticallTargetTest is MulticallTargetIntegrationTest { function deployAndConfigureFastBridge() public override returns (address) { diff --git a/packages/contracts-rfq/test/integration/FastBridgeV2.TokenZapV1.Dst.t.sol b/packages/contracts-rfq/test/integration/FastBridgeV2.TokenZapV1.Dst.t.sol new file mode 100644 index 0000000000..487f344b5e --- /dev/null +++ b/packages/contracts-rfq/test/integration/FastBridgeV2.TokenZapV1.Dst.t.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import {IFastBridge, IFastBridgeV2, TokenZapV1IntegrationTest, VaultManyArguments} from "./TokenZapV1.t.sol"; + +// solhint-disable func-name-mixedcase, ordering +contract FastBridgeV2TokenZapV1DstTest is TokenZapV1IntegrationTest { + event BridgeRelayed( + bytes32 indexed transactionId, + address indexed relayer, + address indexed to, + uint32 originChainId, + address originToken, + address destToken, + uint256 originAmount, + uint256 destAmount, + uint256 chainGasAmount + ); + + function setUp() public virtual override { + vm.chainId(DST_CHAIN_ID); + super.setUp(); + } + + function mintTokens() public virtual override { + deal(relayer, DST_AMOUNT); + dstToken.mint(relayer, DST_AMOUNT); + vm.prank(relayer); + dstToken.approve(address(fastBridge), type(uint256).max); + } + + function relay( + IFastBridge.BridgeParams memory params, + IFastBridgeV2.BridgeParamsV2 memory paramsV2, + bool isToken + ) + public + { + bytes memory encodedBridgeTx = encodeBridgeTx(params, paramsV2); + vm.prank({msgSender: relayer, txOrigin: relayer}); + fastBridge.relay{value: isToken ? paramsV2.zapNative : DST_AMOUNT}(encodedBridgeTx); + } + + function expectEventBridgeRelayed( + IFastBridge.BridgeParams memory params, + IFastBridgeV2.BridgeParamsV2 memory paramsV2, + bool isToken + ) + public + { + bytes32 txId = keccak256(encodeBridgeTx(params, paramsV2)); + vm.expectEmit(address(fastBridge)); + emit BridgeRelayed({ + transactionId: txId, + relayer: relayer, + to: address(dstZap), + originChainId: SRC_CHAIN_ID, + originToken: isToken ? address(srcToken) : NATIVE_GAS_TOKEN, + destToken: isToken ? address(dstToken) : NATIVE_GAS_TOKEN, + originAmount: SRC_AMOUNT, + destAmount: DST_AMOUNT, + chainGasAmount: paramsV2.zapNative + }); + } + + function checkBalances(bool isToken) public view { + if (isToken) { + assertEq(dstToken.balanceOf(user), 0); + assertEq(dstToken.balanceOf(relayer), 0); + assertEq(dstToken.balanceOf(address(fastBridge)), 0); + assertEq(dstToken.balanceOf(address(dstZap)), 0); + assertEq(dstToken.balanceOf(address(dstVault)), DST_AMOUNT); + assertEq(dstVault.balanceOf(user, address(dstToken)), DST_AMOUNT); + } else { + assertEq(address(user).balance, 0); + assertEq(address(relayer).balance, 0); + assertEq(address(fastBridge).balance, 0); + assertEq(address(dstZap).balance, 0); + assertEq(address(dstVault).balance, DST_AMOUNT); + assertEq(dstVault.balanceOf(user, NATIVE_GAS_TOKEN), DST_AMOUNT); + } + } + + function test_relay_depositTokenParams() public { + expectEventBridgeRelayed({params: tokenParams, paramsV2: depositTokenParams, isToken: true}); + relay({params: tokenParams, paramsV2: depositTokenParams, isToken: true}); + checkBalances({isToken: true}); + } + + function test_relay_depositTokenWithZapNativeParams() public { + expectEventBridgeRelayed({params: tokenParams, paramsV2: depositTokenWithZapNativeParams, isToken: true}); + relay({params: tokenParams, paramsV2: depositTokenWithZapNativeParams, isToken: true}); + checkBalances({isToken: true}); + // Extra ETH will be also custodied by the Vault + assertEq(address(dstVault).balance, ZAP_NATIVE); + } + + function test_relay_depositTokenRevertParams_revert() public { + vm.expectRevert(VaultManyArguments.VaultManyArguments__SomeError.selector); + relay({params: tokenParams, paramsV2: depositTokenRevertParams, isToken: true}); + } + + function test_relay_depositNativeParams() public { + expectEventBridgeRelayed({params: nativeParams, paramsV2: depositNativeParams, isToken: false}); + relay({params: nativeParams, paramsV2: depositNativeParams, isToken: false}); + checkBalances({isToken: false}); + } + + function test_relay_depositNativeNoAmountParams() public { + expectEventBridgeRelayed({params: nativeParams, paramsV2: depositNativeNoAmountParams, isToken: false}); + relay({params: nativeParams, paramsV2: depositNativeNoAmountParams, isToken: false}); + checkBalances({isToken: false}); + } + + function test_relay_depositNativeRevertParams_revert() public { + vm.expectRevert(VaultManyArguments.VaultManyArguments__SomeError.selector); + relay({params: nativeParams, paramsV2: depositNativeRevertParams, isToken: false}); + } +} diff --git a/packages/contracts-rfq/test/integration/FastBridgeV2.TokenZapV1.Src.t.sol b/packages/contracts-rfq/test/integration/FastBridgeV2.TokenZapV1.Src.t.sol new file mode 100644 index 0000000000..9b408f5669 --- /dev/null +++ b/packages/contracts-rfq/test/integration/FastBridgeV2.TokenZapV1.Src.t.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import {IFastBridge, IFastBridgeV2, TokenZapV1IntegrationTest} from "./TokenZapV1.t.sol"; + +// solhint-disable func-name-mixedcase, ordering +contract FastBridgeV2TokenZapV1SrcTest is TokenZapV1IntegrationTest { + event BridgeRequested( + bytes32 indexed transactionId, + address indexed sender, + bytes request, + uint32 destChainId, + address originToken, + address destToken, + uint256 originAmount, + uint256 destAmount, + bool sendChainGas + ); + + function setUp() public virtual override { + vm.chainId(SRC_CHAIN_ID); + super.setUp(); + } + + function mintTokens() public virtual override { + deal(user, SRC_AMOUNT); + srcToken.mint(user, SRC_AMOUNT); + vm.prank(user); + srcToken.approve(address(fastBridge), type(uint256).max); + } + + function bridge( + IFastBridge.BridgeParams memory params, + IFastBridgeV2.BridgeParamsV2 memory paramsV2, + bool isToken + ) + public + { + vm.prank({msgSender: user, txOrigin: user}); + fastBridge.bridgeV2{value: isToken ? 0 : SRC_AMOUNT}(params, paramsV2); + } + + function expectEventBridgeRequested( + IFastBridge.BridgeParams memory params, + IFastBridgeV2.BridgeParamsV2 memory paramsV2, + bool isToken + ) + public + { + bytes memory encodedBridgeTx = encodeBridgeTx(params, paramsV2); + bytes32 txId = keccak256(encodedBridgeTx); + vm.expectEmit(address(fastBridge)); + emit BridgeRequested({ + transactionId: txId, + sender: user, + request: encodedBridgeTx, + destChainId: DST_CHAIN_ID, + originToken: isToken ? address(srcToken) : NATIVE_GAS_TOKEN, + destToken: isToken ? address(dstToken) : NATIVE_GAS_TOKEN, + originAmount: SRC_AMOUNT, + destAmount: DST_AMOUNT, + sendChainGas: paramsV2.zapNative > 0 + }); + } + + function checkBalances(bool isToken) public view { + if (isToken) { + assertEq(srcToken.balanceOf(user), 0); + assertEq(srcToken.balanceOf(address(fastBridge)), SRC_AMOUNT); + } else { + assertEq(address(user).balance, 0); + assertEq(address(fastBridge).balance, SRC_AMOUNT); + } + } + + function test_bridge_depositTokenParams() public { + expectEventBridgeRequested({params: tokenParams, paramsV2: depositTokenParams, isToken: true}); + bridge({params: tokenParams, paramsV2: depositTokenParams, isToken: true}); + checkBalances({isToken: true}); + } + + function test_bridge_depositTokenWithZapNativeParams() public { + expectEventBridgeRequested({params: tokenParams, paramsV2: depositTokenWithZapNativeParams, isToken: true}); + bridge({params: tokenParams, paramsV2: depositTokenWithZapNativeParams, isToken: true}); + checkBalances({isToken: true}); + } + + function test_bridge_depositTokenRevertParams() public { + expectEventBridgeRequested({params: tokenParams, paramsV2: depositTokenRevertParams, isToken: true}); + bridge({params: tokenParams, paramsV2: depositTokenRevertParams, isToken: true}); + checkBalances({isToken: true}); + } + + function test_bridge_depositNativeParams() public { + expectEventBridgeRequested({params: nativeParams, paramsV2: depositNativeParams, isToken: false}); + bridge({params: nativeParams, paramsV2: depositNativeParams, isToken: false}); + checkBalances({isToken: false}); + } + + function test_bridge_depositNativeNoAmountParams() public { + expectEventBridgeRequested({params: nativeParams, paramsV2: depositNativeNoAmountParams, isToken: false}); + bridge({params: nativeParams, paramsV2: depositNativeNoAmountParams, isToken: false}); + checkBalances({isToken: false}); + } + + function test_bridge_depositNativeRevertParams() public { + expectEventBridgeRequested({params: nativeParams, paramsV2: depositNativeRevertParams, isToken: false}); + bridge({params: nativeParams, paramsV2: depositNativeRevertParams, isToken: false}); + checkBalances({isToken: false}); + } +} diff --git a/packages/contracts-rfq/test/integration/MulticallTarget.t.sol b/packages/contracts-rfq/test/integration/MulticallTarget.t.sol index c844086c46..0e43522346 100644 --- a/packages/contracts-rfq/test/integration/MulticallTarget.t.sol +++ b/packages/contracts-rfq/test/integration/MulticallTarget.t.sol @@ -6,7 +6,7 @@ import {IFastBridgeV2} from "../../contracts/interfaces/IFastBridgeV2.sol"; import {IMulticallTarget} from "../../contracts/interfaces/IMulticallTarget.sol"; import {DisputePeriodNotPassed} from "../../contracts/libs/Errors.sol"; -import {MockERC20} from "../MockERC20.sol"; +import {MockERC20} from "../mocks/MockERC20.sol"; import {Test} from "forge-std/Test.sol"; diff --git a/packages/contracts-rfq/test/integration/TokenZapV1.t.sol b/packages/contracts-rfq/test/integration/TokenZapV1.t.sol new file mode 100644 index 0000000000..92d3874970 --- /dev/null +++ b/packages/contracts-rfq/test/integration/TokenZapV1.t.sol @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import {FastBridgeV2, IFastBridge, IFastBridgeV2} from "../../contracts/FastBridgeV2.sol"; +import {BridgeTransactionV2Lib} from "../../contracts/libs/BridgeTransactionV2.sol"; +import {ZapDataV1} from "../../contracts/libs/ZapDataV1.sol"; +import {TokenZapV1} from "../../contracts/zaps/TokenZapV1.sol"; + +import {MockERC20} from "../mocks/MockERC20.sol"; +import {VaultManyArguments} from "../mocks/VaultManyArguments.sol"; + +import {Test} from "forge-std/Test.sol"; + +// solhint-disable ordering +abstract contract TokenZapV1IntegrationTest is Test { + address internal constant NATIVE_GAS_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + uint32 internal constant SRC_CHAIN_ID = 1337; + uint32 internal constant DST_CHAIN_ID = 7331; + + uint256 internal constant SRC_AMOUNT = 1 ether; + uint256 internal constant DST_AMOUNT = 0.9999 ether; + uint256 internal constant ZAP_NATIVE = 123_456; + + FastBridgeV2 internal fastBridge; + TokenZapV1 internal dstZap; + + address internal user = makeAddr("User"); + address internal relayer = makeAddr("Relayer"); + + MockERC20 internal srcToken; + MockERC20 internal dstToken; + + VaultManyArguments internal dstVault; + + IFastBridge.BridgeParams internal tokenParams; + IFastBridge.BridgeParams internal nativeParams; + + IFastBridgeV2.BridgeParamsV2 internal depositTokenParams; + IFastBridgeV2.BridgeParamsV2 internal depositTokenWithZapNativeParams; + IFastBridgeV2.BridgeParamsV2 internal depositTokenRevertParams; + IFastBridgeV2.BridgeParamsV2 internal depositNativeParams; + IFastBridgeV2.BridgeParamsV2 internal depositNativeNoAmountParams; + IFastBridgeV2.BridgeParamsV2 internal depositNativeRevertParams; + + function setUp() public virtual { + fastBridge = new FastBridgeV2(address(this)); + fastBridge.grantRole(fastBridge.PROVER_ROLE(), relayer); + + srcToken = new MockERC20("SRC", 18); + dstToken = new MockERC20("DST", 18); + + dstZap = new TokenZapV1(); + dstVault = new VaultManyArguments(); + + createFixtures(); + mintTokens(); + } + + function createFixtures() public virtual { + tokenParams = IFastBridge.BridgeParams({ + dstChainId: DST_CHAIN_ID, + sender: user, + to: address(dstZap), + originToken: address(srcToken), + destToken: address(dstToken), + originAmount: SRC_AMOUNT, + destAmount: DST_AMOUNT, + sendChainGas: false, + deadline: block.timestamp + 1 days + }); + nativeParams = IFastBridge.BridgeParams({ + dstChainId: DST_CHAIN_ID, + sender: user, + to: address(dstZap), + originToken: NATIVE_GAS_TOKEN, + destToken: NATIVE_GAS_TOKEN, + originAmount: SRC_AMOUNT, + destAmount: DST_AMOUNT, + sendChainGas: false, + deadline: block.timestamp + 1 days + }); + // Deposit token + bytes memory zapData = dstZap.encodeZapData({ + target: address(dstVault), + payload: getDepositPayload(address(dstToken)), + amountPosition: 4 + 32 * 2 + }); + depositTokenParams.zapData = zapData; + depositTokenWithZapNativeParams.zapData = zapData; + depositTokenWithZapNativeParams.zapNative = ZAP_NATIVE; + // Deposit native + depositNativeParams.zapData = dstZap.encodeZapData({ + target: address(dstVault), + payload: getDepositPayload(NATIVE_GAS_TOKEN), + amountPosition: 4 + 32 * 2 + }); + // Deposit no amount + depositNativeNoAmountParams.zapData = dstZap.encodeZapData({ + target: address(dstVault), + payload: getDepositNoAmountPayload(), + amountPosition: ZapDataV1.AMOUNT_NOT_PRESENT + }); + // Deposit revert + depositTokenRevertParams.zapData = dstZap.encodeZapData({ + target: address(dstVault), + payload: getDepositRevertPayload(), + amountPosition: ZapDataV1.AMOUNT_NOT_PRESENT + }); + depositNativeRevertParams.zapData = dstZap.encodeZapData({ + target: address(dstVault), + payload: getDepositRevertPayload(), + amountPosition: ZapDataV1.AMOUNT_NOT_PRESENT + }); + } + + function mintTokens() public virtual; + + function encodeBridgeTx( + IFastBridge.BridgeParams memory params, + IFastBridgeV2.BridgeParamsV2 memory paramsV2 + ) + public + pure + returns (bytes memory) + { + IFastBridgeV2.BridgeTransactionV2 memory bridgeTx = IFastBridgeV2.BridgeTransactionV2({ + originChainId: SRC_CHAIN_ID, + destChainId: params.dstChainId, + originSender: params.sender, + destRecipient: params.to, + originToken: params.originToken, + destToken: params.destToken, + originAmount: params.originAmount, + destAmount: params.destAmount, + // No protocol fees for the test + originFeeAmount: 0, + deadline: params.deadline, + // Single tx is sent, so nonce is 0 + nonce: 0, + exclusivityRelayer: address(0), + exclusivityEndTime: 0, + zapNative: paramsV2.zapNative, + zapData: paramsV2.zapData + }); + return BridgeTransactionV2Lib.encodeV2(bridgeTx); + } + + function getDepositPayload(address token) public view returns (bytes memory) { + return abi.encodeCall(dstVault.deposit, (token, abi.encode(token), DST_AMOUNT, user, abi.encode(user))); + } + + function getDepositNoAmountPayload() public view returns (bytes memory) { + return abi.encodeCall(dstVault.depositNoAmount, (user)); + } + + function getDepositRevertPayload() public view returns (bytes memory) { + return abi.encodeCall(dstVault.depositWithRevert, ()); + } +} diff --git a/packages/contracts-rfq/test/UniversalTokenLib.t.sol b/packages/contracts-rfq/test/libs/UniversalTokenLib.t.sol similarity index 96% rename from packages/contracts-rfq/test/UniversalTokenLib.t.sol rename to packages/contracts-rfq/test/libs/UniversalTokenLib.t.sol index 58bbd34435..8e900e9918 100644 --- a/packages/contracts-rfq/test/UniversalTokenLib.t.sol +++ b/packages/contracts-rfq/test/libs/UniversalTokenLib.t.sol @@ -1,10 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.17; -import {TokenNotContract} from "../contracts/libs/Errors.sol"; -import {UniversalTokenLibHarness} from "./UniversalTokenLibHarness.sol"; -import {MockERC20} from "./MockERC20.sol"; -import {MockRevertingRecipient} from "./MockRevertingRecipient.sol"; +import {TokenNotContract} from "../../contracts/libs/Errors.sol"; + +import {UniversalTokenLibHarness} from "../harnesses/UniversalTokenLibHarness.sol"; +import {MockERC20} from "../mocks/MockERC20.sol"; +import {MockRevertingRecipient} from "../mocks/MockRevertingRecipient.sol"; import {Test} from "forge-std/Test.sol"; diff --git a/packages/contracts-rfq/test/libs/ZapDataV1.t.sol b/packages/contracts-rfq/test/libs/ZapDataV1.t.sol new file mode 100644 index 0000000000..f592782ca8 --- /dev/null +++ b/packages/contracts-rfq/test/libs/ZapDataV1.t.sol @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import {ZapDataV1, ZapDataV1Harness} from "../harnesses/ZapDataV1Harness.sol"; + +import {Test} from "forge-std/Test.sol"; + +// solhint-disable func-name-mixedcase, ordering +contract ZapDataV1Test is Test { + uint16 internal constant EXPECTED_VERSION = 1; + + ZapDataV1Harness internal harness; + + function setUp() public { + harness = new ZapDataV1Harness(); + } + + function encodeZapData( + uint16 version, + uint16 amountPosition, + address target, + bytes memory payload + ) + public + pure + returns (bytes memory) + { + return abi.encodePacked(version, amountPosition, target, payload); + } + + function test_roundtrip_withAmount( + address target, + uint256 amount, + bytes memory prefix, + bytes memory postfix + ) + public + view + { + vm.assume(prefix.length + 32 + postfix.length < type(uint16).max); + vm.assume(target != address(0)); + + // We don't know the amount at the time of encoding, so we provide a placeholder. + uint16 amountPosition = uint16(prefix.length); + bytes memory encodedPayload = abi.encodePacked(prefix, uint256(0), postfix); + // We expect the correct amount to be substituted in the payload at the time of Zap. + bytes memory finalPayload = abi.encodePacked(prefix, amount, postfix); + + bytes memory zapData = harness.encodeV1(amountPosition, target, encodedPayload); + + harness.validateV1(zapData); + assertEq(harness.version(zapData), 1); + assertEq(harness.target(zapData), target); + assertEq(harness.payload(zapData, amount), finalPayload); + // Check against manually encoded ZapData. + assertEq(zapData, encodeZapData(EXPECTED_VERSION, amountPosition, target, encodedPayload)); + } + + function test_roundtrip_noAmount(address target, uint256 amount, bytes memory payload) public view { + vm.assume(payload.length < type(uint16).max); + vm.assume(target != address(0)); + + uint16 amountPosition = type(uint16).max; + bytes memory zapData = harness.encodeV1(amountPosition, target, payload); + + harness.validateV1(zapData); + assertEq(harness.version(zapData), 1); + assertEq(harness.target(zapData), target); + assertEq(harness.payload(zapData, amount), payload); + // Check against manually encoded ZapData. + assertEq(zapData, encodeZapData(EXPECTED_VERSION, amountPosition, target, payload)); + } + + function test_encodeV1_revert_targetZeroAddress() public { + vm.expectRevert(ZapDataV1.ZapDataV1__TargetZeroAddress.selector); + harness.encodeV1(type(uint16).max, address(0), ""); + } + + function test_encodeDecodeV1_revert_invalidAmountPosition( + address target, + uint16 amountPosition, + uint256 amount, + bytes memory payload + ) + public + { + vm.assume(payload.length < type(uint16).max); + vm.assume(target != address(0)); + // Make sure that (amountPosition + 32) is outside the bounds of the payload. + uint16 incorrectMin = payload.length > 31 ? uint16(payload.length) - 31 : 0; + uint16 incorrectMax = type(uint16).max - 1; + amountPosition = uint16(bound(uint256(amountPosition), incorrectMin, incorrectMax)); + bytes memory invalidEncodedZapData = abi.encodePacked(uint16(1), amountPosition, target, payload); + + vm.expectRevert(ZapDataV1.ZapDataV1__InvalidEncoding.selector); + harness.encodeV1(amountPosition, target, payload); + + // Validation should pass + harness.validateV1(invalidEncodedZapData); + harness.target(invalidEncodedZapData); + // But payload extraction should revert + vm.expectRevert(ZapDataV1.ZapDataV1__InvalidEncoding.selector); + harness.payload(invalidEncodedZapData, amount); + } + + function test_validateV1_revert_unsupportedVersion_withAmount( + uint16 version, + address target, + bytes memory prefix, + bytes memory postfix + ) + public + { + vm.assume(version != 1); + vm.assume(prefix.length + 32 + postfix.length < type(uint16).max); + // We don't know the amount at the time of encoding, so we provide a placeholder. + uint16 amountPosition = uint16(prefix.length); + bytes memory encodedPayload = abi.encodePacked(prefix, uint256(0), postfix); + + bytes memory invalidEncodedZapData = encodeZapData(version, amountPosition, target, encodedPayload); + + vm.expectRevert(abi.encodeWithSelector(ZapDataV1.ZapDataV1__UnsupportedVersion.selector, version)); + harness.validateV1(invalidEncodedZapData); + } + + function test_validateV1_revert_unsupportedVersion_noAmount( + uint16 version, + address target, + bytes memory payload + ) + public + { + vm.assume(version != 1); + vm.assume(payload.length < type(uint16).max); + + uint16 amountPosition = type(uint16).max; + bytes memory invalidEncodedZapData = encodeZapData(version, amountPosition, target, payload); + + vm.expectRevert(abi.encodeWithSelector(ZapDataV1.ZapDataV1__UnsupportedVersion.selector, version)); + harness.validateV1(invalidEncodedZapData); + } + + function test_validateV1_revert_invalidLength(bytes calldata fuzzData) public { + bytes memory minimumValidZapData = encodeZapData(EXPECTED_VERSION, type(uint16).max, address(0), ""); + uint256 invalidLength = fuzzData.length % minimumValidZapData.length; + bytes calldata invalidEncodedZapData = fuzzData[:invalidLength]; + + vm.expectRevert(ZapDataV1.ZapDataV1__InvalidEncoding.selector); + harness.validateV1(invalidEncodedZapData); + } +} diff --git a/packages/contracts-rfq/test/FastBridgeMock.sol b/packages/contracts-rfq/test/mocks/FastBridgeMock.sol similarity index 96% rename from packages/contracts-rfq/test/FastBridgeMock.sol rename to packages/contracts-rfq/test/mocks/FastBridgeMock.sol index 87c660312d..d2f7bcb3c1 100644 --- a/packages/contracts-rfq/test/FastBridgeMock.sol +++ b/packages/contracts-rfq/test/mocks/FastBridgeMock.sol @@ -1,9 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import {Admin} from "../contracts/Admin.sol"; -import {IFastBridge} from "../contracts/interfaces/IFastBridge.sol"; -import {FastBridge} from "../contracts/FastBridge.sol"; +import {Admin} from "../../contracts/Admin.sol"; + +import {FastBridge} from "../../contracts/FastBridge.sol"; +import {IFastBridge} from "../../contracts/interfaces/IFastBridge.sol"; contract FastBridgeMock is IFastBridge, Admin { // @dev the block the contract was deployed at diff --git a/packages/contracts-rfq/test/MockERC20.sol b/packages/contracts-rfq/test/mocks/MockERC20.sol similarity index 100% rename from packages/contracts-rfq/test/MockERC20.sol rename to packages/contracts-rfq/test/mocks/MockERC20.sol diff --git a/packages/contracts-rfq/test/MockRevertingRecipient.sol b/packages/contracts-rfq/test/mocks/MockRevertingRecipient.sol similarity index 100% rename from packages/contracts-rfq/test/MockRevertingRecipient.sol rename to packages/contracts-rfq/test/mocks/MockRevertingRecipient.sol diff --git a/packages/contracts-rfq/test/mocks/SimpleVaultMock.sol b/packages/contracts-rfq/test/mocks/SimpleVaultMock.sol new file mode 100644 index 0000000000..219976979e --- /dev/null +++ b/packages/contracts-rfq/test/mocks/SimpleVaultMock.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {VaultMock} from "./VaultMock.sol"; + +// solhint-disable no-empty-blocks +/// @notice Vault mock for testing purposes. DO NOT USE IN PRODUCTION. +contract SimpleVaultMock is VaultMock { + /// @notice We include an empty "test" function so that this contract does not appear in the coverage report. + function testSimpleVaultMock() external {} + + function deposit(address token, uint256 amount, address user) external payable { + _deposit(user, token, amount); + } +} diff --git a/packages/contracts-rfq/test/mocks/VaultManyArguments.sol b/packages/contracts-rfq/test/mocks/VaultManyArguments.sol new file mode 100644 index 0000000000..20408bf6b9 --- /dev/null +++ b/packages/contracts-rfq/test/mocks/VaultManyArguments.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {VaultMock} from "./VaultMock.sol"; + +// solhint-disable no-empty-blocks +/// @notice Vault mock for testing purposes. DO NOT USE IN PRODUCTION. +contract VaultManyArguments is VaultMock { + error VaultManyArguments__SomeError(); + + /// @notice We include an empty "test" function so that this contract does not appear in the coverage report. + function testSimpleVaultMock() external {} + + function deposit( + address token, + bytes memory encodedToken, + uint256 amount, + address user, + bytes memory encodedUser + ) + external + payable + { + // Make sure the data is not malformed + _validateBytes(token, encodedToken); + _validateBytes(user, encodedUser); + _deposit(user, token, amount); + } + + function depositNoAmount(address user) external payable { + _deposit(user, NATIVE_GAS_TOKEN, msg.value); + } + + function depositWithRevert() external payable { + revert VaultManyArguments__SomeError(); + } + + function _validateBytes(address addr, bytes memory encoded) internal pure { + if (keccak256(abi.encode(addr)) != keccak256(encoded)) revert VaultManyArguments__SomeError(); + } +} diff --git a/packages/contracts-rfq/test/mocks/VaultMock.sol b/packages/contracts-rfq/test/mocks/VaultMock.sol new file mode 100644 index 0000000000..b4d1f514ec --- /dev/null +++ b/packages/contracts-rfq/test/mocks/VaultMock.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +/// @notice Vault mock for testing purposes. DO NOT USE IN PRODUCTION. +abstract contract VaultMock { + using SafeERC20 for IERC20; + + address internal constant NATIVE_GAS_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + mapping(address user => mapping(address token => uint256 amount)) public balanceOf; + + error VaultMock__AmountIncorrect(); + + function _deposit(address user, address token, uint256 amount) internal { + if (token == NATIVE_GAS_TOKEN) { + if (msg.value != amount) revert VaultMock__AmountIncorrect(); + } else { + IERC20(token).safeTransferFrom(msg.sender, address(this), amount); + } + balanceOf[user][token] += amount; + } +} diff --git a/packages/contracts-rfq/test/mocks/WETHMock.sol b/packages/contracts-rfq/test/mocks/WETHMock.sol new file mode 100644 index 0000000000..75b5c4d5dd --- /dev/null +++ b/packages/contracts-rfq/test/mocks/WETHMock.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +// solhint-disable no-empty-blocks +/// @notice WETH mock for testing purposes. DO NOT USE IN PRODUCTION. +contract WETHMock is ERC20 { + constructor() ERC20("Mock Wrapped Ether", "Mock WETH") {} + + receive() external payable { + deposit(); + } + + /// @notice We include an empty "test" function so that this contract does not appear in the coverage report. + function testWETHMock() external {} + + function withdraw(uint256 amount) external { + _burn(msg.sender, amount); + Address.sendValue(payable(msg.sender), amount); + } + + function deposit() public payable { + _mint(msg.sender, msg.value); + } +} diff --git a/packages/contracts-rfq/test/zaps/TokenZapV1.GasBench.t.sol b/packages/contracts-rfq/test/zaps/TokenZapV1.GasBench.t.sol new file mode 100644 index 0000000000..5352a5e4fb --- /dev/null +++ b/packages/contracts-rfq/test/zaps/TokenZapV1.GasBench.t.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import {TokenZapV1} from "../../contracts/zaps/TokenZapV1.sol"; + +import {MockERC20} from "../mocks/MockERC20.sol"; +import {SimpleVaultMock} from "../mocks/SimpleVaultMock.sol"; + +import {Test} from "forge-std/Test.sol"; + +// solhint-disable func-name-mixedcase, ordering +contract TokenZapV1GasBenchmarkTest is Test { + uint256 internal constant AMOUNT = 0.1337 ether; + + SimpleVaultMock internal vault; + TokenZapV1 internal tokenZap; + MockERC20 internal erc20; + + address internal user; + address internal nativeGasToken = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + function setUp() public { + tokenZap = new TokenZapV1(); + vault = new SimpleVaultMock(); + erc20 = new MockERC20("TKN", 18); + + user = makeAddr("user"); + + erc20.mint(address(this), AMOUNT); + deal(address(this), AMOUNT); + // To simulate an average case we assume that the Vault contract has already other deposited funds. + erc20.mint(address(vault), 1000 * AMOUNT); + deal(address(vault), 1000 * AMOUNT); + // We also assume that this is not the first tx through the Zap, so the infinite approval has already been set. + vm.prank(address(tokenZap)); + erc20.approve(address(vault), type(uint256).max); + } + + function getVaultPayload(address token, uint256 amount) public view returns (bytes memory) { + return abi.encodeCall(vault.deposit, (token, amount, user)); + } + + function getZapData(bytes memory originalPayload) public view returns (bytes memory) { + // Amount is the second argument of the deposit function. + return tokenZap.encodeZapData(address(vault), originalPayload, 4 + 32); + } + + function test_deposit_erc20() public { + bytes memory depositPayload = getVaultPayload(address(erc20), AMOUNT); + bytes memory zapData = getZapData(depositPayload); + // Transfer tokens to the zap contract first. + erc20.transfer(address(tokenZap), AMOUNT); + tokenZap.zap(address(erc20), AMOUNT, zapData); + // Check that the vault registered the deposit. + assertEq(vault.balanceOf(user, address(erc20)), AMOUNT); + } + + function test_deposit_native() public { + bytes memory depositPayload = getVaultPayload(nativeGasToken, AMOUNT); + bytes memory zapData = getZapData(depositPayload); + tokenZap.zap{value: AMOUNT}(nativeGasToken, AMOUNT, zapData); + // Check that the vault registered the deposit. + assertEq(vault.balanceOf(user, nativeGasToken), AMOUNT); + } +} diff --git a/packages/contracts-rfq/test/zaps/TokenZapV1.t.sol b/packages/contracts-rfq/test/zaps/TokenZapV1.t.sol new file mode 100644 index 0000000000..e081831372 --- /dev/null +++ b/packages/contracts-rfq/test/zaps/TokenZapV1.t.sol @@ -0,0 +1,498 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import {ZapDataV1} from "../../contracts/libs/ZapDataV1.sol"; +import {TokenZapV1} from "../../contracts/zaps/TokenZapV1.sol"; + +import {MockERC20} from "../mocks/MockERC20.sol"; +import {NonPayableRecipient} from "../mocks/NonPayableRecipient.sol"; +import {RecipientMock} from "../mocks/RecipientMock.sol"; +import {VaultManyArguments} from "../mocks/VaultManyArguments.sol"; +import {WETHMock} from "../mocks/WETHMock.sol"; + +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {Test} from "forge-std/Test.sol"; + +// solhint-disable func-name-mixedcase, ordering +contract TokenZapV1Test is Test { + uint256 internal constant AMOUNT = 0.987 ether; + + TokenZapV1 internal tokenZap; + VaultManyArguments internal vault; + MockERC20 internal erc20; + WETHMock internal weth; + address internal payableMock; + address internal nonPayableMock; + + address internal user; + address internal nativeGasToken = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + function setUp() public { + tokenZap = new TokenZapV1(); + vault = new VaultManyArguments(); + erc20 = new MockERC20("TKN", 18); + weth = new WETHMock(); + payableMock = address(new RecipientMock()); + nonPayableMock = address(new NonPayableRecipient()); + + user = makeAddr("user"); + + erc20.mint(address(this), 100 * AMOUNT); + deal(address(this), 200 * AMOUNT); + weth.deposit{value: 100 * AMOUNT}(); + } + + function getVaultPayload(address token, uint256 amount) public view returns (bytes memory) { + return abi.encodeCall(vault.deposit, (token, abi.encode(token), amount, user, abi.encode(user))); + } + + function getVaultPayloadNoAmount() public view returns (bytes memory) { + return abi.encodeCall(vault.depositNoAmount, (user)); + } + + function getVaultPayloadWithRevert() public view returns (bytes memory) { + return abi.encodeCall(vault.depositWithRevert, ()); + } + + function getZapData(bytes memory originalPayload) public view returns (bytes memory) { + // Amount is the third argument of the deposit function + return tokenZap.encodeZapData(address(vault), originalPayload, 4 + 32 * 2); + } + + function getZapDataNoAmount(bytes memory originalPayload) public view returns (bytes memory) { + return tokenZap.encodeZapData(address(vault), originalPayload, originalPayload.length); + } + + function checkERC20HappyPath(bytes memory zapData, uint256 msgValue) public { + // Transfer tokens to the zap contract first + erc20.transfer(address(tokenZap), AMOUNT); + bytes4 returnValue = tokenZap.zap{value: msgValue}(address(erc20), AMOUNT, zapData); + assertEq(returnValue, tokenZap.zap.selector); + // Check that the vault registered the deposit + assertEq(vault.balanceOf(user, address(erc20)), AMOUNT); + } + + function test_zap_erc20_placeholderZero() public { + bytes memory zapData = getZapData(getVaultPayload(address(erc20), 0)); + checkERC20HappyPath(zapData, 0); + } + + function test_zap_erc20_placeholderNonZero() public { + // Use the approximate amount of tokens as placeholder + bytes memory zapData = getZapData(getVaultPayload(address(erc20), 1 ether)); + checkERC20HappyPath(zapData, 0); + } + + function test_zap_erc20_placeholderZero_withMsgValue() public { + bytes memory zapData = getZapData(getVaultPayload(address(erc20), 0)); + checkERC20HappyPath(zapData, 123_456); + // Should forward the msg.value to the vault + assertEq(address(vault).balance, 123_456); + } + + function test_zap_erc20_placeholderNonZero_withMsgValue() public { + bytes memory zapData = getZapData(getVaultPayload(address(erc20), 1 ether)); + checkERC20HappyPath(zapData, 123_456); + // Should forward the msg.value to the vault + assertEq(address(vault).balance, 123_456); + } + + function test_zap_erc20_placeholderZero_extraTokens() public { + // Mint some extra tokens to the zap contract + erc20.mint(address(tokenZap), AMOUNT); + // Should not affect the zap + test_zap_erc20_placeholderZero(); + } + + function test_zap_erc20_placeholderNonZero_extraTokens() public { + // Mint some extra tokens to the zap contract + erc20.mint(address(tokenZap), AMOUNT); + // Should not affect the zap + test_zap_erc20_placeholderNonZero(); + } + + function checkNativeHappyPath(bytes memory zapData) public { + bytes4 returnValue = tokenZap.zap{value: AMOUNT}(nativeGasToken, AMOUNT, zapData); + assertEq(returnValue, tokenZap.zap.selector); + // Check that the vault registered the deposit + assertEq(vault.balanceOf(user, nativeGasToken), AMOUNT); + } + + function test_zap_native_placeholderZero() public { + bytes memory zapData = getZapData(getVaultPayload(nativeGasToken, 0)); + checkNativeHappyPath(zapData); + } + + function test_zap_native_placeholderNonZero() public { + // Use the approximate amount of tokens as placeholder + bytes memory zapData = getZapData(getVaultPayload(nativeGasToken, 1 ether)); + checkNativeHappyPath(zapData); + } + + function test_zap_native_noAmount() public { + bytes memory zapData = getZapDataNoAmount(getVaultPayloadNoAmount()); + checkNativeHappyPath(zapData); + } + + function test_zap_native_placeholderZero_extraNative() public { + // Mint some extra native tokens to the zap contract + deal(address(tokenZap), AMOUNT); + // Should not affect the zap + test_zap_native_placeholderZero(); + } + + function test_zap_native_placeholderNonZero_extraNative() public { + // Mint some extra native tokens to the zap contract + deal(address(tokenZap), AMOUNT); + // Should not affect the zap + test_zap_native_placeholderNonZero(); + } + + function test_zap_native_noAmount_extraNative() public { + // Mint some extra native tokens to the zap contract + deal(address(tokenZap), AMOUNT); + // Should not affect the zap + test_zap_native_noAmount(); + } + + /// @notice Should be able to use amount lower than msg.value. + function test_zap_native_msgValueHigherThanAmount() public { + bytes memory zapData = getZapData(getVaultPayload(nativeGasToken, 1 ether)); + bytes4 returnValue = tokenZap.zap{value: AMOUNT + 1 wei}(nativeGasToken, AMOUNT, zapData); + assertEq(returnValue, tokenZap.zap.selector); + // Check that the vault registered the deposit + assertEq(vault.balanceOf(user, nativeGasToken), AMOUNT); + // Note: the extra funds are left in the contract + assertEq(address(tokenZap).balance, 1 wei); + } + + /// @notice Should be able to utilize both msg.value and existing native balance. + function test_zap_native_msgValueLowerThanAmount_extraNative() public { + deal(address(tokenZap), 1337); + bytes memory zapData = getZapData(getVaultPayload(nativeGasToken, 1 ether)); + bytes4 returnValue = tokenZap.zap{value: AMOUNT - 1337}(nativeGasToken, AMOUNT, zapData); + assertEq(returnValue, tokenZap.zap.selector); + // Check that the vault registered the deposit + assertEq(vault.balanceOf(user, nativeGasToken), AMOUNT); + } + + // ═════════════════════════════════════════════════ MULTIHOPS ═════════════════════════════════════════════════════ + + function getZapDataWithdraw(uint256 amount) public view returns (bytes memory) { + return tokenZap.encodeZapData(address(weth), abi.encodeCall(WETHMock.withdraw, (amount)), 4); + } + + function test_zap_withdraw_depositNative_placeholderZero() public { + bytes memory zapDataWithdraw = getZapDataWithdraw(0); + bytes memory zapDataDeposit = getZapDataNoAmount(getVaultPayloadNoAmount()); + weth.transfer(address(tokenZap), AMOUNT); + // Do two Zaps in a row + bytes4 returnValue = tokenZap.zap(address(weth), AMOUNT, zapDataWithdraw); + assertEq(returnValue, tokenZap.zap.selector); + returnValue = tokenZap.zap(nativeGasToken, AMOUNT, zapDataDeposit); + assertEq(returnValue, tokenZap.zap.selector); + // Check that the vault registered the deposit + assertEq(vault.balanceOf(user, nativeGasToken), AMOUNT); + } + + function test_zap_withdraw_depositNative_placeholderNonZero() public { + // Use the approximate amount of tokens as placeholder + bytes memory zapDataWithdraw = getZapDataWithdraw(1 ether); + bytes memory zapDataDeposit = getZapDataNoAmount(getVaultPayloadNoAmount()); + weth.transfer(address(tokenZap), AMOUNT); + // Do two Zaps in a row + bytes4 returnValue = tokenZap.zap(address(weth), AMOUNT, zapDataWithdraw); + assertEq(returnValue, tokenZap.zap.selector); + returnValue = tokenZap.zap(nativeGasToken, AMOUNT, zapDataDeposit); + assertEq(returnValue, tokenZap.zap.selector); + // Check that the vault registered the deposit + assertEq(vault.balanceOf(user, nativeGasToken), AMOUNT); + } + + function test_zap_withdraw_depositNative_placeholderZero_extraTokens() public { + // Transfer some extra tokens to the zap contract + weth.transfer(address(tokenZap), AMOUNT); + // Should not affect the zap + test_zap_withdraw_depositNative_placeholderZero(); + } + + function test_zap_withdraw_depositNative_placeholderZero_extraNative() public { + // Transfer some extra native tokens to the zap contract + deal(address(tokenZap), AMOUNT); + // Should not affect the zap + test_zap_withdraw_depositNative_placeholderZero(); + } + + function test_zap_withdraw_depositNative_placeholderNonZero_extraTokens() public { + // Transfer some extra tokens to the zap contract + weth.transfer(address(tokenZap), AMOUNT); + // Should not affect the zap + test_zap_withdraw_depositNative_placeholderNonZero(); + } + + function test_zap_withdraw_depositNative_placeholderNonZero_extraNative() public { + // Transfer some extra native tokens to the zap contract + deal(address(tokenZap), AMOUNT); + // Should not affect the zap + test_zap_withdraw_depositNative_placeholderNonZero(); + } + + function test_zap_withdraw_transferNativeEOA_placeholderZero() public { + bytes memory zapDataWithdraw = getZapDataWithdraw(0); + bytes memory zapDataTransfer = tokenZap.encodeZapData({target: user, payload: "", amountPosition: 0}); + weth.transfer(address(tokenZap), AMOUNT); + // Do two Zaps in a row + bytes4 returnValue = tokenZap.zap(address(weth), AMOUNT, zapDataWithdraw); + assertEq(returnValue, tokenZap.zap.selector); + returnValue = tokenZap.zap(nativeGasToken, AMOUNT, zapDataTransfer); + assertEq(returnValue, tokenZap.zap.selector); + // Check that the user received the native tokens + assertEq(user.balance, AMOUNT); + } + + function test_zap_withdraw_transferNativeEOA_placeholderNonZero() public { + // Use the approximate amount of tokens as placeholder + bytes memory zapDataWithdraw = getZapDataWithdraw(1 ether); + bytes memory zapDataTransfer = tokenZap.encodeZapData({target: user, payload: "", amountPosition: 0}); + weth.transfer(address(tokenZap), AMOUNT); + // Do two Zaps in a row + bytes4 returnValue = tokenZap.zap(address(weth), AMOUNT, zapDataWithdraw); + assertEq(returnValue, tokenZap.zap.selector); + returnValue = tokenZap.zap(nativeGasToken, AMOUNT, zapDataTransfer); + assertEq(returnValue, tokenZap.zap.selector); + // Check that the user received the native tokens + assertEq(user.balance, AMOUNT); + } + + function test_zap_withdraw_transferNativeEOA_placeholderZero_extraTokens() public { + // Transfer some extra tokens to the zap contract + weth.transfer(address(tokenZap), AMOUNT); + // Should not affect the zap + test_zap_withdraw_transferNativeEOA_placeholderZero(); + } + + function test_zap_withdraw_transferNativeEOA_placeholderZero_extraNative() public { + // Transfer some extra native tokens to the zap contract + deal(address(tokenZap), AMOUNT); + // Should not affect the zap + test_zap_withdraw_transferNativeEOA_placeholderZero(); + } + + function test_zap_withdraw_transferNativeEOA_placeholderNonZero_extraTokens() public { + // Transfer some extra tokens to the zap contract + weth.transfer(address(tokenZap), AMOUNT); + // Should not affect the zap + test_zap_withdraw_transferNativeEOA_placeholderNonZero(); + } + + function test_zap_withdraw_transferNativeEOA_placeholderNonZero_extraNative() public { + // Transfer some extra native tokens to the zap contract + deal(address(tokenZap), AMOUNT); + // Should not affect the zap + test_zap_withdraw_transferNativeEOA_placeholderNonZero(); + } + + function test_zap_withdraw_transferNativeContract_placeholderZero() public { + bytes memory zapDataWithdraw = getZapDataWithdraw(0); + bytes memory zapDataTransfer = tokenZap.encodeZapData({target: payableMock, payload: "", amountPosition: 0}); + weth.transfer(address(tokenZap), AMOUNT); + // Do two Zaps in a row + bytes4 returnValue = tokenZap.zap(address(weth), AMOUNT, zapDataWithdraw); + assertEq(returnValue, tokenZap.zap.selector); + returnValue = tokenZap.zap(nativeGasToken, AMOUNT, zapDataTransfer); + assertEq(returnValue, tokenZap.zap.selector); + // Check that the contract received the native tokens + assertEq(payableMock.balance, AMOUNT); + } + + function test_zap_withdraw_transferNativeContract_placeholderNonZero() public { + // Use the approximate amount of tokens as placeholder + bytes memory zapDataWithdraw = getZapDataWithdraw(1 ether); + bytes memory zapDataTransfer = tokenZap.encodeZapData({target: payableMock, payload: "", amountPosition: 0}); + weth.transfer(address(tokenZap), AMOUNT); + // Do two Zaps in a row + bytes4 returnValue = tokenZap.zap(address(weth), AMOUNT, zapDataWithdraw); + assertEq(returnValue, tokenZap.zap.selector); + returnValue = tokenZap.zap(nativeGasToken, AMOUNT, zapDataTransfer); + assertEq(returnValue, tokenZap.zap.selector); + // Check that the contract received the native tokens + assertEq(payableMock.balance, AMOUNT); + } + + function test_zap_withdraw_transferNativeContract_placeholderZero_extraTokens() public { + // Transfer some extra tokens to the zap contract + weth.transfer(address(tokenZap), AMOUNT); + // Should not affect the zap + test_zap_withdraw_transferNativeContract_placeholderZero(); + } + + function test_zap_withdraw_transferNativeContract_placeholderZero_extraNative() public { + // Transfer some extra native tokens to the zap contract + deal(address(tokenZap), AMOUNT); + // Should not affect the zap + test_zap_withdraw_transferNativeContract_placeholderZero(); + } + + function test_zap_withdraw_transferNativeContract_placeholderNonZero_extraTokens() public { + // Transfer some extra tokens to the zap contract + weth.transfer(address(tokenZap), AMOUNT); + // Should not affect the zap + test_zap_withdraw_transferNativeContract_placeholderNonZero(); + } + + function test_zap_withdraw_transferNativeContract_placeholderNonZero_extraNative() public { + // Transfer some extra native tokens to the zap contract + deal(address(tokenZap), AMOUNT); + // Should not affect the zap + test_zap_withdraw_transferNativeContract_placeholderNonZero(); + } + + // ═════════════════════════════════════════════════ ENCODING ══════════════════════════════════════════════════════ + + function test_encodeZapData_roundtrip(address token, uint256 placeholderAmount, uint256 amount) public view { + bytes memory originalPayload = getVaultPayload(token, placeholderAmount); + bytes memory expectedPayload = getVaultPayload(token, amount); + + bytes memory zapData = getZapData(originalPayload); + (address target, bytes memory payload) = tokenZap.decodeZapData(zapData, amount); + + assertEq(target, address(vault)); + assertEq(payload, expectedPayload); + } + + function test_encodeZapData_roundtripNoAmount(uint256 amountPosition) public view { + bytes memory payload = getVaultPayloadNoAmount(); + // Any value >= payload.length could be used to signal that the amount is not an argument of the target function + amountPosition = bound(amountPosition, payload.length, type(uint256).max); + + bytes memory zapData = tokenZap.encodeZapData(address(vault), payload, amountPosition); + (address target, bytes memory decodedPayload) = tokenZap.decodeZapData(zapData, 0); + assertEq(target, address(vault)); + assertEq(decodedPayload, payload); + } + + // ══════════════════════════════════════════════════ REVERTS ══════════════════════════════════════════════════════ + + function getZeroTargetZapData(bytes memory payload, uint16 amountPosition) public pure returns (bytes memory) { + // Encode manually as the library checks for zero address + return abi.encodePacked(ZapDataV1.VERSION, amountPosition, address(0), payload); + } + + function test_zap_erc20_revert_notEnoughTokens() public { + bytes memory zapData = getZapData(getVaultPayload(address(erc20), 0)); + // Transfer tokens to the zap contract first, but not enough + erc20.transfer(address(tokenZap), AMOUNT - 1); + vm.expectRevert(); + tokenZap.zap(address(erc20), AMOUNT, zapData); + } + + function test_zap_erc20_revert_targetReverted() public { + bytes memory zapData = getZapData(getVaultPayloadWithRevert()); + // Transfer tokens to the zap contract first + erc20.transfer(address(tokenZap), AMOUNT); + vm.expectRevert(VaultManyArguments.VaultManyArguments__SomeError.selector); + tokenZap.zap(address(erc20), AMOUNT, zapData); + } + + function test_zap_erc20_revert_targetZeroAddress_emptyPayload() public { + bytes memory zapData = getZeroTargetZapData({payload: "", amountPosition: 0}); + // Transfer tokens to the zap contract first + erc20.transfer(address(tokenZap), AMOUNT); + vm.expectRevert(TokenZapV1.TokenZapV1__TargetZeroAddress.selector); + tokenZap.zap(address(erc20), AMOUNT, zapData); + } + + function test_zap_erc20_revert_targetZeroAddress_nonEmptyPayload() public { + bytes memory zapData = + getZeroTargetZapData({payload: getVaultPayload(address(erc20), 0), amountPosition: 4 + 32 * 2}); + // Transfer tokens to the zap contract first + erc20.transfer(address(tokenZap), AMOUNT); + vm.expectRevert(TokenZapV1.TokenZapV1__TargetZeroAddress.selector); + tokenZap.zap(address(erc20), AMOUNT, zapData); + } + + function test_zap_erc20_revert_targetEOA_nonEmptyPayload() public { + bytes memory zapData = tokenZap.encodeZapData({ + target: user, + payload: getVaultPayload(address(erc20), 0), + amountPosition: 4 + 32 * 2 + }); + // Transfer tokens to the zap contract first + erc20.transfer(address(tokenZap), AMOUNT); + vm.expectRevert(abi.encodeWithSelector(Address.AddressEmptyCode.selector, user)); + tokenZap.zap(address(erc20), AMOUNT, zapData); + } + + function test_zap_erc20_revert_targetEOA_emptyPayload() public { + bytes memory zapData = tokenZap.encodeZapData({target: user, payload: "", amountPosition: 0}); + // Transfer tokens to the zap contract first + erc20.transfer(address(tokenZap), AMOUNT); + vm.expectRevert(abi.encodeWithSelector(Address.AddressEmptyCode.selector, user)); + tokenZap.zap(address(erc20), AMOUNT, zapData); + } + + function test_zap_native_revert_targetReverted() public { + bytes memory zapData = getZapData(getVaultPayloadWithRevert()); + vm.expectRevert(VaultManyArguments.VaultManyArguments__SomeError.selector); + tokenZap.zap{value: AMOUNT}(nativeGasToken, AMOUNT, zapData); + } + + function test_zap_native_revert_msgValueLowerThanExpected() public { + bytes memory originalPayload = getVaultPayload(nativeGasToken, 0); + bytes memory zapData = getZapData(originalPayload); + + vm.expectRevert(abi.encodeWithSelector(Address.AddressInsufficientBalance.selector, tokenZap)); + tokenZap.zap{value: 1 ether - 1 wei}(nativeGasToken, 1 ether, zapData); + } + + function test_zap_withdraw_transferNative_revert_targetReverted() public { + bytes memory zapDataWithdraw = getZapDataWithdraw(0); + bytes memory zapDataTransfer = tokenZap.encodeZapData({target: nonPayableMock, payload: "", amountPosition: 0}); + weth.transfer(address(tokenZap), AMOUNT); + tokenZap.zap(address(weth), AMOUNT, zapDataWithdraw); + vm.expectRevert(Address.FailedInnerCall.selector); + tokenZap.zap(address(weth), AMOUNT, zapDataTransfer); + } + + function test_zap_withdraw_transferNative_revert_targetZeroAddress_emptyPayload() public { + bytes memory zapDataWithdraw = getZapDataWithdraw(0); + bytes memory zapDataTransfer = getZeroTargetZapData({payload: "", amountPosition: 0}); + weth.transfer(address(tokenZap), AMOUNT); + tokenZap.zap(address(weth), AMOUNT, zapDataWithdraw); + vm.expectRevert(TokenZapV1.TokenZapV1__TargetZeroAddress.selector); + tokenZap.zap(address(weth), AMOUNT, zapDataTransfer); + } + + function test_zap_withdraw_transferNative_revert_targetZeroAddress_nonEmptyPayload() public { + bytes memory zapDataWithdraw = getZapDataWithdraw(0); + bytes memory payload = getVaultPayloadNoAmount(); + bytes memory zapDataTransfer = getZeroTargetZapData({payload: payload, amountPosition: uint16(payload.length)}); + weth.transfer(address(tokenZap), AMOUNT); + tokenZap.zap(address(weth), AMOUNT, zapDataWithdraw); + vm.expectRevert(TokenZapV1.TokenZapV1__TargetZeroAddress.selector); + tokenZap.zap(address(weth), AMOUNT, zapDataTransfer); + } + + function test_zap_withdraw_transferNative_revert_targetEOA_nonEmptyPayload() public { + bytes memory zapDataWithdraw = getZapDataWithdraw(0); + bytes memory zapDataTransfer = + tokenZap.encodeZapData({target: user, payload: getVaultPayloadNoAmount(), amountPosition: 0}); + weth.transfer(address(tokenZap), AMOUNT); + tokenZap.zap(address(weth), AMOUNT, zapDataWithdraw); + vm.expectRevert(abi.encodeWithSelector(Address.AddressEmptyCode.selector, user)); + tokenZap.zap(address(weth), AMOUNT, zapDataTransfer); + } + + function test_encodeZapData_revert_payloadLengthAboveMax() public { + bytes memory tooLongPayload = new bytes(2 ** 16); + vm.expectRevert(TokenZapV1.TokenZapV1__PayloadLengthAboveMax.selector); + tokenZap.encodeZapData(address(vault), tooLongPayload, 0); + } + + function test_encodeZapData_revert_targetZeroAddress() public { + bytes memory payload = getVaultPayloadNoAmount(); + + vm.expectRevert(ZapDataV1.ZapDataV1__TargetZeroAddress.selector); + tokenZap.encodeZapData(address(0), payload, payload.length); + } +} diff --git a/packages/explorer-ui/CHANGELOG.md b/packages/explorer-ui/CHANGELOG.md index 39bc5e1dd5..caa1b0fd5e 100644 --- a/packages/explorer-ui/CHANGELOG.md +++ b/packages/explorer-ui/CHANGELOG.md @@ -3,6 +3,30 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.5.10](https://github.com/synapsecns/sanguine/compare/@synapsecns/explorer-ui@0.5.9...@synapsecns/explorer-ui@0.5.10) (2024-12-02) + +**Note:** Version bump only for package @synapsecns/explorer-ui + + + + + +## [0.5.9](https://github.com/synapsecns/sanguine/compare/@synapsecns/explorer-ui@0.5.8...@synapsecns/explorer-ui@0.5.9) (2024-12-01) + +**Note:** Version bump only for package @synapsecns/explorer-ui + + + + + +## [0.5.8](https://github.com/synapsecns/sanguine/compare/@synapsecns/explorer-ui@0.5.7...@synapsecns/explorer-ui@0.5.8) (2024-11-30) + +**Note:** Version bump only for package @synapsecns/explorer-ui + + + + + ## [0.5.7](https://github.com/synapsecns/sanguine/compare/@synapsecns/explorer-ui@0.5.6...@synapsecns/explorer-ui@0.5.7) (2024-11-07) **Note:** Version bump only for package @synapsecns/explorer-ui diff --git a/packages/explorer-ui/package.json b/packages/explorer-ui/package.json index 81cd21c6f4..bab3fec384 100644 --- a/packages/explorer-ui/package.json +++ b/packages/explorer-ui/package.json @@ -1,6 +1,6 @@ { "name": "@synapsecns/explorer-ui", - "version": "0.5.7", + "version": "0.5.10", "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.8.2", + "@synapsecns/synapse-constants": "^1.8.5", "@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/CHANGELOG.md b/packages/rest-api/CHANGELOG.md index 2e3042a10d..55fd061187 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.8.12](https://github.com/synapsecns/sanguine/compare/@synapsecns/rest-api@1.8.11...@synapsecns/rest-api@1.8.12) (2024-12-02) + + +### Bug Fixes + +* update klaytn/kaia rpc url ([#3426](https://github.com/synapsecns/sanguine/issues/3426)) ([590594a](https://github.com/synapsecns/sanguine/commit/590594a80d576ea916052f25e22220823623e088)) + + + + + +## [1.8.11](https://github.com/synapsecns/sanguine/compare/@synapsecns/rest-api@1.8.10...@synapsecns/rest-api@1.8.11) (2024-12-01) + +**Note:** Version bump only for package @synapsecns/rest-api + + + + + +## [1.8.10](https://github.com/synapsecns/sanguine/compare/@synapsecns/rest-api@1.8.9...@synapsecns/rest-api@1.8.10) (2024-12-01) + +**Note:** Version bump only for package @synapsecns/rest-api + + + + + +## [1.8.9](https://github.com/synapsecns/sanguine/compare/@synapsecns/rest-api@1.8.8...@synapsecns/rest-api@1.8.9) (2024-11-30) + +**Note:** Version bump only for package @synapsecns/rest-api + + + + + +## [1.8.8](https://github.com/synapsecns/sanguine/compare/@synapsecns/rest-api@1.8.7...@synapsecns/rest-api@1.8.8) (2024-11-19) + +**Note:** Version bump only for package @synapsecns/rest-api + + + + + ## [1.8.7](https://github.com/synapsecns/sanguine/compare/@synapsecns/rest-api@1.8.6...@synapsecns/rest-api@1.8.7) (2024-11-11) **Note:** Version bump only for package @synapsecns/rest-api diff --git a/packages/rest-api/package.json b/packages/rest-api/package.json index f5727c6543..98611d1271 100644 --- a/packages/rest-api/package.json +++ b/packages/rest-api/package.json @@ -1,6 +1,6 @@ { "name": "@synapsecns/rest-api", - "version": "1.8.7", + "version": "1.8.12", "private": "true", "engines": { "node": ">=18.17.0" @@ -22,8 +22,10 @@ "@ethersproject/bignumber": "^5.7.0", "@ethersproject/providers": "^5.7.2", "@ethersproject/units": "5.7.0", - "@synapsecns/sdk-router": "^0.11.6", + "@synapsecns/sdk-router": "^0.11.8", + "@synapsecns/synapse-constants": "^1.8.5", "bignumber": "^1.1.0", + "cross-fetch": "^4.0.0", "dotenv": "^16.4.5", "ethers": "5.7.2", "express": "^4.18.2", @@ -32,7 +34,7 @@ "jest": "^29.7.0", "lodash": "^4.17.21", "supertest": "^6.3.3", - "typescript": "^4.8.3", + "typescript": "^5.3.3", "winston": "^3.14.2" }, "description": "A node.js project exposing a rest api for synapse sdk quotes", diff --git a/packages/rest-api/src/constants/bridgeMap.ts b/packages/rest-api/src/constants/bridgeMap.ts index bc84ea0073..dda798b68c 100644 --- a/packages/rest-api/src/constants/bridgeMap.ts +++ b/packages/rest-api/src/constants/bridgeMap.ts @@ -28,13 +28,6 @@ export const BRIDGE_MAP = { destination: ['JEWEL'], swappable: [], }, - '0x163f8C2467924be0ae7B5347228CABF260318753': { - decimals: 18, - symbol: 'WLD', - origin: ['RFQ.WLD'], - destination: ['RFQ.WLD'], - swappable: [], - }, '0x1B84765dE8B7566e4cEAF4D0fD3c5aF52D3DdE4F': { decimals: 18, symbol: 'nUSD', @@ -462,13 +455,6 @@ export const BRIDGE_MAP = { destination: ['L2DAO'], swappable: [], }, - '0xdC6fF44d5d932Cbd77B52E5612Ba0529DC6226F1': { - decimals: 18, - symbol: 'WLD', - origin: ['RFQ.WLD'], - destination: ['RFQ.WLD'], - swappable: [], - }, }, '25': { '0x396c9c192dd323995346632581BEF92a31AC623b': { @@ -522,13 +508,6 @@ export const BRIDGE_MAP = { destination: ['JUMP'], swappable: [], }, - '0x2170Ed0880ac9A755fd29B2688956BD959F933F8': { - decimals: 18, - symbol: 'ETH', - origin: ['RFQ.ETH'], - destination: [], - swappable: [], - }, '0x23b891e5C62E0955ae2bD185990103928Ab817b3': { decimals: 18, symbol: 'nUSD', @@ -576,6 +555,13 @@ export const BRIDGE_MAP = { '0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56', ], }, + '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE': { + decimals: 18, + symbol: 'BNB', + origin: ['RFQ.BNB'], + destination: [], + swappable: [], + }, '0xa4080f1778e69467E905B8d6F72f6e441f9e9484': { decimals: 18, symbol: 'SYN', @@ -590,6 +576,13 @@ export const BRIDGE_MAP = { destination: ['DOG'], swappable: [], }, + '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c': { + decimals: 18, + symbol: 'WBNB', + origin: ['RFQ.BNB'], + destination: [], + swappable: [], + }, '0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56': { decimals: 18, symbol: 'BUSD', @@ -894,11 +887,11 @@ export const BRIDGE_MAP = { }, }, '480': { - '0x2cFc85d8E48F8EAB294be644d9E25C3030863003': { + '0x4200000000000000000000000000000000000006': { decimals: 18, - symbol: 'WLD', - origin: ['RFQ.WLD'], - destination: ['RFQ.WLD'], + symbol: 'WETH', + origin: ['RFQ.ETH'], + destination: [], swappable: [], }, '0x79A02482A880bCE3F13e09Da970dC34db4CD24d1': { @@ -912,7 +905,7 @@ export const BRIDGE_MAP = { decimals: 18, symbol: 'ETH', origin: ['RFQ.ETH'], - destination: ['RFQ.ETH'], + destination: [], swappable: [], }, }, diff --git a/packages/rest-api/src/constants/bridgeable.ts b/packages/rest-api/src/constants/bridgeable.ts index eb35353586..a1bf82ebf7 100644 --- a/packages/rest-api/src/constants/bridgeable.ts +++ b/packages/rest-api/src/constants/bridgeable.ts @@ -1412,24 +1412,3 @@ export const SPECTRAL: BridgeableToken = { imgUrl: 'https://105bc697.sanguine-fe.pages.dev/_next/static/media/spectral.6d51750c.svg', } - -export const WLD: BridgeableToken = { - priorityRank: 106, - addresses: { - [CHAINS.ETHEREUM.id]: '0x163f8C2467924be0ae7B5347228CABF260318753', - [CHAINS.OPTIMISM.id]: '0xdC6fF44d5d932Cbd77B52E5612Ba0529DC6226F1', - [CHAINS.WORLDCHAIN.id]: '0x2cFc85d8E48F8EAB294be644d9E25C3030863003', - }, - decimals: { - [CHAINS.ETHEREUM.id]: 18, - [CHAINS.OPTIMISM.id]: 18, - [CHAINS.WORLDCHAIN.id]: 18, - }, - symbol: 'WLD', - name: 'Worldcoin', - swapableType: 'WLD', - color: 'gray', - routeSymbol: 'WLD', - imgUrl: - 'https://synapse-interface-worldchain.sanguine-fe.pages.dev/_next/static/media/worldchain.62d1dfd2.svg', -} diff --git a/packages/rest-api/src/constants/chains.ts b/packages/rest-api/src/constants/chains.ts index fb5edca42b..57679b5161 100644 --- a/packages/rest-api/src/constants/chains.ts +++ b/packages/rest-api/src/constants/chains.ts @@ -176,7 +176,7 @@ export const KLAYTN: Chain = { name: 'Klaytn', rpcUrls: { primary: getOmniRpcUrl(8217), - fallback: 'https://klaytn.blockpi.network/v1/rpc/public', + fallback: 'https://kaia.blockpi.network/v1/rpc/public', }, explorerUrl: 'https://scope.klaytn.com', explorerName: 'Klaytn Explorer', diff --git a/packages/rest-api/src/routes/addressIconRoute.ts b/packages/rest-api/src/routes/addressIconRoute.ts new file mode 100644 index 0000000000..3891f6b791 --- /dev/null +++ b/packages/rest-api/src/routes/addressIconRoute.ts @@ -0,0 +1,53 @@ +import express from 'express' +import { BRIDGABLE_TOKENS, Token } from '@synapsecns/synapse-constants' +import fetch from 'cross-fetch' + +const router: express.Router = express.Router() + +router.get('/:chainId/:address.svg', async (req, res) => { + const chainId = parseInt(req.params.chainId, 10) + const address = req.params.address.toLowerCase() + + // Find the token with matching address on the specified chain + const token = Object.values(BRIDGABLE_TOKENS[chainId] || []).find( + (t): t is Token => + typeof t === 'object' && + t !== null && + 'addresses' in t && + Object.entries(t.addresses).some(([chain, addr]) => { + const matches = + parseInt(chain, 10) === chainId && addr.toLowerCase() === address + return matches + }) + ) + + if (!token || !token.icon) { + console.log('Token not found or no icon:', { token }) + res.status(404).json({ error: 'Token icon not found' }) + return + } + + try { + // Fetch the image from the URL + const response = await fetch(token.icon) + if (!response.ok) { + throw new Error(`Failed to fetch image: ${response.statusText}`) + } + + const buffer = await response.arrayBuffer() + + // Set cache headers (cache for 1 week) + res.set({ + 'Cache-Control': 'public, max-age=604800', + 'Content-Type': response.headers.get('content-type') || 'image/svg+xml', + }) + + res.send(Buffer.from(buffer)) + } catch (error) { + console.error('Error fetching token icon:', error) + res.status(500).json({ error: 'Failed to fetch token icon' }) + } + return +}) + +export default router diff --git a/packages/rest-api/src/routes/bridgeTxInfoRoute.ts b/packages/rest-api/src/routes/bridgeTxInfoRoute.ts index 357c08fe65..10e74c4e50 100644 --- a/packages/rest-api/src/routes/bridgeTxInfoRoute.ts +++ b/packages/rest-api/src/routes/bridgeTxInfoRoute.ts @@ -19,8 +19,8 @@ const router: express.Router = express.Router() * @openapi * /bridgeTxInfo: * get: - * summary: Get bridge transaction information - * description: Retrieve transaction information for bridging tokens between chains + * summary: "[Deprecated] in favor of using the /bridge endpoint, which now returns call data" + * description: "[Deprecated] Originally used to get Bridge transaction information" * parameters: * - in: query * name: fromChain diff --git a/packages/rest-api/src/routes/chainIconRoute.ts b/packages/rest-api/src/routes/chainIconRoute.ts new file mode 100644 index 0000000000..708a4bad6c --- /dev/null +++ b/packages/rest-api/src/routes/chainIconRoute.ts @@ -0,0 +1,44 @@ +import express from 'express' +import { CHAINS, Chain } from '@synapsecns/synapse-constants' +import fetch from 'cross-fetch' + +const router: express.Router = express.Router() + +router.get('/:chainId.svg', async (req, res) => { + const chainId = parseInt(req.params.chainId, 10) + + // Find the chain with matching ID + const chain = Object.values(CHAINS).find( + (c): c is Chain => + typeof c === 'object' && c !== null && 'id' in c && c.id === chainId + ) + + if (!chain || !chain.chainImg) { + res.status(404).json({ error: 'Chain icon not found' }) + return + } + + try { + // Fetch the image from the URL + const response = await fetch(chain.chainImg) + if (!response.ok) { + throw new Error(`Failed to fetch image: ${response.statusText}`) + } + + const buffer = await response.arrayBuffer() + + // Set cache headers (cache for 1 week) + res.set({ + 'Cache-Control': 'public, max-age=604800', + 'Content-Type': response.headers.get('content-type') || 'image/svg+xml', + }) + + res.send(Buffer.from(buffer)) + } catch (error) { + console.error('Error fetching chain icon:', error) + res.status(500).json({ error: 'Failed to fetch chain icon' }) + } + return +}) + +export default router diff --git a/packages/rest-api/src/routes/index.ts b/packages/rest-api/src/routes/index.ts index c723d8d21b..0065901805 100644 --- a/packages/rest-api/src/routes/index.ts +++ b/packages/rest-api/src/routes/index.ts @@ -11,6 +11,8 @@ import destinationTxRoute from './destinationTxRoute' import tokenListRoute from './tokenListRoute' import destinationTokensRoute from './destinationTokensRoute' import bridgeLimitsRoute from './bridgeLimitsRoute' +import chainIconRoute from './chainIconRoute' +import addressIconRoute from './addressIconRoute' const router: express.Router = express.Router() @@ -25,5 +27,7 @@ router.use('/bridgeTxStatus', bridgeTxStatusRoute) router.use('/destinationTx', destinationTxRoute) router.use('/tokenList', tokenListRoute) router.use('/destinationTokens', destinationTokensRoute) +router.use('/chainIcon', chainIconRoute) +router.use('/tokenIcon', addressIconRoute) export default router diff --git a/packages/rest-api/src/routes/swapTxInfoRoute.ts b/packages/rest-api/src/routes/swapTxInfoRoute.ts index 041fc29ac3..218b9a130b 100644 --- a/packages/rest-api/src/routes/swapTxInfoRoute.ts +++ b/packages/rest-api/src/routes/swapTxInfoRoute.ts @@ -18,8 +18,8 @@ const router: express.Router = express.Router() * @openapi * /swapTxInfo: * get: - * summary: Get swap transaction information - * description: Retrieve transaction information for swapping tokens on a specific chain + * summary: "[Deprecated] in favor of using the /swap endpoint, which now returns call data + * description: "[Deprecated] Originally used to get Swap transaction information * parameters: * - in: query * name: chain diff --git a/packages/rest-api/src/tests/indexRoute.test.ts b/packages/rest-api/src/tests/indexRoute.test.ts index afb7c0e146..f5dbecf375 100644 --- a/packages/rest-api/src/tests/indexRoute.test.ts +++ b/packages/rest-api/src/tests/indexRoute.test.ts @@ -27,6 +27,6 @@ describe('Index Route', () => { expect(response.body.availableChains.length).toBe(23) expect(response.body).toHaveProperty('availableTokens') - expect(response.body.availableTokens.length).toBe(64) + expect(response.body.availableTokens.length).toBe(63) }) }) diff --git a/packages/rest-api/src/tests/tokenListRoute.test.ts b/packages/rest-api/src/tests/tokenListRoute.test.ts index 9c0109e4cb..d9f68e1106 100644 --- a/packages/rest-api/src/tests/tokenListRoute.test.ts +++ b/packages/rest-api/src/tests/tokenListRoute.test.ts @@ -14,7 +14,7 @@ describe('Index Route', () => { const keys = Object.keys(response.body) - expect(keys.length).toBe(64) + expect(keys.length).toBe(63) expect(response.body['ETH']['addresses']['1']).toBe( '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE' ) diff --git a/packages/rest-api/swagger.json b/packages/rest-api/swagger.json index 7190b10524..2cb16577c3 100644 --- a/packages/rest-api/swagger.json +++ b/packages/rest-api/swagger.json @@ -396,8 +396,8 @@ }, "/bridgeTxInfo": { "get": { - "summary": "Get bridge transaction information", - "description": "Retrieve transaction information for bridging tokens between chains", + "summary": "[Deprecated] in favor of using the /bridge endpoint, which now returns call data", + "description": "[Deprecated] Originally used to get Bridge transaction information", "parameters": [ { "in": "query", @@ -1345,8 +1345,8 @@ }, "/swapTxInfo": { "get": { - "summary": "Get swap transaction information", - "description": "Retrieve transaction information for swapping tokens on a specific chain", + "summary": "[Deprecated] in favor of using the /swap endpoint, which now returns call data", + "description": "[Deprecated] Originally used to get Swap transaction information", "parameters": [ { "in": "query", @@ -2638,10 +2638,10 @@ "/rfq": { "put": { "tags": ["RFQ API"], - "summary": "Handle user quote request", - "description": "Handle user quote request and return the best quote available.", + "summary": "Initiate an Active RFQ", + "description": "Initiate an Active Request-For-Quote return the best quote available.", "requestBody": { - "description": "User quote request", + "description": "Initiate an Active Request-For-Quote", "content": { "application/json": { "schema": { @@ -2677,8 +2677,8 @@ "/rfq_stream": { "get": { "tags": ["RFQ API"], - "summary": "Handle WebSocket connection for active quote requests", - "description": "Establish a WebSocket connection to receive active quote requests.", + "summary": "Listen for Active RFQs", + "description": "Establish a WebSocket connection to listen for Active Requests-For-Quote.", "responses": { "101": { "description": "Switching Protocols", diff --git a/packages/sdk-router/CHANGELOG.md b/packages/sdk-router/CHANGELOG.md index 43bc5fbf94..98e6f7784c 100644 --- a/packages/sdk-router/CHANGELOG.md +++ b/packages/sdk-router/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.11.8](https://github.com/synapsecns/sanguine/compare/@synapsecns/sdk-router@0.11.7...@synapsecns/sdk-router@0.11.8) (2024-12-01) + + +### Reverts + +* Revert "reducing dependency bloat (#3411)" (#3421) ([5feb5a0](https://github.com/synapsecns/sanguine/commit/5feb5a0883e297bafa328fbe5c86935ed1ed2fa5)), closes [#3411](https://github.com/synapsecns/sanguine/issues/3411) [#3421](https://github.com/synapsecns/sanguine/issues/3421) + + + + + +## [0.11.7](https://github.com/synapsecns/sanguine/compare/@synapsecns/sdk-router@0.11.6...@synapsecns/sdk-router@0.11.7) (2024-11-30) + +**Note:** Version bump only for package @synapsecns/sdk-router + + + + + ## [0.11.6](https://github.com/synapsecns/sanguine/compare/@synapsecns/sdk-router@0.11.5...@synapsecns/sdk-router@0.11.6) (2024-11-07) **Note:** Version bump only for package @synapsecns/sdk-router diff --git a/packages/sdk-router/package.json b/packages/sdk-router/package.json index 471a4f10e4..e8569b9ba9 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.6", + "version": "0.11.8", "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 8e167621e9..a9e1f0ae02 100644 --- a/packages/synapse-constants/CHANGELOG.md +++ b/packages/synapse-constants/CHANGELOG.md @@ -3,6 +3,36 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.8.5](https://github.com/synapsecns/sanguine/compare/@synapsecns/synapse-constants@1.8.4...@synapsecns/synapse-constants@1.8.5) (2024-12-02) + + +### Bug Fixes + +* update klaytn/kaia rpc url ([#3426](https://github.com/synapsecns/sanguine/issues/3426)) ([590594a](https://github.com/synapsecns/sanguine/commit/590594a80d576ea916052f25e22220823623e088)) + + + + + +## [1.8.4](https://github.com/synapsecns/sanguine/compare/@synapsecns/synapse-constants@1.8.3...@synapsecns/synapse-constants@1.8.4) (2024-12-01) + + +### Reverts + +* Revert "reducing dependency bloat (#3411)" (#3421) ([5feb5a0](https://github.com/synapsecns/sanguine/commit/5feb5a0883e297bafa328fbe5c86935ed1ed2fa5)), closes [#3411](https://github.com/synapsecns/sanguine/issues/3411) [#3421](https://github.com/synapsecns/sanguine/issues/3421) + + + + + +## [1.8.3](https://github.com/synapsecns/sanguine/compare/@synapsecns/synapse-constants@1.8.2...@synapsecns/synapse-constants@1.8.3) (2024-11-30) + +**Note:** Version bump only for package @synapsecns/synapse-constants + + + + + ## [1.8.2](https://github.com/synapsecns/sanguine/compare/@synapsecns/synapse-constants@1.8.1...@synapsecns/synapse-constants@1.8.2) (2024-11-07) **Note:** Version bump only for package @synapsecns/synapse-constants diff --git a/packages/synapse-constants/package.json b/packages/synapse-constants/package.json index 88fb1a70f9..eb89b579e2 100644 --- a/packages/synapse-constants/package.json +++ b/packages/synapse-constants/package.json @@ -1,6 +1,6 @@ { "name": "@synapsecns/synapse-constants", - "version": "1.8.2", + "version": "1.8.5", "description": "This is an npm package that maintains all synapse constants", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", diff --git a/packages/synapse-constants/src/constants/bridgeMap.ts b/packages/synapse-constants/src/constants/bridgeMap.ts index bc84ea0073..dda798b68c 100644 --- a/packages/synapse-constants/src/constants/bridgeMap.ts +++ b/packages/synapse-constants/src/constants/bridgeMap.ts @@ -28,13 +28,6 @@ export const BRIDGE_MAP = { destination: ['JEWEL'], swappable: [], }, - '0x163f8C2467924be0ae7B5347228CABF260318753': { - decimals: 18, - symbol: 'WLD', - origin: ['RFQ.WLD'], - destination: ['RFQ.WLD'], - swappable: [], - }, '0x1B84765dE8B7566e4cEAF4D0fD3c5aF52D3DdE4F': { decimals: 18, symbol: 'nUSD', @@ -462,13 +455,6 @@ export const BRIDGE_MAP = { destination: ['L2DAO'], swappable: [], }, - '0xdC6fF44d5d932Cbd77B52E5612Ba0529DC6226F1': { - decimals: 18, - symbol: 'WLD', - origin: ['RFQ.WLD'], - destination: ['RFQ.WLD'], - swappable: [], - }, }, '25': { '0x396c9c192dd323995346632581BEF92a31AC623b': { @@ -522,13 +508,6 @@ export const BRIDGE_MAP = { destination: ['JUMP'], swappable: [], }, - '0x2170Ed0880ac9A755fd29B2688956BD959F933F8': { - decimals: 18, - symbol: 'ETH', - origin: ['RFQ.ETH'], - destination: [], - swappable: [], - }, '0x23b891e5C62E0955ae2bD185990103928Ab817b3': { decimals: 18, symbol: 'nUSD', @@ -576,6 +555,13 @@ export const BRIDGE_MAP = { '0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56', ], }, + '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE': { + decimals: 18, + symbol: 'BNB', + origin: ['RFQ.BNB'], + destination: [], + swappable: [], + }, '0xa4080f1778e69467E905B8d6F72f6e441f9e9484': { decimals: 18, symbol: 'SYN', @@ -590,6 +576,13 @@ export const BRIDGE_MAP = { destination: ['DOG'], swappable: [], }, + '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c': { + decimals: 18, + symbol: 'WBNB', + origin: ['RFQ.BNB'], + destination: [], + swappable: [], + }, '0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56': { decimals: 18, symbol: 'BUSD', @@ -894,11 +887,11 @@ export const BRIDGE_MAP = { }, }, '480': { - '0x2cFc85d8E48F8EAB294be644d9E25C3030863003': { + '0x4200000000000000000000000000000000000006': { decimals: 18, - symbol: 'WLD', - origin: ['RFQ.WLD'], - destination: ['RFQ.WLD'], + symbol: 'WETH', + origin: ['RFQ.ETH'], + destination: [], swappable: [], }, '0x79A02482A880bCE3F13e09Da970dC34db4CD24d1': { @@ -912,7 +905,7 @@ export const BRIDGE_MAP = { decimals: 18, symbol: 'ETH', origin: ['RFQ.ETH'], - destination: ['RFQ.ETH'], + destination: [], swappable: [], }, }, diff --git a/packages/synapse-constants/src/constants/chains/master.ts b/packages/synapse-constants/src/constants/chains/master.ts index b8201f939c..26c796de62 100644 --- a/packages/synapse-constants/src/constants/chains/master.ts +++ b/packages/synapse-constants/src/constants/chains/master.ts @@ -192,7 +192,7 @@ export const KLAYTN: Chain = { codeName: 'klaytn', blockTime: 1000, rpcUrls: { - primary: 'https://klaytn.blockpi.network/v1/rpc/public', + primary: 'https://kaia.blockpi.network/v1/rpc/public', fallback: 'https://klaytn.api.onfinality.io/public', }, nativeCurrency: { name: 'Klaytn', symbol: 'KLAY', decimals: 18 }, diff --git a/packages/synapse-constants/src/constants/tokens/bridgeable.ts b/packages/synapse-constants/src/constants/tokens/bridgeable.ts index bd1ab4c805..a7af1ec79f 100644 --- a/packages/synapse-constants/src/constants/tokens/bridgeable.ts +++ b/packages/synapse-constants/src/constants/tokens/bridgeable.ts @@ -1295,23 +1295,3 @@ export const SPECTRAL = new Token({ color: 'blue', routeSymbol: 'SPEC', }) - -export const WLD = new Token({ - priorityRank: 106, - addresses: { - [CHAINS.ETH.id]: '0x163f8c2467924be0ae7b5347228cabf260318753', - [CHAINS.OPTIMISM.id]: '0xdC6fF44d5d932Cbd77B52E5612Ba0529DC6226F1', - [CHAINS.WORLDCHAIN.id]: '0x2cFc85d8E48F8EAB294be644d9E25C3030863003', - }, - decimals: { - [CHAINS.ETH.id]: 18, - [CHAINS.OPTIMISM.id]: 18, - [CHAINS.WORLDCHAIN.id]: 18, - }, - symbol: 'WLD', - name: 'Worldcoin', - logo: 'https://synapse-interface-worldchain.sanguine-fe.pages.dev/_next/static/media/worldchain.62d1dfd2.svg', - swapableType: 'WLD', - color: 'gray', - routeSymbol: 'WLD', -}) diff --git a/packages/synapse-interface/CHANGELOG.md b/packages/synapse-interface/CHANGELOG.md index e6a26db06c..d68d4a38b3 100644 --- a/packages/synapse-interface/CHANGELOG.md +++ b/packages/synapse-interface/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.40.23](https://github.com/synapsecns/sanguine/compare/@synapsecns/synapse-interface@0.40.22...@synapsecns/synapse-interface@0.40.23) (2024-12-06) + +**Note:** Version bump only for package @synapsecns/synapse-interface + + + + + +## [0.40.22](https://github.com/synapsecns/sanguine/compare/@synapsecns/synapse-interface@0.40.21...@synapsecns/synapse-interface@0.40.22) (2024-12-02) + + +### Bug Fixes + +* update klaytn/kaia rpc url ([#3426](https://github.com/synapsecns/sanguine/issues/3426)) ([590594a](https://github.com/synapsecns/sanguine/commit/590594a80d576ea916052f25e22220823623e088)) + + + + + +## [0.40.21](https://github.com/synapsecns/sanguine/compare/@synapsecns/synapse-interface@0.40.20...@synapsecns/synapse-interface@0.40.21) (2024-12-01) + +**Note:** Version bump only for package @synapsecns/synapse-interface + + + + + +## [0.40.20](https://github.com/synapsecns/sanguine/compare/@synapsecns/synapse-interface@0.40.19...@synapsecns/synapse-interface@0.40.20) (2024-11-30) + +**Note:** Version bump only for package @synapsecns/synapse-interface + + + + + +## [0.40.19](https://github.com/synapsecns/sanguine/compare/@synapsecns/synapse-interface@0.40.18...@synapsecns/synapse-interface@0.40.19) (2024-11-19) + +**Note:** Version bump only for package @synapsecns/synapse-interface + + + + + ## [0.40.18](https://github.com/synapsecns/sanguine/compare/@synapsecns/synapse-interface@0.40.17...@synapsecns/synapse-interface@0.40.18) (2024-11-12) **Note:** Version bump only for package @synapsecns/synapse-interface diff --git a/packages/synapse-interface/assets/icons/wld.svg b/packages/synapse-interface/assets/icons/wld.svg deleted file mode 100644 index 126d25174e..0000000000 --- a/packages/synapse-interface/assets/icons/wld.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/packages/synapse-interface/components/layouts/LandingPageWrapper/index.tsx b/packages/synapse-interface/components/layouts/LandingPageWrapper/index.tsx index b8b2ec294c..aef82543a5 100644 --- a/packages/synapse-interface/components/layouts/LandingPageWrapper/index.tsx +++ b/packages/synapse-interface/components/layouts/LandingPageWrapper/index.tsx @@ -211,9 +211,9 @@ function MoreInfoButtons() { description="See preliminary analytics of the bridge" /> ) diff --git a/packages/synapse-interface/components/toast/ToastContent.tsx b/packages/synapse-interface/components/toast/ToastContent.tsx index 361cfee462..3d138fddd1 100644 --- a/packages/synapse-interface/components/toast/ToastContent.tsx +++ b/packages/synapse-interface/components/toast/ToastContent.tsx @@ -1,3 +1,4 @@ +import React from 'react' import toast from 'react-hot-toast' import { XIcon, diff --git a/packages/synapse-interface/components/toast/index.tsx b/packages/synapse-interface/components/toast/index.tsx index 0ef7675d2c..83d4bde824 100644 --- a/packages/synapse-interface/components/toast/index.tsx +++ b/packages/synapse-interface/components/toast/index.tsx @@ -1,7 +1,9 @@ +// @ts-nocheck +import React from 'react' import toast, { Toaster, ToastBar } from 'react-hot-toast' import ToastContent from './ToastContent' -export default function CustomToaster() { +const CustomToaster: React.FC = () => { return ( ) } + +export default CustomToaster diff --git a/packages/synapse-interface/constants/bridgeMap.ts b/packages/synapse-interface/constants/bridgeMap.ts index bc84ea0073..dda798b68c 100644 --- a/packages/synapse-interface/constants/bridgeMap.ts +++ b/packages/synapse-interface/constants/bridgeMap.ts @@ -28,13 +28,6 @@ export const BRIDGE_MAP = { destination: ['JEWEL'], swappable: [], }, - '0x163f8C2467924be0ae7B5347228CABF260318753': { - decimals: 18, - symbol: 'WLD', - origin: ['RFQ.WLD'], - destination: ['RFQ.WLD'], - swappable: [], - }, '0x1B84765dE8B7566e4cEAF4D0fD3c5aF52D3DdE4F': { decimals: 18, symbol: 'nUSD', @@ -462,13 +455,6 @@ export const BRIDGE_MAP = { destination: ['L2DAO'], swappable: [], }, - '0xdC6fF44d5d932Cbd77B52E5612Ba0529DC6226F1': { - decimals: 18, - symbol: 'WLD', - origin: ['RFQ.WLD'], - destination: ['RFQ.WLD'], - swappable: [], - }, }, '25': { '0x396c9c192dd323995346632581BEF92a31AC623b': { @@ -522,13 +508,6 @@ export const BRIDGE_MAP = { destination: ['JUMP'], swappable: [], }, - '0x2170Ed0880ac9A755fd29B2688956BD959F933F8': { - decimals: 18, - symbol: 'ETH', - origin: ['RFQ.ETH'], - destination: [], - swappable: [], - }, '0x23b891e5C62E0955ae2bD185990103928Ab817b3': { decimals: 18, symbol: 'nUSD', @@ -576,6 +555,13 @@ export const BRIDGE_MAP = { '0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56', ], }, + '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE': { + decimals: 18, + symbol: 'BNB', + origin: ['RFQ.BNB'], + destination: [], + swappable: [], + }, '0xa4080f1778e69467E905B8d6F72f6e441f9e9484': { decimals: 18, symbol: 'SYN', @@ -590,6 +576,13 @@ export const BRIDGE_MAP = { destination: ['DOG'], swappable: [], }, + '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c': { + decimals: 18, + symbol: 'WBNB', + origin: ['RFQ.BNB'], + destination: [], + swappable: [], + }, '0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56': { decimals: 18, symbol: 'BUSD', @@ -894,11 +887,11 @@ export const BRIDGE_MAP = { }, }, '480': { - '0x2cFc85d8E48F8EAB294be644d9E25C3030863003': { + '0x4200000000000000000000000000000000000006': { decimals: 18, - symbol: 'WLD', - origin: ['RFQ.WLD'], - destination: ['RFQ.WLD'], + symbol: 'WETH', + origin: ['RFQ.ETH'], + destination: [], swappable: [], }, '0x79A02482A880bCE3F13e09Da970dC34db4CD24d1': { @@ -912,7 +905,7 @@ export const BRIDGE_MAP = { decimals: 18, symbol: 'ETH', origin: ['RFQ.ETH'], - destination: ['RFQ.ETH'], + destination: [], swappable: [], }, }, diff --git a/packages/synapse-interface/constants/chains/master.tsx b/packages/synapse-interface/constants/chains/master.tsx index 6171a25443..ad44fa27a6 100644 --- a/packages/synapse-interface/constants/chains/master.tsx +++ b/packages/synapse-interface/constants/chains/master.tsx @@ -247,7 +247,7 @@ export const KLAYTN: Chain = { layer: 1, blockTime: 1000, rpcUrls: { - primary: 'https://klaytn.blockpi.network/v1/rpc/public', + primary: 'https://kaia.blockpi.network/v1/rpc/public', fallback: 'https://internal.klaytn.rpc.defikingdoms.com/api=654302102', }, nativeCurrency: { diff --git a/packages/synapse-interface/constants/routes.ts b/packages/synapse-interface/constants/routes.ts index b33db9f19e..1381e2a87c 100644 --- a/packages/synapse-interface/constants/routes.ts +++ b/packages/synapse-interface/constants/routes.ts @@ -6,8 +6,8 @@ import { POOL_PATH, LANDING_PATH, BRIDGE_PATH, - INTERCHAIN_LINK, SOLANA_BRIDGE_LINK, + SYN_TOKEN_LINK, } from './urls' export interface RouteObject { @@ -53,9 +53,9 @@ export const NAVIGATION: RouteObject = { text: 'Explorer', match: null, }, - Contracts: { - path: INTERCHAIN_LINK, - text: 'Interchain Network', + SYN: { + path: SYN_TOKEN_LINK, + text: '$SYN', match: null, }, Solana: { diff --git a/packages/synapse-interface/constants/tokens/bridgeable.ts b/packages/synapse-interface/constants/tokens/bridgeable.ts index be6bbc5dff..7c0d4e327d 100644 --- a/packages/synapse-interface/constants/tokens/bridgeable.ts +++ b/packages/synapse-interface/constants/tokens/bridgeable.ts @@ -42,7 +42,6 @@ import usdtLogo from '@assets/icons/usdt.svg' import vstaLogo from '@assets/icons/vsta.svg' import wbtcLogo from '@assets/icons/wbtc.svg' import wethLogo from '@assets/icons/weth.svg' -import wldLogo from '@assets/icons/wld.svg' import { Token } from '@/utils/types' import * as CHAINS from '@/constants/chains/master' @@ -1249,24 +1248,3 @@ export const METIS = new Token({ color: 'blue', routeSymbol: 'Metis', }) - -export const WLD = new Token({ - visibilityRank: 106, - addresses: { - [CHAINS.ETH.id]: '0x163f8C2467924be0ae7B5347228CABF260318753', - [CHAINS.OPTIMISM.id]: '0xdC6fF44d5d932Cbd77B52E5612Ba0529DC6226F1', - [CHAINS.WORLDCHAIN.id]: '0x2cFc85d8E48F8EAB294be644d9E25C3030863003', - }, - decimals: { - [CHAINS.ETH.id]: 18, - [CHAINS.OPTIMISM.id]: 18, - [CHAINS.WORLDCHAIN.id]: 18, - }, - symbol: 'WLD', - name: 'Worldcoin', - logo: wldLogo, - swapableType: 'WLD', - color: 'gray', - priorityRank: 106, - routeSymbol: 'WLD', -}) diff --git a/packages/synapse-interface/constants/urls/index.tsx b/packages/synapse-interface/constants/urls/index.tsx index a3cac309ad..5c1c5df241 100644 --- a/packages/synapse-interface/constants/urls/index.tsx +++ b/packages/synapse-interface/constants/urls/index.tsx @@ -25,6 +25,7 @@ export const LANDING_PATH = '/landing' export const EXPLORER_KAPPA = 'https://explorer.synapseprotocol.com/tx/' export const EXPLORER_PATH = 'https://explorer.synapseprotocol.com/' export const INTERCHAIN_LINK = 'https://interchain.synapseprotocol.com/' +export const SYN_TOKEN_LINK = 'https://docs.synapseprotocol.com/docs/About/SYN' export const SOLANA_BRIDGE_LINK = 'https://solana.synapseprotocol.com/' export const TERMS_OF_SERVICE_PATH = 'https://explorer.synapseprotocol.com/terms' diff --git a/packages/synapse-interface/messages/ar.json b/packages/synapse-interface/messages/ar.json index 33849d6878..09f07631d4 100644 --- a/packages/synapse-interface/messages/ar.json +++ b/packages/synapse-interface/messages/ar.json @@ -344,6 +344,7 @@ "Telegram": "تليجرام", "Functions": "الوظائف", "Developers": "المطورون", + "$SYN": "$SYN", "Support": "الدعم" }, "ReturnToMonke": { diff --git a/packages/synapse-interface/messages/en-US.json b/packages/synapse-interface/messages/en-US.json index ce150f5e79..8452406b49 100644 --- a/packages/synapse-interface/messages/en-US.json +++ b/packages/synapse-interface/messages/en-US.json @@ -344,6 +344,7 @@ "Telegram": "Telegram", "Functions": "Functions", "Developers": "Developers", + "$SYN": "$SYN", "Support": "Support" }, "ReturnToMonke": { diff --git a/packages/synapse-interface/messages/es.json b/packages/synapse-interface/messages/es.json index d35b19a8e8..5c121dfbe6 100644 --- a/packages/synapse-interface/messages/es.json +++ b/packages/synapse-interface/messages/es.json @@ -344,6 +344,7 @@ "Telegram": "Telegram", "Functions": "Funciones", "Developers": "Desarrolladores", + "$SYN": "$SYN", "Support": "Soporte" }, "ReturnToMonke": { diff --git a/packages/synapse-interface/messages/fr.json b/packages/synapse-interface/messages/fr.json index 3298973c04..5f4608d38e 100644 --- a/packages/synapse-interface/messages/fr.json +++ b/packages/synapse-interface/messages/fr.json @@ -344,6 +344,7 @@ "Telegram": "Telegram", "Functions": "Fonctions", "Developers": "Développeurs", + "$SYN": "$SYN", "Support": "Support" }, "ReturnToMonke": { diff --git a/packages/synapse-interface/messages/jp.json b/packages/synapse-interface/messages/jp.json index 8dff093e1d..ee05d2a385 100644 --- a/packages/synapse-interface/messages/jp.json +++ b/packages/synapse-interface/messages/jp.json @@ -344,6 +344,7 @@ "Telegram": "Telegram", "Functions": "機能", "Developers": "開発者", + "$SYN": "$SYN", "Support": "サポート" }, "ReturnToMonke": { diff --git a/packages/synapse-interface/messages/tr.json b/packages/synapse-interface/messages/tr.json index a6ac858168..d51a91667a 100644 --- a/packages/synapse-interface/messages/tr.json +++ b/packages/synapse-interface/messages/tr.json @@ -344,6 +344,7 @@ "Telegram": "Telegram", "Functions": "Fonksiyonlar", "Developers": "Geliştiriciler", + "$SYN": "$SYN", "Support": "Destek" }, "ReturnToMonke": { diff --git a/packages/synapse-interface/messages/zh-CN.json b/packages/synapse-interface/messages/zh-CN.json index fc4d1e5d93..d6a864354b 100644 --- a/packages/synapse-interface/messages/zh-CN.json +++ b/packages/synapse-interface/messages/zh-CN.json @@ -344,6 +344,7 @@ "Telegram": "Telegram", "Functions": "功能", "Developers": "开发者", + "$SYN": "$SYN", "Support": "支持" }, "ReturnToMonke": { diff --git a/packages/synapse-interface/package.json b/packages/synapse-interface/package.json index 61f986a812..d2e2ddb88e 100644 --- a/packages/synapse-interface/package.json +++ b/packages/synapse-interface/package.json @@ -1,6 +1,6 @@ { "name": "@synapsecns/synapse-interface", - "version": "0.40.18", + "version": "0.40.23", "private": true, "engines": { "node": ">=18.18.0" @@ -40,7 +40,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.6", + "@synapsecns/sdk-router": "^0.11.8", "@tailwindcss/aspect-ratio": "^0.4.2", "@tailwindcss/forms": "^0.5.3", "@tailwindcss/typography": "^0.5.9", diff --git a/packages/synapse-interface/pages/_app.tsx b/packages/synapse-interface/pages/_app.tsx index 74e50e8d27..83acb1d8e5 100644 --- a/packages/synapse-interface/pages/_app.tsx +++ b/packages/synapse-interface/pages/_app.tsx @@ -52,6 +52,7 @@ function App({ Component, pageProps }: AppProps) { timeZone="UTC" messages={pageProps.messages} > + {/* @ts-ignore */}