Skip to content

Commit e775abc

Browse files
authored
evm: on-chain quoter (#26)
* ci: pin foundry * evm: on-chain quoter * evm: on-chain quoter deploy scripts * evm: quoter gas optimizations * evm: fix most new lints * evm: more test coverage * design: on-chain quotes * evm: EG01 add senderAddress * evm: test quoteExecution and requestExecution * evm: add EQ02 body and quote vs execution method * design: review feedback
1 parent 3e24f77 commit e775abc

18 files changed

+1052
-6
lines changed

.cspell/custom-dictionary.txt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,17 @@ hashv
66
idls
77
keccak
88
Linea
9+
Mezo
910
permissionless
1011
permissionlessly
12+
permissionlessness
1113
pubkey
1214
Pubkey
15+
Redoc
1316
Ruleset
1417
Rulesets
1518
runtimes
1619
Solana
1720
struct
1821
structs
1922
Unichain
20-
Redoc
21-
Mezo

.github/workflows/evm.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
- name: Install Foundry
2424
uses: foundry-rs/foundry-toolchain@v1
2525
with:
26-
version: nightly
26+
version: v1.3.6
2727

2828
- name: Show Forge version
2929
run: |

design/02_On_Chain_Quotes.md

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
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.

evm/foundry.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,10 @@ out = "out"
99
libs = ["lib"]
1010

1111
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
12+
13+
[lint]
14+
exclude_lints = [
15+
"asm-keccak256",
16+
"mixed-case-function",
17+
"unaliased-plain-import"
18+
]
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
pragma solidity ^0.8.19;
3+
4+
import {ExecutorQuoter, EXECUTOR_QUOTER_VERSION_STR} from "../src/ExecutorQuoter.sol";
5+
import "forge-std/Script.sol";
6+
7+
// DeployExecutorQuoter is a forge script to deploy the ExecutorQuoter contract. Use ./sh/deployExecutorQuoter.sh to invoke this.
8+
// e.g. anvil
9+
// EVM_CHAIN_ID=31337 MNEMONIC=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 ./sh/deployExecutorQuoter.sh
10+
// e.g. anvil --fork-url https://ethereum-rpc.publicnode.com
11+
// EVM_CHAIN_ID=1 MNEMONIC=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 ./sh/deployExecutorQuoter.sh
12+
contract DeployExecutorQuoter is Script {
13+
function test() public {} // Exclude this from coverage report.
14+
15+
function dryRun(address quoterAddress, address updaterAddress, uint8 srcTokenDecimals, bytes32 payeeAddress)
16+
public
17+
{
18+
_deploy(quoterAddress, updaterAddress, srcTokenDecimals, payeeAddress);
19+
}
20+
21+
function run(address quoterAddress, address updaterAddress, uint8 srcTokenDecimals, bytes32 payeeAddress)
22+
public
23+
returns (address deployedAddress)
24+
{
25+
vm.startBroadcast();
26+
(deployedAddress) = _deploy(quoterAddress, updaterAddress, srcTokenDecimals, payeeAddress);
27+
vm.stopBroadcast();
28+
}
29+
30+
function _deploy(address quoterAddress, address updaterAddress, uint8 srcTokenDecimals, bytes32 payeeAddress)
31+
internal
32+
returns (address deployedAddress)
33+
{
34+
bytes32 salt = keccak256(abi.encodePacked(EXECUTOR_QUOTER_VERSION_STR));
35+
ExecutorQuoter executorQuoter =
36+
new ExecutorQuoter{salt: salt}(quoterAddress, updaterAddress, srcTokenDecimals, payeeAddress);
37+
38+
return (address(executorQuoter));
39+
}
40+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
pragma solidity ^0.8.19;
3+
4+
import {ExecutorQuoterRouter, EXECUTOR_QUOTER_ROUTER_VERSION_STR} from "../src/ExecutorQuoterRouter.sol";
5+
import "forge-std/Script.sol";
6+
7+
// DeployExecutorQuoterRouter is a forge script to deploy the ExecutorQuoterRouter contract. Use ./sh/deployExecutorQuoterRouter.sh to invoke this.
8+
// e.g. anvil
9+
// EVM_CHAIN_ID=31337 MNEMONIC=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 ./sh/deployExecutorQuoterRouter.sh
10+
// e.g. anvil --fork-url https://ethereum-rpc.publicnode.com
11+
// EVM_CHAIN_ID=1 MNEMONIC=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 ./sh/deployExecutorQuoterRouter.sh
12+
contract DeployExecutorQuoterRouter is Script {
13+
function test() public {} // Exclude this from coverage report.
14+
15+
function dryRun(address executor) public {
16+
_deploy(executor);
17+
}
18+
19+
function run(address executor) public returns (address deployedAddress) {
20+
vm.startBroadcast();
21+
(deployedAddress) = _deploy(executor);
22+
vm.stopBroadcast();
23+
}
24+
25+
function _deploy(address executor) internal returns (address deployedAddress) {
26+
bytes32 salt = keccak256(abi.encodePacked(EXECUTOR_QUOTER_ROUTER_VERSION_STR));
27+
ExecutorQuoterRouter executorQuoterRouter = new ExecutorQuoterRouter{salt: salt}(executor);
28+
29+
return (address(executorQuoterRouter));
30+
}
31+
}

evm/sh/deployExecutorQuoter.sh

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#!/bin/bash
2+
3+
#
4+
# This script deploys the Executor contract.
5+
# Usage: RPC_URL= MNEMONIC= QUOTER= UPDATER= SRC_DECIMALS= PAYEE_ADDRESS= EVM_CHAIN_ID= ./sh/deployExecutorQuoter.sh
6+
# anvil: EVM_CHAIN_ID=31337 MNEMONIC=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 ./sh/deployExecutorQuoter.sh
7+
8+
if [ "${RPC_URL}X" == "X" ]; then
9+
RPC_URL=http://localhost:8545
10+
fi
11+
12+
if [ "${MNEMONIC}X" == "X" ]; then
13+
MNEMONIC=0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d
14+
fi
15+
16+
if [ "${EVM_CHAIN_ID}X" == "X" ]; then
17+
EVM_CHAIN_ID=1337
18+
fi
19+
20+
if [ -n "${CREATE2_ADDRESS}" ]; then
21+
CREATE2_FLAG="--create2-deployer ${CREATE2_ADDRESS}"
22+
echo "Using custom CREATE2 deployer at: ${CREATE2_ADDRESS}"
23+
else
24+
CREATE2_FLAG=""
25+
echo "Using default CREATE2 deployer"
26+
fi
27+
28+
forge script ./script/DeployExecutorQuoter.s.sol:DeployExecutorQuoter \
29+
--sig "run(address, address, uint8, bytes32)" $QUOTER $UPDATER $SRC_DECIMALS $PAYEE_ADDRESS \
30+
--rpc-url "$RPC_URL" \
31+
--private-key "$MNEMONIC" \
32+
$CREATE2_FLAG \
33+
--broadcast ${FORGE_ARGS}
34+
35+
returnInfo=$(cat ./broadcast/DeployExecutorQuoter.s.sol/$EVM_CHAIN_ID/run-latest.json)
36+
37+
DEPLOYED_ADDRESS=$(jq -r '.returns.deployedAddress.value' <<< "$returnInfo")
38+
echo "Deployed ExecutorQuoter address: $DEPLOYED_ADDRESS"
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#!/bin/bash
2+
3+
#
4+
# This script deploys the Executor contract.
5+
# Usage: RPC_URL= MNEMONIC= EXECUTOR= EVM_CHAIN_ID= ./sh/deployExecutorQuoterRouter.sh
6+
# anvil: EVM_CHAIN_ID=31337 MNEMONIC=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 ./sh/deployExecutorQuoterRouter.sh
7+
8+
if [ "${RPC_URL}X" == "X" ]; then
9+
RPC_URL=http://localhost:8545
10+
fi
11+
12+
if [ "${MNEMONIC}X" == "X" ]; then
13+
MNEMONIC=0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d
14+
fi
15+
16+
if [ "${EVM_CHAIN_ID}X" == "X" ]; then
17+
EVM_CHAIN_ID=1337
18+
fi
19+
20+
if [ -n "${CREATE2_ADDRESS}" ]; then
21+
CREATE2_FLAG="--create2-deployer ${CREATE2_ADDRESS}"
22+
echo "Using custom CREATE2 deployer at: ${CREATE2_ADDRESS}"
23+
else
24+
CREATE2_FLAG=""
25+
echo "Using default CREATE2 deployer"
26+
fi
27+
28+
forge script ./script/DeployExecutorQuoterRouter.s.sol:DeployExecutorQuoterRouter \
29+
--sig "run(address)" $EXECUTOR \
30+
--rpc-url "$RPC_URL" \
31+
--private-key "$MNEMONIC" \
32+
$CREATE2_FLAG \
33+
--broadcast ${FORGE_ARGS}
34+
35+
returnInfo=$(cat ./broadcast/DeployExecutorQuoterRouter.s.sol/$EVM_CHAIN_ID/run-latest.json)
36+
37+
DEPLOYED_ADDRESS=$(jq -r '.returns.deployedAddress.value' <<< "$returnInfo")
38+
echo "Deployed ExecutorQuoterRouter address: $DEPLOYED_ADDRESS"

0 commit comments

Comments
 (0)