Skip to content

Commit cd628e8

Browse files
committed
evm: on-chain quoter
1 parent 5fd5004 commit cd628e8

File tree

5 files changed

+367
-0
lines changed

5 files changed

+367
-0
lines changed

evm/src/ExecutorQuoter.sol

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
pragma solidity ^0.8.13;
3+
4+
import "./interfaces/IExecutorQuoter.sol";
5+
6+
string constant executorQuoterVersion = "Executor-Quoter-0.0.1";
7+
8+
contract ExecutorQuoter is IExecutorQuoter {
9+
string public constant EXECUTOR_QUOTER_VERSION = executorQuoterVersion;
10+
uint8 private constant QUOTE_DECIMALS = 10;
11+
uint8 private constant DECIMAL_RESOLUTION = 18;
12+
13+
address public immutable quoterAddress;
14+
address public immutable updaterAddress;
15+
uint8 public immutable srcTokenDecimals;
16+
bytes32 public immutable payeeAddress;
17+
18+
/// This is the same as an EQ01 quote body
19+
/// It fits into a single bytes32 storage slot
20+
struct OnChainQuoteBody {
21+
/// The base fee, in sourceChain native currency, required by the quoter to perform an execution on the destination chain
22+
uint64 baseFee;
23+
/// The current gas price on the destination chain
24+
uint64 dstGasPrice;
25+
/// The USD price, in 10^10, of the sourceChain native currency
26+
uint64 srcPrice;
27+
/// The USD price, in 10^10, of the destinationChain native currency
28+
uint64 dstPrice;
29+
}
30+
31+
struct ChainDecimals {
32+
bool enabled;
33+
uint8 gasPriceDecimals;
34+
uint8 nativeDecimals;
35+
}
36+
37+
struct QuoteUpdate {
38+
uint16 chainId;
39+
OnChainQuoteBody quote;
40+
}
41+
42+
struct DecimalsUpdate {
43+
uint16 chainId;
44+
ChainDecimals decimals;
45+
}
46+
47+
mapping(uint16 => OnChainQuoteBody) public quoteByDstChain;
48+
mapping(uint16 => ChainDecimals) public decimalsByDstChain;
49+
50+
/// @dev Selector 0x40788bb5.
51+
error InvalidUpdater(address sender, address expected);
52+
/// @dev Selector 0x4dc2c273.
53+
error ChainDisabled(uint16 chainId);
54+
/// @dev Selector 0x0d2e6713.
55+
error UnsupportedInstruction(uint8 ixType);
56+
/// @dev Selector 0x3a5a1720.
57+
error MoreThanOneDropOff();
58+
59+
constructor(address _quoterAddress, address _updaterAddress, uint8 _srcTokenDecimals, bytes32 _payeeAddress) {
60+
quoterAddress = _quoterAddress;
61+
updaterAddress = _updaterAddress;
62+
srcTokenDecimals = _srcTokenDecimals;
63+
payeeAddress = _payeeAddress;
64+
}
65+
66+
function decimalsUpdate(DecimalsUpdate[] calldata updates) public {
67+
if (msg.sender != updaterAddress) {
68+
revert InvalidUpdater(msg.sender, updaterAddress);
69+
}
70+
uint256 updatesLength = updates.length;
71+
for (uint256 i = 0; i < updatesLength;) {
72+
DecimalsUpdate memory update = updates[i];
73+
decimalsByDstChain[update.chainId] = update.decimals;
74+
unchecked {
75+
i += 1;
76+
}
77+
}
78+
}
79+
80+
// TODO: pack these updates instead to save l2 cost
81+
function quoteUpdate(QuoteUpdate[] calldata updates) public {
82+
if (msg.sender != updaterAddress) {
83+
revert InvalidUpdater(msg.sender, updaterAddress);
84+
}
85+
uint256 updatesLength = updates.length;
86+
for (uint256 i = 0; i < updatesLength;) {
87+
QuoteUpdate memory update = updates[i];
88+
quoteByDstChain[update.chainId] = update.quote;
89+
unchecked {
90+
i += 1;
91+
}
92+
}
93+
}
94+
95+
function normalize(uint256 amount, uint8 from, uint8 to) internal pure returns (uint256) {
96+
if (from > to) {
97+
return amount / 10 ** uint256(from - to);
98+
} else if (from < to) {
99+
return amount * 10 ** uint256(to - from);
100+
}
101+
return amount;
102+
}
103+
104+
function mul(uint256 a, uint256 b, uint8 decimals) internal pure returns (uint256) {
105+
return (a * b) / 10 ** uint256(decimals);
106+
}
107+
108+
function div(uint256 a, uint256 b, uint8 decimals) internal pure returns (uint256) {
109+
return (a * 10 ** uint256(decimals)) / b;
110+
}
111+
112+
/// Calculates the total gas limit and total message value from a set of relay instructions.
113+
/// Each relay instruction can be either a `GasInstruction` or a `GasDropOffInstruction`.
114+
/// - `GasInstruction` contributes to both `gasLimit` and `msgValue`.
115+
/// - `GasDropOffInstruction` contributes only to `msgValue`.
116+
/// Throws If an unsupported instruction type is encountered.
117+
function totalGasLimitAndMsgValue(bytes calldata relayInstructions)
118+
internal
119+
pure
120+
returns (uint256 gasLimit, uint256 msgValue)
121+
{
122+
uint256 offset = 0;
123+
uint8 ixType;
124+
uint128 ixGasLimit;
125+
uint128 ixMsgValue;
126+
bool hasDropOff = false;
127+
uint256 relayInstructionsLength = relayInstructions.length;
128+
while (offset < relayInstructionsLength) {
129+
assembly {
130+
ixType := shr(248, calldataload(add(relayInstructions.offset, offset)))
131+
offset := add(offset, 1)
132+
}
133+
if (ixType == 1) {
134+
assembly {
135+
ixGasLimit := shr(128, calldataload(add(relayInstructions.offset, offset)))
136+
offset := add(offset, 16)
137+
ixMsgValue := shr(128, calldataload(add(relayInstructions.offset, offset)))
138+
offset := add(offset, 16)
139+
}
140+
gasLimit = gasLimit + ixGasLimit;
141+
msgValue = msgValue + ixMsgValue;
142+
} else if (ixType == 2) {
143+
if (hasDropOff) {
144+
revert MoreThanOneDropOff();
145+
}
146+
hasDropOff = true;
147+
assembly {
148+
ixMsgValue := shr(128, calldataload(add(relayInstructions.offset, offset)))
149+
offset := add(offset, 48)
150+
}
151+
msgValue = msgValue + ixMsgValue;
152+
} else {
153+
revert UnsupportedInstruction(ixType);
154+
}
155+
}
156+
}
157+
158+
function estimateQuote(
159+
OnChainQuoteBody storage quote,
160+
ChainDecimals storage dstChainDecimals,
161+
uint256 gasLimit,
162+
uint256 msgValue
163+
) internal view returns (uint256) {
164+
uint256 srcChainValueForBaseFee = normalize(quote.baseFee, QUOTE_DECIMALS, srcTokenDecimals);
165+
166+
uint256 nSrcPrice = normalize(quote.srcPrice, QUOTE_DECIMALS, DECIMAL_RESOLUTION);
167+
uint256 nDstPrice = normalize(quote.dstPrice, QUOTE_DECIMALS, DECIMAL_RESOLUTION);
168+
uint256 scaledConversion = div(nDstPrice, nSrcPrice, DECIMAL_RESOLUTION);
169+
170+
uint256 nGasLimitCost =
171+
normalize(gasLimit * quote.dstGasPrice, dstChainDecimals.gasPriceDecimals, DECIMAL_RESOLUTION);
172+
173+
uint256 srcChainValueForGasLimit =
174+
normalize(mul(nGasLimitCost, scaledConversion, DECIMAL_RESOLUTION), DECIMAL_RESOLUTION, srcTokenDecimals);
175+
176+
uint256 nMsgValue = normalize(msgValue, dstChainDecimals.nativeDecimals, DECIMAL_RESOLUTION);
177+
uint256 srcChainValueForMsgValue =
178+
normalize(mul(nMsgValue, scaledConversion, DECIMAL_RESOLUTION), DECIMAL_RESOLUTION, srcTokenDecimals);
179+
return srcChainValueForBaseFee + srcChainValueForGasLimit + srcChainValueForMsgValue;
180+
}
181+
182+
function requestQuote(
183+
uint16 dstChain,
184+
bytes32, //dstAddr,
185+
address, //refundAddr,
186+
bytes calldata, //requestBytes,
187+
bytes calldata relayInstructions
188+
) public view returns (bytes32, uint256) {
189+
ChainDecimals storage dstChainDecimals = decimalsByDstChain[dstChain];
190+
if (!dstChainDecimals.enabled) {
191+
revert ChainDisabled(dstChain);
192+
}
193+
OnChainQuoteBody storage quote = quoteByDstChain[dstChain];
194+
(uint256 gasLimit, uint256 msgValue) = totalGasLimitAndMsgValue(relayInstructions);
195+
// NOTE: this does not include any maxGasLimit or maxMsgValue checks
196+
uint256 requiredPayment = estimateQuote(quote, dstChainDecimals, gasLimit, msgValue);
197+
198+
return (payeeAddress, requiredPayment);
199+
}
200+
}

evm/src/ExecutorQuoterRouter.sol

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
pragma solidity ^0.8.13;
3+
4+
import "./interfaces/IExecutor.sol";
5+
import "./interfaces/IExecutorQuoter.sol";
6+
import "./interfaces/IExecutorQuoterRouter.sol";
7+
8+
string constant executorQuoterRouterVersion = "Executor-Quote-Router-0.0.1";
9+
10+
contract ExecutorQuoterRouter is IExecutorQuoterRouter {
11+
string public constant EXECUTOR_QUOTER_ROUTER_VERSION = executorQuoterRouterVersion;
12+
bytes4 private constant QUOTE_PREFIX = "EQ02";
13+
bytes4 private constant GOVERNANCE_PREFIX = "EG01";
14+
uint64 private constant EXPIRY_TIME = type(uint64).max;
15+
16+
IExecutor public immutable executor;
17+
uint16 public immutable ourChain;
18+
19+
mapping(address => IExecutorQuoter) public quoterContract;
20+
21+
/// @notice Error when the payment is less than required.
22+
/// @dev Selector 0xf3ebc384.
23+
/// @param provided The msg.value.
24+
/// @param expected The required payment from the quoter.
25+
error Underpaid(uint256 provided, uint256 expected);
26+
/// @notice Error when the refund to the sender fails.
27+
/// @dev Selector 0x2645bdc2.
28+
/// @param refundAddr The refund address.
29+
error RefundFailed(address refundAddr);
30+
error ChainIdMismatch(uint16 govChain, uint16 ourChain);
31+
error InvalidSignature();
32+
error GovernanceExpired(uint64 expiryTime);
33+
error NotAnEvmAddress(bytes32);
34+
35+
constructor(address _executor) {
36+
executor = IExecutor(_executor);
37+
ourChain = executor.ourChain();
38+
}
39+
40+
function updateQuoterContract(bytes calldata gov) public {
41+
bytes4 prefix;
42+
uint16 chainId;
43+
uint160 quoter;
44+
address quoterAddr;
45+
bytes32 universalContractAddress;
46+
uint64 expiryTime;
47+
bytes32 r;
48+
bytes32 s;
49+
uint8 v;
50+
assembly {
51+
prefix := calldataload(gov.offset)
52+
chainId := shr(240, calldataload(add(gov.offset, 4)))
53+
quoter := shr(96, calldataload(add(gov.offset, 6)))
54+
universalContractAddress := calldataload(add(gov.offset, 26))
55+
expiryTime := shr(192, calldataload(add(gov.offset, 58)))
56+
r := calldataload(add(gov.offset, 66))
57+
s := calldataload(add(gov.offset, 98))
58+
v := shr(248, calldataload(add(gov.offset, 130)))
59+
}
60+
if (chainId != ourChain) {
61+
revert ChainIdMismatch(chainId, ourChain);
62+
}
63+
// Check if the higher 96 bits (left-most 12 bytes) are non-zero
64+
if (uint256(universalContractAddress) >> 160 != 0) {
65+
revert NotAnEvmAddress(universalContractAddress);
66+
}
67+
if (expiryTime <= block.timestamp) {
68+
revert GovernanceExpired(expiryTime);
69+
}
70+
quoterAddr = address(quoter);
71+
bytes32 hash = keccak256(gov[0:66]);
72+
address signer = ecrecover(hash, v, r, s);
73+
if (signer == address(0)) {
74+
revert InvalidSignature();
75+
}
76+
if (signer != quoterAddr) {
77+
revert InvalidSignature();
78+
}
79+
address contractAddress = address(uint160(uint256(universalContractAddress)));
80+
quoterContract[quoterAddr] = IExecutorQuoter(contractAddress);
81+
emit QuoterContractUpdate(quoterAddr, contractAddress);
82+
}
83+
84+
function quoteExecution(
85+
uint16 dstChain,
86+
bytes32 dstAddr,
87+
address refundAddr,
88+
address quoterAddr,
89+
bytes calldata requestBytes,
90+
bytes calldata relayInstructions
91+
) public view returns (uint256 requiredPayment) {
92+
(, requiredPayment) =
93+
quoterContract[quoterAddr].requestQuote(dstChain, dstAddr, refundAddr, requestBytes, relayInstructions);
94+
}
95+
96+
function requestExecution(
97+
uint16 dstChain,
98+
bytes32 dstAddr,
99+
address refundAddr,
100+
address quoterAddr,
101+
bytes calldata requestBytes,
102+
bytes calldata relayInstructions
103+
) public payable {
104+
IExecutorQuoter implementation = quoterContract[quoterAddr];
105+
(bytes32 payeeAddress, uint256 requiredPayment) =
106+
implementation.requestQuote(dstChain, dstAddr, refundAddr, requestBytes, relayInstructions);
107+
if (msg.value < requiredPayment) {
108+
revert Underpaid(msg.value, requiredPayment);
109+
}
110+
if (msg.value > requiredPayment) {
111+
(bool refundSuccessful,) = payable(refundAddr).call{value: msg.value - requiredPayment}("");
112+
if (!refundSuccessful) {
113+
revert RefundFailed(refundAddr);
114+
}
115+
}
116+
executor.requestExecution{value: requiredPayment}(
117+
dstChain,
118+
dstAddr,
119+
refundAddr,
120+
abi.encodePacked(QUOTE_PREFIX, quoterAddr, payeeAddress, ourChain, dstChain, EXPIRY_TIME),
121+
requestBytes,
122+
relayInstructions
123+
);
124+
// this must emit a message in this function in order to verify off-chain that this contract generated the quote
125+
// the implementation is the only data available in this context that is not available from the executor event
126+
emit OnChainQuote(address(implementation));
127+
}
128+
}

evm/src/interfaces/IExecutor.sol

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ interface IExecutor {
2222
bytes relayInstructions
2323
);
2424

25+
function ourChain() external returns (uint16);
26+
2527
function requestExecution(
2628
uint16 dstChain,
2729
bytes32 dstAddr,
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
pragma solidity ^0.8.19;
3+
4+
interface IExecutorQuoter {
5+
function requestQuote(
6+
uint16 dstChain,
7+
bytes32 dstAddr,
8+
address refundAddr,
9+
bytes calldata requestBytes,
10+
bytes calldata relayInstructions
11+
) external view returns (bytes32, uint256);
12+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
pragma solidity ^0.8.19;
3+
4+
interface IExecutorQuoterRouter {
5+
event OnChainQuote(address implementation);
6+
event QuoterContractUpdate(address indexed quoterAddress, address implementation);
7+
8+
function quoteExecution(
9+
uint16 dstChain,
10+
bytes32 dstAddr,
11+
address refundAddr,
12+
address quoterAddr,
13+
bytes calldata requestBytes,
14+
bytes calldata relayInstructions
15+
) external returns (uint256);
16+
17+
function requestExecution(
18+
uint16 dstChain,
19+
bytes32 dstAddr,
20+
address refundAddr,
21+
address quoterAddr,
22+
bytes calldata requestBytes,
23+
bytes calldata relayInstructions
24+
) external payable;
25+
}

0 commit comments

Comments
 (0)