|
| 1 | +# Executor On-Chain Quotes |
| 2 | + |
| 3 | +# Objective |
| 4 | + |
| 5 | +Some would-be Executor integrators may need on-chain quotes as they do not necessarily control the end-user flow or integration API to their contracts. This design proposes a solution which can allow for the on-chain resolution they require without sacrificing the original motivations of the Executor design. |
| 6 | + |
| 7 | +## Runtime Support |
| 8 | + |
| 9 | +- [x] [EVM](./evm/) |
| 10 | +- [ ] [SVM](./svm/) |
| 11 | + |
| 12 | +# **Background** |
| 13 | + |
| 14 | +The initial [Executor](../README.md) design requires passing a quote to the on-chain contract which was intended to be passed by the off-chain caller, fetched from an Executor service provider. This quote must comply with a specific [header format](../README.md#off-chain-quote) but may otherwise contain any data specified by the Relay Provider. It was also recommended to perform the gas calculation off-chain. All this was done in service of reducing operational costs and on-chain complexity. Notably, the quote contains an expiry - the Executor on-chain contract enforces that a quote can not be used after its expiry. |
| 15 | + |
| 16 | +This approach worked for use cases where the UI can be changed to accommodate the new requirements. However, some integrators are composing with other protocols with pre-established APIs. They require an approach which only relies on on-chain state. |
| 17 | + |
| 18 | +EVM integrators may be familiar with a common pattern used by other on-chain services of having one `view` function to quote and another `payable` function to execute. For example, [NTT](https://github.com/wormhole-foundation/native-token-transfers) requires this on-chain pattern in its [Transceiver](https://github.com/wormhole-foundation/native-token-transfers/blob/c4db04fcbee08dd474d40c9bc121dbb701b3a535/evm/src/interfaces/ITransceiver.sol#L49-L73) in order to split a `msg.value` across multiple Transceivers. |
| 19 | + |
| 20 | +# **Goals** |
| 21 | + |
| 22 | +- Provide a new mechanism for requesting execution on-chain that is compatible with the existing Executor contract and does not require additional parameters to be passed from off-chain. |
| 23 | +- Do not substantially increase the operational cost or complexity of operating an Executor Relay Provider. |
| 24 | +- Maintain compatibility with the existing Executor design. This includes the key principles of permissionlessness and immutability. |
| 25 | + |
| 26 | +# Non-Goals |
| 27 | + |
| 28 | +- Support the same on-chain API as another relaying service or a particular EIP. |
| 29 | +- Pricing mechanisms for generating quotes or appraising relay costs. |
| 30 | +- Supporting non native gas token payments. |
| 31 | + |
| 32 | +# **Overview** |
| 33 | + |
| 34 | +```mermaid |
| 35 | +sequenceDiagram |
| 36 | + actor OffChain |
| 37 | + OffChain->>Integrator: quote(...) |
| 38 | + Integrator->>ExecutorQuoterRouter: quote(quoterAddr, ...) |
| 39 | + ExecutorQuoterRouter->>ExecutorQuoterRouter: lookupQuoterImplementation |
| 40 | + ExecutorQuoterRouter->>ExecutorQuoter: quote(...) |
| 41 | + ExecutorQuoter->>ExecutorQuoter: custom logic |
| 42 | + ExecutorQuoter-->>ExecutorQuoterRouter: payee, value |
| 43 | + ExecutorQuoterRouter-->>Integrator: value |
| 44 | + Integrator-->>OffChain: value |
| 45 | + OffChain->>Integrator: execute{value}(...) |
| 46 | + Integrator->>ExecutorQuoterRouter: requestExecution{value}(quoterAddr, ...) |
| 47 | + ExecutorQuoterRouter->>ExecutorQuoterRouter: lookupQuoterImplementation |
| 48 | + ExecutorQuoterRouter->>ExecutorQuoter: quote(...) |
| 49 | + ExecutorQuoter->>ExecutorQuoter: custom logic |
| 50 | + ExecutorQuoter-->>ExecutorQuoterRouter: payee, value |
| 51 | + ExecutorQuoterRouter->>ExecutorQuoterRouter: buildQuote |
| 52 | + ExecutorQuoterRouter->>Executor: requestExecution{value} |
| 53 | + ExecutorQuoterRouter->>ExecutorQuoterRouter: emit OnChainQuote |
| 54 | +``` |
| 55 | + |
| 56 | +# Detailed Design |
| 57 | + |
| 58 | +The existing Executor contracts are immutable, handle payment in the native gas token, and require a standardized quote header. This design introduces and standardizes the minimum viable approach to form quotes on-chain, allow for permissionless quoter selection, and reuse the rest of the on- and off-chain tooling. |
| 59 | + |
| 60 | +## Technical Details |
| 61 | + |
| 62 | +### EVM |
| 63 | + |
| 64 | +On EVM, two new contracts will be introduced. |
| 65 | + |
| 66 | +1. **ExecutorQuoter** represents the on-chain quoting logic of a particular Quoter / Relay Provider. It may implement any logic desired by the Relay Provider as long as it adheres to this interface. It SHOULD be immutable. |
| 67 | + |
| 68 | +```solidity |
| 69 | +interface IExecutorQuoter { |
| 70 | + /// This method is used by on- or off-chain services which need to determine the cost of a relay |
| 71 | + /// It only returns the required cost (msg.value) |
| 72 | + /// It is explicitly marked view |
| 73 | + function requestQuote( |
| 74 | + uint16 dstChain, |
| 75 | + bytes32 dstAddr, |
| 76 | + address refundAddr, |
| 77 | + bytes calldata requestBytes, |
| 78 | + bytes calldata relayInstructions |
| 79 | + ) external view returns (uint256 requiredPayment); |
| 80 | + /// This method is used by an ExecutorQuoterRouter during the execution flow |
| 81 | + /// It returns the required cost (msg.value) in addition to the payee and EQ02 quote body |
| 82 | + /// It is explicitly NOT marked view in order to allow the quoter the flexibility to emit events or update state |
| 83 | + function requestExecutionQuote( |
| 84 | + uint16 dstChain, |
| 85 | + bytes32 dstAddr, |
| 86 | + address refundAddr, |
| 87 | + bytes calldata requestBytes, |
| 88 | + bytes calldata relayInstructions |
| 89 | + ) external returns (uint256 requiredPayment, bytes32 payeeAddress, bytes32 quoteBody); |
| 90 | +} |
| 91 | +``` |
| 92 | + |
| 93 | +2. **ExecutorQuoterRouter** replaces **Executor** as the entry-point for integrators. It MUST be immutable and non-administered / fully permissionless. This provides three critical functionalities. |
| 94 | + |
| 95 | + 1. `updateQuoterContract(bytes calldata gov)` allows a Quoter to set their `ExecutorQuoter` contract via signed governance (detailed below). This MUST |
| 96 | + 1. Verify the chain ID matches the Executor’s `ourChain`. |
| 97 | + 2. Verify the contract address is an EVM address. |
| 98 | + 3. Verify the sender matches the sender on the governance. |
| 99 | + 4. Verify the governance has not expired. |
| 100 | + 5. Verify the signature `ecrecover`s to the quoter address on the governance. |
| 101 | + 6. Assign the specified contract address to that quoter address. |
| 102 | + 7. Emit a `QuoterContractUpdate` event (on applicable runtimes, e.g. EVM). |
| 103 | + 2. `quoteExecution` allows an integrator to quote the cost of an execution for a given quoter in place of a signed quote. This MUST call `requestQuote` from that Quoter’s registered contract. |
| 104 | + 3. `requestExecution` allows an integrator to request execution via Executor providing a quoter address in place of a signed quote. This MUST |
| 105 | + 1. Call `requestExecutionQuote` from that Quoter’s registered contract. |
| 106 | + 2. Enforce the required payment. |
| 107 | + 3. Refund excess payment. |
| 108 | + 4. Request execution, forming a `EQ02` quote on-chain. |
| 109 | + 5. Emit an `OnChainQuote` event (on applicable runtimes, e.g. EVM). |
| 110 | + |
| 111 | +```solidity |
| 112 | +interface IExecutorQuoterRouter { |
| 113 | + event OnChainQuote(address implementation); |
| 114 | + event QuoterContractUpdate(address indexed quoterAddress, address implementation); |
| 115 | +
|
| 116 | + function quoteExecution( |
| 117 | + uint16 dstChain, |
| 118 | + bytes32 dstAddr, |
| 119 | + address refundAddr, |
| 120 | + address quoterAddr, |
| 121 | + bytes calldata requestBytes, |
| 122 | + bytes calldata relayInstructions |
| 123 | + ) external view returns (uint256); |
| 124 | +
|
| 125 | + function requestExecution( |
| 126 | + uint16 dstChain, |
| 127 | + bytes32 dstAddr, |
| 128 | + address refundAddr, |
| 129 | + address quoterAddr, |
| 130 | + bytes calldata requestBytes, |
| 131 | + bytes calldata relayInstructions |
| 132 | + ) external payable; |
| 133 | +} |
| 134 | +``` |
| 135 | + |
| 136 | +### SVM |
| 137 | + |
| 138 | +The SVM implementation should follow the requirements above relevant to the SVM Executor implementation. |
| 139 | + |
| 140 | +<aside> |
| 141 | +⚠️ TODO: The primary additional consideration is how to handle the accounts used for fetching the quote from an ExecutorQuoter in a standardized way. |
| 142 | +</aside> |
| 143 | + |
| 144 | +### Other |
| 145 | + |
| 146 | +Other platforms are not in-scope at this time, but similar designs should be achievable. |
| 147 | + |
| 148 | +## Protocol Integration |
| 149 | + |
| 150 | +Relay Providers will need to change their verification for Executor requests. If the prefix is [`EQ02`](#quote---version-2-eq02), they MUST check the following event to ensure it is an `OnChainQuote` emitted by the canonical `ExecutorQuoterRouter` on that chain in place of verifying the signature. |
| 151 | + |
| 152 | +Since the 32 byte body from `EQ01` is added, no additional changes will be required apart from the above. |
| 153 | + |
| 154 | +## **API / database schema** |
| 155 | + |
| 156 | +### Governance |
| 157 | + |
| 158 | +This design introduces a new concept of a Quoter’s on-chain governance. |
| 159 | + |
| 160 | +The governance includes a sender address and expiry time in order to prevent replay attacks in lieu of a nonce and hash storage. The intention being that a short enough expiry time along with a pre-designated submitter mitigates the event where a quoter could be rolled back to a previous implementation by replaying their governance even when two governance messages are generated in short succession. |
| 161 | + |
| 162 | +```solidity |
| 163 | +bytes4 prefix = "EG01"; // 4-byte prefix for this struct |
| 164 | +uint16 sourceChain; // Wormhole Chain ID |
| 165 | +address quoterAddress; // The public key of the quoter. Used to identify an execution provider. |
| 166 | +bytes32 contractAddress; // UniversalAddress the quote contract to assign. |
| 167 | +bytes32 senderAddress; // The public key of address expected to submit this governance. |
| 168 | +uint64 expiryTime; // The unix time, in seconds, after which this quote should no longer be considered valid for requesting an execution |
| 169 | +[65]byte signature // Quoter's signature of the previous bytes |
| 170 | +``` |
| 171 | + |
| 172 | +### Quote - Version 2 (EQ02) |
| 173 | + |
| 174 | +This introduces a new Quote version to the [Executor spec](../README.md#api--database-schema). It has the same body as `EQ01` sans signature. This is useful for parsing and validating off-chain. |
| 175 | + |
| 176 | +```solidity |
| 177 | +Header header // prefix = "EQ02" |
| 178 | +uint64 baseFee // The base fee, in sourceChain native currency, required by the quoter to perform an execution on the destination chain |
| 179 | +uint64 destinationGasPrice // The current gas price on the destination chain |
| 180 | +uint64 sourcePrice // The USD price, in 10^10, of the sourceChain native currency |
| 181 | +uint64 destinationPrice // The USD price, in 10^10, of the destinationChain native currency |
| 182 | +``` |
| 183 | + |
| 184 | +# **Caveats** |
| 185 | + |
| 186 | +Integrators MAY now choose to construct their relay instructions on-chain. They will need to manage how to handle challenging cross-chain situations, such as calculating the required rent on SVM or gas usage differences across different EVMs. |
| 187 | + |
| 188 | +Unlike the off-chain signed quote, there may be a price update for the on-chain quote between when the client code requests the quote and when the transaction is executed on the source chain. This may cause the transaction to fail if the price increased during that time period and a sufficient buffer was not added to the quote. |
| 189 | + |
| 190 | +# **Alternatives Considered** |
| 191 | + |
| 192 | +## Subscriptions |
| 193 | + |
| 194 | +A separate design where integrators pay for Executor costs via a subscription model was proposed, but this exposes a severe DoS risk where integrators incur arbitrary costs effectively controlled by end users if messaging is permissionless. Instead, this design maintains the costs with end users, keeping the risk equivalent. |
| 195 | + |
| 196 | +## ExecutorV2 |
| 197 | + |
| 198 | +It is possible to keep the same or similar interface as Executor in the ExecutorQuoterRouter contract and allow the client to toggle between on- or off- chain quotes based on the first 4 bytes of the quote passed. This is still possible to add an additional wrapper around in the future, though would involve another contract. It is not immediately clear if there are integrators that would desire such flexibility. |
| 199 | + |
| 200 | +## ExecutorQuoter ABI |
| 201 | + |
| 202 | +While it was not strictly necessary to return the quote body for on-chain execution purposes, it is useful for off-chain integrations to validate and display price information. In order to slightly reduce costs and allow the quoter contract to differentiate between the quote and execute paths, two different functions are used with different modifiers and return values. |
| 203 | + |
| 204 | +# **Security Considerations** |
| 205 | + |
| 206 | +The `ExecutorQuoterRouter` remains permissionless and any Quoter can freely register/update and implement their own `ExecutorQuoter` implementation. The only change in the trust assumption for the Relay Provider is that they previously only relied on a given chain’s RPC implementation and their RPC provider in regards to the request for execution event and amount paid. Now they may also trust the resulting quote and required payment, as it is not signed by their Quoter. |
| 207 | + |
| 208 | +The `ExecutorQuoterRouter`, plays a critical role in its emission of an event to ensure to off-chain services that the unsigned `EQ02` quote was formed by the designated Quoter’s registered `ExecutorQuoter` implementation. |
0 commit comments