Skip to content

Commit 03105ba

Browse files
committed
evm: on-chain quoter
1 parent 3e24f77 commit 03105ba

File tree

5 files changed

+361
-0
lines changed

5 files changed

+361
-0
lines changed

evm/src/ExecutorQuoter.sol

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
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+
function quoteUpdate(QuoteUpdate[] calldata updates) public {
81+
if (msg.sender != updaterAddress) {
82+
revert InvalidUpdater(msg.sender, updaterAddress);
83+
}
84+
uint256 updatesLength = updates.length;
85+
for (uint256 i = 0; i < updatesLength;) {
86+
QuoteUpdate memory update = updates[i];
87+
quoteByDstChain[update.chainId] = update.quote;
88+
unchecked {
89+
i += 1;
90+
}
91+
}
92+
}
93+
94+
function normalize(uint256 amount, uint8 from, uint8 to) internal pure returns (uint256) {
95+
if (from > to) {
96+
return amount / 10 ** uint256(from - to);
97+
} else if (from < to) {
98+
return amount * 10 ** uint256(to - from);
99+
}
100+
return amount;
101+
}
102+
103+
function mul(uint256 a, uint256 b, uint8 decimals) internal pure returns (uint256) {
104+
return (a * b) / 10 ** uint256(decimals);
105+
}
106+
107+
function div(uint256 a, uint256 b, uint8 decimals) internal pure returns (uint256) {
108+
return (a * 10 ** uint256(decimals)) / b;
109+
}
110+
111+
/// Calculates the total gas limit and total message value from a set of relay instructions.
112+
/// Each relay instruction can be either a `GasInstruction` or a `GasDropOffInstruction`.
113+
/// - `GasInstruction` contributes to both `gasLimit` and `msgValue`.
114+
/// - `GasDropOffInstruction` contributes only to `msgValue`.
115+
/// Throws If an unsupported instruction type is encountered.
116+
function totalGasLimitAndMsgValue(bytes calldata relayInstructions)
117+
internal
118+
pure
119+
returns (uint256 gasLimit, uint256 msgValue)
120+
{
121+
uint256 offset = 0;
122+
uint8 ixType;
123+
bool hasDropOff = false;
124+
uint256 relayInstructionsLength = relayInstructions.length;
125+
while (offset < relayInstructionsLength) {
126+
assembly {
127+
ixType := shr(248, calldataload(add(relayInstructions.offset, offset)))
128+
offset := add(offset, 1)
129+
}
130+
if (ixType == 1) {
131+
assembly {
132+
gasLimit := shr(128, calldataload(add(relayInstructions.offset, offset)))
133+
offset := add(offset, 16)
134+
msgValue := shr(128, calldataload(add(relayInstructions.offset, offset)))
135+
offset := add(offset, 16)
136+
}
137+
} else if (ixType == 2) {
138+
if (hasDropOff) {
139+
revert MoreThanOneDropOff();
140+
}
141+
hasDropOff = true;
142+
assembly {
143+
msgValue := shr(128, calldataload(add(relayInstructions.offset, offset)))
144+
offset := add(offset, 48)
145+
}
146+
} else {
147+
revert UnsupportedInstruction(ixType);
148+
}
149+
}
150+
}
151+
152+
function estimateQuote(
153+
OnChainQuoteBody storage quote,
154+
ChainDecimals storage dstChainDecimals,
155+
uint256 gasLimit,
156+
uint256 msgValue
157+
) internal view returns (uint256) {
158+
uint256 srcChainValueForBaseFee = normalize(quote.baseFee, QUOTE_DECIMALS, srcTokenDecimals);
159+
160+
uint256 nSrcPrice = normalize(quote.srcPrice, QUOTE_DECIMALS, DECIMAL_RESOLUTION);
161+
uint256 nDstPrice = normalize(quote.dstPrice, QUOTE_DECIMALS, DECIMAL_RESOLUTION);
162+
uint256 scaledConversion = div(nDstPrice, nSrcPrice, DECIMAL_RESOLUTION);
163+
164+
uint256 nGasLimitCost =
165+
normalize(gasLimit * quote.dstGasPrice, dstChainDecimals.gasPriceDecimals, DECIMAL_RESOLUTION);
166+
167+
uint256 srcChainValueForGasLimit =
168+
normalize(mul(nGasLimitCost, scaledConversion, DECIMAL_RESOLUTION), DECIMAL_RESOLUTION, srcTokenDecimals);
169+
170+
uint256 nMsgValue = normalize(msgValue, dstChainDecimals.nativeDecimals, DECIMAL_RESOLUTION);
171+
uint256 srcChainValueForMsgValue =
172+
normalize(mul(nMsgValue, scaledConversion, DECIMAL_RESOLUTION), DECIMAL_RESOLUTION, srcTokenDecimals);
173+
return srcChainValueForBaseFee + srcChainValueForGasLimit + srcChainValueForMsgValue;
174+
}
175+
176+
function requestQuote(
177+
uint16 dstChain,
178+
bytes32, //dstAddr,
179+
address, //refundAddr,
180+
bytes calldata, //requestBytes,
181+
bytes calldata relayInstructions
182+
) public view returns (bytes32, uint256) {
183+
ChainDecimals storage dstChainDecimals = decimalsByDstChain[dstChain];
184+
if (!dstChainDecimals.enabled) {
185+
revert ChainDisabled(dstChain);
186+
}
187+
OnChainQuoteBody storage quote = quoteByDstChain[dstChain];
188+
(uint256 gasLimit, uint256 msgValue) = totalGasLimitAndMsgValue(relayInstructions);
189+
// NOTE: this does not include any maxGasLimit or maxMsgValue checks
190+
uint256 requiredPayment = estimateQuote(quote, dstChainDecimals, gasLimit, msgValue);
191+
192+
return (payeeAddress, requiredPayment);
193+
}
194+
}

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 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 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)