@crossecute/protocol 0.0.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -1
- package/abi/Blake2b256.json +1 -0
- package/abi/Blake2bScheme.json +1 -0
- package/abi/ChainRegistry.json +1 -0
- package/abi/Commitment.json +1 -0
- package/abi/Conduits.json +1 -0
- package/abi/DivergentConduits.json +1 -0
- package/abi/DivergentTransceiver.json +1 -0
- package/abi/DivergentTransceiverExtension.json +1 -0
- package/abi/Envelope.json +1 -0
- package/abi/Erc7930.json +1 -0
- package/abi/Executor.json +1 -0
- package/abi/IBootstrapAllowlist.json +1 -0
- package/abi/ICancel.json +1 -0
- package/abi/IChainRegistryRefs.json +1 -0
- package/abi/ICommitFinalize.json +1 -0
- package/abi/ICommitmentScheme.json +1 -0
- package/abi/IConduitReceiverReport.json +1 -0
- package/abi/IConduitTransceiver.json +1 -0
- package/abi/IERC7786GatewaySource.json +1 -0
- package/abi/IERC7786Recipient.json +1 -0
- package/abi/IExecute.json +1 -0
- package/abi/ILzOftConduitFactory.json +1 -0
- package/abi/IProviderIdTable.json +1 -0
- package/abi/IProxy.json +1 -0
- package/abi/IReceiverInit.json +1 -0
- package/abi/IRefValidator.json +1 -0
- package/abi/IReportFloat.json +1 -0
- package/abi/ITransmitterInit.json +1 -0
- package/abi/IVmDeriver.json +1 -0
- package/abi/KeccakScheme.json +1 -0
- package/abi/Move.json +1 -0
- package/abi/MoveValidator.json +1 -0
- package/abi/OpStackTransmitterBase.json +1 -0
- package/abi/OutboundBase.json +1 -0
- package/abi/OwnableTransmitter.json +1 -0
- package/abi/ProviderAddress.json +1 -0
- package/abi/ProviderAttribute.json +1 -0
- package/abi/ProviderChainId.json +1 -0
- package/abi/ProviderReceiver.json +1 -0
- package/abi/ProviderTransceiver.json +1 -0
- package/abi/Proxy.json +1 -0
- package/abi/ProxyDeployer.json +1 -0
- package/abi/ProxyInitCode.json +1 -0
- package/abi/ReceiverBase.json +1 -0
- package/abi/Roles.json +1 -0
- package/abi/RolesEnumerable.json +1 -0
- package/abi/SchemeFold.json +1 -0
- package/abi/Sha256Scheme.json +1 -0
- package/abi/StarknetDerive.json +1 -0
- package/abi/StarknetValidator.json +1 -0
- package/abi/SuiDerive.json +1 -0
- package/abi/TransceiverBase.json +1 -0
- package/abi/TransceiverCore.json +1 -0
- package/abi/TransceiverExtension.json +1 -0
- package/abi/TransmitterBase.json +1 -0
- package/abi/Treasury.json +1 -0
- package/abi/TronConduits.json +1 -0
- package/abi/TronTransceiver.json +1 -0
- package/abi/TronTransceiverExtension.json +1 -0
- package/abi/VmDeriver.json +1 -0
- package/abi/ZkSyncConduits.json +1 -0
- package/abi/ZkSyncTransceiver.json +1 -0
- package/abi/ZkSyncTransceiverExtension.json +1 -0
- package/abi/index.d.ts +63 -0
- package/abi/index.js +63 -0
- package/contracts/addressing/ChainKey.sol +45 -0
- package/contracts/addressing/ChainType.sol +50 -0
- package/contracts/addressing/Erc7930.sol +209 -0
- package/contracts/addressing/Move.sol +212 -0
- package/contracts/conduit/Conduits.sol +43 -0
- package/contracts/conduit/Proxy.sol +144 -0
- package/contracts/conduit/ProxyDeployer.sol +42 -0
- package/contracts/conduit/ProxyInitCode.sol +47 -0
- package/contracts/derivation/AddressDerive.sol +326 -0
- package/contracts/derivation/BitcoinDerive.sol +33 -0
- package/contracts/derivation/Blake2b256.sol +122 -0
- package/contracts/derivation/IVmDeriver.sol +35 -0
- package/contracts/derivation/StarknetDerive.sol +93 -0
- package/contracts/derivation/SuiDerive.sol +102 -0
- package/contracts/derivation/VmDeriver.sol +291 -0
- package/contracts/messaging/Call.sol +43 -0
- package/contracts/messaging/Commitment.sol +131 -0
- package/contracts/messaging/DeliveryGas.sol +18 -0
- package/contracts/messaging/Envelope.sol +123 -0
- package/contracts/messaging/Executor.sol +52 -0
- package/contracts/messaging/IErc7786.sol +68 -0
- package/contracts/messaging/Payload.sol +90 -0
- package/contracts/messaging/Roles.sol +113 -0
- package/contracts/messaging/inbound/ReceiverBase.sol +359 -0
- package/contracts/messaging/outbound/OutboundBase.sol +221 -0
- package/contracts/messaging/outbound/OwnableTransmitter.sol +23 -0
- package/contracts/messaging/outbound/TransmitterBase.sol +604 -0
- package/contracts/messaging/transceiver/DivergentConduits.sol +92 -0
- package/contracts/messaging/transceiver/DivergentTransceiver.sol +97 -0
- package/contracts/messaging/transceiver/IBootstrapAllowlist.sol +8 -0
- package/contracts/messaging/transceiver/TransceiverBase.sol +541 -0
- package/contracts/messaging/transceiver/TransceiverCore.sol +307 -0
- package/contracts/messaging/transceiver/TransceiverExtension.sol +278 -0
- package/contracts/protocols/ProviderAddress.sol +32 -0
- package/contracts/protocols/ProviderAttribute.sol +65 -0
- package/contracts/protocols/ProviderChainId.sol +115 -0
- package/contracts/protocols/ProviderReceiver.sol +24 -0
- package/contracts/protocols/ProviderTransceiver.sol +48 -0
- package/contracts/protocols/layerzero/ILzOftConduitFactory.sol +10 -0
- package/contracts/protocols/op-stack/OpStackTransmitterBase.sol +11 -0
- package/contracts/registry/ChainRegistry.sol +667 -0
- package/contracts/registry/IChainRegistryRefs.sol +81 -0
- package/contracts/registry/ICommitmentScheme.sol +46 -0
- package/contracts/registry/IRefValidator.sol +13 -0
- package/contracts/registry/Provenance.sol +25 -0
- package/contracts/schemes/Blake2bScheme.sol +18 -0
- package/contracts/schemes/KeccakScheme.sol +19 -0
- package/contracts/schemes/Sha256Scheme.sol +15 -0
- package/contracts/treasury/Treasury.sol +76 -0
- package/contracts/validators/MoveValidator.sol +24 -0
- package/contracts/validators/StarknetValidator.sol +20 -0
- package/package.json +34 -3
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.0;
|
|
3
|
+
|
|
4
|
+
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
|
|
5
|
+
import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol";
|
|
6
|
+
import {EnumerableMap} from "@openzeppelin/contracts/utils/structs/EnumerableMap.sol";
|
|
7
|
+
import {Call} from "../Call.sol";
|
|
8
|
+
import {Commitment} from "../Commitment.sol";
|
|
9
|
+
import {Erc7930} from "../../addressing/Erc7930.sol";
|
|
10
|
+
import {Executor} from "../Executor.sol";
|
|
11
|
+
import {Payload} from "../Payload.sol";
|
|
12
|
+
import {RolesEnumerable} from "../Roles.sol";
|
|
13
|
+
import {IERC7786Recipient} from "../IErc7786.sol";
|
|
14
|
+
|
|
15
|
+
/// @notice Two-step execution: approve a hash now, supply the matching array later.
|
|
16
|
+
/// @dev `commit` is gated and `finalize` is not: only an array matching an approved hash does
|
|
17
|
+
/// anything, so whoever relays it is irrelevant.
|
|
18
|
+
interface ICommitFinalize {
|
|
19
|
+
function commit(bytes32 commitment) external returns (uint256 approvals);
|
|
20
|
+
function finalize(Call[] calldata calls) external;
|
|
21
|
+
function finalize(Call[][] calldata batches) external;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// @notice Withdrawing an approval.
|
|
25
|
+
/// @dev Gated on a receiver by its transmitter, or a payload it is already executing.
|
|
26
|
+
interface ICancel {
|
|
27
|
+
function cancel(bytes32 commitment) external;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// @notice One-step execution: run this array, no hash comparison.
|
|
31
|
+
/// @dev `finalize` is permissionless because it checks the array against an approved hash;
|
|
32
|
+
/// `execute` skips the hash, so it is gated on the caller instead. `Call[]` only: an EVM
|
|
33
|
+
/// receiver runs EVM calls, and `Commitment` hashes both element forms to one value.
|
|
34
|
+
interface IExecute {
|
|
35
|
+
function execute(Call[] calldata calls) external payable;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// @notice What a transceiver needs from the receiver it creates and reuses.
|
|
39
|
+
interface IReceiverInit is ICommitFinalize, ICancel, IExecute {
|
|
40
|
+
function initialize(address sourceTransmitter, bytes32 homeOrigin, Call[] calldata calls) external;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// @title ReceiverBase
|
|
44
|
+
/// @notice The destination-side half. One receiver per transmitter per destination, created
|
|
45
|
+
/// by the transceiver and reused for every payload that transmitter sends.
|
|
46
|
+
///
|
|
47
|
+
/// @dev Approvals are an unordered map of hash to count. A map, so a pending approval never
|
|
48
|
+
/// blocks recording the next; a count, since two identical payloads are two approvals;
|
|
49
|
+
/// unordered, so an unrelayed payload stalls nothing, and a relayer holding two arrays
|
|
50
|
+
/// chooses their order. `cancel` takes the hash, so it cannot go stale like an index.
|
|
51
|
+
///
|
|
52
|
+
/// @dev All state is storage, set in the single-shot `initialize`: immutables would live in
|
|
53
|
+
/// the implementation and be shared by every conduit.
|
|
54
|
+
///
|
|
55
|
+
/// @dev `Commitment.hashCalls` seeds with `ChainKey.local()`, so an array approved for one
|
|
56
|
+
/// chain cannot be finalized on another where the same conduit address exists.
|
|
57
|
+
///
|
|
58
|
+
/// @dev Uses OZ 5.4's `ReentrancyGuardUpgradeable`, whose state is ERC-7201 namespaced; 5.4's
|
|
59
|
+
/// plain `ReentrancyGuard` would take a sequential slot. `__ReceiverBase_init` starts it
|
|
60
|
+
/// before any payload runs. The guard covers `_onMessage`, both `finalize` overloads, and
|
|
61
|
+
/// `execute` under one lock.
|
|
62
|
+
abstract contract ReceiverBase is
|
|
63
|
+
Initializable,
|
|
64
|
+
Executor,
|
|
65
|
+
RolesEnumerable,
|
|
66
|
+
ReentrancyGuardUpgradeable,
|
|
67
|
+
IReceiverInit,
|
|
68
|
+
IERC7786Recipient
|
|
69
|
+
{
|
|
70
|
+
using EnumerableMap for EnumerableMap.Bytes32ToUintMap;
|
|
71
|
+
|
|
72
|
+
/// @notice The outstanding approvals: commitment => how many times it may still be
|
|
73
|
+
/// finalized.
|
|
74
|
+
/// @dev Enumerable, so what is outstanding can be read on-chain. Each `finalize`
|
|
75
|
+
/// decrements, and the entry leaves the map at zero.
|
|
76
|
+
EnumerableMap.Bytes32ToUintMap private _commitments;
|
|
77
|
+
|
|
78
|
+
/// The transmitter this receiver answers to. Set once, at initialization.
|
|
79
|
+
address public sourceTransmitter;
|
|
80
|
+
/// The transceiver that created this receiver. Not `transceiver`, which on
|
|
81
|
+
/// `TransmitterBase` means the other direction.
|
|
82
|
+
address public parentTransceiver;
|
|
83
|
+
/// The conduit's home as the transport names an origin: the provider's id for it, or its
|
|
84
|
+
/// chainKey over an ERC-7786 gateway. Set once, at initialization.
|
|
85
|
+
/// @dev The transmitter's address on another chain is not always this conduit's, and where
|
|
86
|
+
/// the provider's transceiver is not yet deployed its deployer can place code there; no
|
|
87
|
+
/// message is sent to such a chain until the registry marks the transceiver live.
|
|
88
|
+
bytes32 public homeOrigin;
|
|
89
|
+
|
|
90
|
+
event ReceiverInitialized(address indexed sourceTransmitter, address indexed transceiver);
|
|
91
|
+
event ReceiverExecuted(address indexed caller, uint256 callCount);
|
|
92
|
+
event Committed(bytes32 indexed commitment, uint256 outstanding);
|
|
93
|
+
event Finalized(bytes32 indexed commitment, uint256 remaining, uint256 callCount);
|
|
94
|
+
/// @dev A payload that ran on arrival, as distinct from one a commitment discharged or a
|
|
95
|
+
/// gated entry point drove locally.
|
|
96
|
+
event Delivered(uint256 callCount);
|
|
97
|
+
/// @dev Carries what was dropped, since cancelling removes every outstanding copy.
|
|
98
|
+
event Cancelled(bytes32 indexed commitment, uint256 dropped);
|
|
99
|
+
|
|
100
|
+
error NotSourceTransmitter();
|
|
101
|
+
error ZeroTransmitter();
|
|
102
|
+
error ZeroHomeOrigin();
|
|
103
|
+
/// @dev The message came from a chain other than this conduit's home.
|
|
104
|
+
error NotHomeOrigin(bytes32 origin);
|
|
105
|
+
/// @dev No approval matches the array supplied.
|
|
106
|
+
error CommitmentMismatch();
|
|
107
|
+
/// @dev Nothing outstanding under that hash. Refused rather than a no-op, since success
|
|
108
|
+
/// would suggest a payload was stopped that may already have run.
|
|
109
|
+
error NotCommitted(bytes32 commitment);
|
|
110
|
+
/// @dev Zero would make "committed" indistinguishable from "never committed".
|
|
111
|
+
error ZeroCommitment();
|
|
112
|
+
error EmptyBatch();
|
|
113
|
+
/// @dev The sender envelope's address half is not 20 bytes, so it cannot be an EVM
|
|
114
|
+
/// transmitter.
|
|
115
|
+
error UnauthenticatedSender(bytes sender);
|
|
116
|
+
|
|
117
|
+
/// @notice Whether `conduit` is the transmitter this receiver was created for.
|
|
118
|
+
function isSourceTransmitter(address conduit) public view returns (bool) {
|
|
119
|
+
return conduit != address(0) && conduit == sourceTransmitter;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/// @notice Who may drive this receiver: its transmitter, and a payload it is executing.
|
|
123
|
+
///
|
|
124
|
+
/// @dev Not the transceiver: after initialization it has no authority here, so one
|
|
125
|
+
/// compromised transceiver cannot drive every conduit it created.
|
|
126
|
+
///
|
|
127
|
+
/// @dev `address(this)` lets a deferred payload call this receiver's own `commit`. Only
|
|
128
|
+
/// `_execute` can produce it, reachable from an authenticated inbound message or a
|
|
129
|
+
/// gated entry point. Stated explicitly because receiver and transmitter addresses
|
|
130
|
+
/// differ on zkSync and Tron.
|
|
131
|
+
function isAuthorizedCaller(address conduit) public view returns (bool) {
|
|
132
|
+
return conduit == address(this) || isSourceTransmitter(conduit);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
modifier onlySourceTransmitter() {
|
|
136
|
+
if (!isAuthorizedCaller(msg.sender)) revert NotSourceTransmitter();
|
|
137
|
+
_;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* ================================ initializing ============================== */
|
|
141
|
+
|
|
142
|
+
/// @notice Bind this conduit to its transmitter and run the payload it was created for.
|
|
143
|
+
/// @dev For a binding that needs no setup of its own; one that does declares its own
|
|
144
|
+
/// `initialize` and calls `__ReceiverBase_init` last.
|
|
145
|
+
function initialize(address sourceTransmitter_, bytes32 homeOrigin_, Call[] calldata calls)
|
|
146
|
+
external
|
|
147
|
+
virtual
|
|
148
|
+
override
|
|
149
|
+
initializer
|
|
150
|
+
{
|
|
151
|
+
__ReceiverBase_init(sourceTransmitter_, homeOrigin_, calls);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/// @notice The work behind `initialize`: bind, then run the bootstrap calls.
|
|
155
|
+
///
|
|
156
|
+
/// @dev Runs the calls rather than recording a commitment; a payload that should wait
|
|
157
|
+
/// carries a self-call to `commit`. An empty array is allowed here, unlike in
|
|
158
|
+
/// `execute`, since creating the receiver is the intent.
|
|
159
|
+
///
|
|
160
|
+
/// @dev Separate so a binding can configure its provider before the payload runs, from
|
|
161
|
+
/// inside its own `initializer` and ahead of this call; `super.initialize` would run the
|
|
162
|
+
/// payload first. The transceiver's reach into a receiver ends here.
|
|
163
|
+
function __ReceiverBase_init(address sourceTransmitter_, bytes32 homeOrigin_, Call[] calldata calls)
|
|
164
|
+
internal
|
|
165
|
+
onlyInitializing
|
|
166
|
+
{
|
|
167
|
+
if (sourceTransmitter_ == address(0)) revert ZeroTransmitter();
|
|
168
|
+
if (homeOrigin_ == bytes32(0)) revert ZeroHomeOrigin();
|
|
169
|
+
|
|
170
|
+
// First, so the guard is live before the payload at the end of this function runs.
|
|
171
|
+
__ReentrancyGuard_init();
|
|
172
|
+
|
|
173
|
+
sourceTransmitter = sourceTransmitter_;
|
|
174
|
+
homeOrigin = homeOrigin_;
|
|
175
|
+
parentTransceiver = msg.sender;
|
|
176
|
+
emit ReceiverInitialized(sourceTransmitter_, msg.sender);
|
|
177
|
+
if (calls.length != 0) _execute(calls);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/// @notice Stop trusting a transport this receiver was armed with.
|
|
181
|
+
///
|
|
182
|
+
/// @dev The only membership change after initialization anywhere, and it only subtracts:
|
|
183
|
+
/// `grantRole` is `onlyInitializing`, and `Proxy` arms once, so a dropped
|
|
184
|
+
/// transport is never replaced. A gateway that can deliver can forge, so going deaf is
|
|
185
|
+
/// the recoverable failure.
|
|
186
|
+
///
|
|
187
|
+
/// @dev A transceiver has no equivalent: it is shared by every owner on its chain.
|
|
188
|
+
function revokeGateway(address gateway) external onlySourceTransmitter {
|
|
189
|
+
_revokeRole(GATEWAY_ROLE, gateway);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/* ================================== approving =============================== */
|
|
193
|
+
|
|
194
|
+
/// @notice Approve the hash of a call array, to be executed later by anyone.
|
|
195
|
+
/// @dev The same hash may be approved twice; each approval needs its own `finalize`.
|
|
196
|
+
/// @return approvals How many times this hash may now be finalized.
|
|
197
|
+
function commit(bytes32 commitment_) external override onlySourceTransmitter returns (uint256 approvals) {
|
|
198
|
+
if (commitment_ == bytes32(0)) revert ZeroCommitment();
|
|
199
|
+
|
|
200
|
+
(, uint256 held) = _commitments.tryGet(commitment_);
|
|
201
|
+
approvals = held + 1;
|
|
202
|
+
_commitments.set(commitment_, approvals);
|
|
203
|
+
emit Committed(commitment_, approvals);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/// @notice Withdraw an approval so it can never be finalized.
|
|
207
|
+
/// @dev Gated like `commit`, which already lets the caller approve any array. Drops every
|
|
208
|
+
/// copy: a wrong payload is wrong in every copy, and re-approving is one `commit`
|
|
209
|
+
/// away. An absent approval reverts, since success would suggest a payload was stopped
|
|
210
|
+
/// that may have run.
|
|
211
|
+
function cancel(bytes32 commitment_) external virtual override onlySourceTransmitter {
|
|
212
|
+
(bool held, uint256 dropped) = _commitments.tryGet(commitment_);
|
|
213
|
+
if (!held) revert NotCommitted(commitment_);
|
|
214
|
+
|
|
215
|
+
_commitments.remove(commitment_);
|
|
216
|
+
emit Cancelled(commitment_, dropped);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/* ================================= finalizing =============================== */
|
|
220
|
+
|
|
221
|
+
/// @notice Supply an approved array, and run it.
|
|
222
|
+
///
|
|
223
|
+
/// @dev Ungated: only an array matching an outstanding commitment does anything, which lets
|
|
224
|
+
/// a third party pay the gas. It discharges the approval the array hashes to.
|
|
225
|
+
///
|
|
226
|
+
/// @dev `Commitment` hashes `Call[]` and the equivalent opaque elements to one value, so an
|
|
227
|
+
/// approval made over `bytes[]` is discharged by the typed array.
|
|
228
|
+
function finalize(Call[] calldata calls) external override nonReentrant {
|
|
229
|
+
_finalize(calls);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/// @notice Discharge several approvals in one transaction, in the order given.
|
|
233
|
+
/// @dev All or nothing, since each entry is decremented before its payload runs.
|
|
234
|
+
function finalize(Call[][] calldata batches) external override nonReentrant {
|
|
235
|
+
uint256 n = batches.length;
|
|
236
|
+
if (n == 0) revert EmptyBatch();
|
|
237
|
+
for (uint256 i; i < n; ++i) {
|
|
238
|
+
_finalize(batches[i]);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/// @dev `Commitment.hashCalls` seeds with `ChainKey.local()`, so an array approved for
|
|
243
|
+
/// another chain hashes to a value this map does not hold. The decrement happens
|
|
244
|
+
/// before `_execute`, so a re-entrant `finalize` of the same array finds it spent.
|
|
245
|
+
function _finalize(Call[] calldata calls) private {
|
|
246
|
+
bytes32 pending = Commitment.hashCalls(calls);
|
|
247
|
+
|
|
248
|
+
(bool held, uint256 approvals) = _commitments.tryGet(pending);
|
|
249
|
+
if (!held) revert CommitmentMismatch();
|
|
250
|
+
|
|
251
|
+
uint256 remaining = approvals - 1;
|
|
252
|
+
if (remaining == 0) {
|
|
253
|
+
_commitments.remove(pending);
|
|
254
|
+
} else {
|
|
255
|
+
_commitments.set(pending, remaining);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
emit Finalized(pending, remaining, calls.length);
|
|
259
|
+
_execute(calls);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/// @notice Run these calls now, with no commitment and no hash comparison.
|
|
263
|
+
///
|
|
264
|
+
/// @dev Grants nothing `commit` does not: the transmitter can already approve any array
|
|
265
|
+
/// and let anyone finalize it. `commit`, `cancel`, and `execute` share one gate.
|
|
266
|
+
///
|
|
267
|
+
/// @dev Leaves pending commitments alone; each still needs its own `finalize`.
|
|
268
|
+
function execute(Call[] calldata calls) external payable virtual onlySourceTransmitter nonReentrant {
|
|
269
|
+
if (calls.length == 0) revert EmptyExecution();
|
|
270
|
+
emit ReceiverExecuted(msg.sender, calls.length);
|
|
271
|
+
_execute(calls);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/* ============================== approval reads ============================== */
|
|
275
|
+
|
|
276
|
+
/// @notice How many times `commitment_` may still be finalized. Zero means never.
|
|
277
|
+
function outstanding(bytes32 commitment_) public view returns (uint256) {
|
|
278
|
+
(, uint256 held) = _commitments.tryGet(commitment_);
|
|
279
|
+
return held;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/// @notice Whether `commitment_` has an approval left.
|
|
283
|
+
function isCommitted(bytes32 commitment_) public view returns (bool) {
|
|
284
|
+
return _commitments.contains(commitment_);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/// @notice Every outstanding approval.
|
|
288
|
+
/// @dev The order is not stable: discharging one swaps the last entry into its place.
|
|
289
|
+
function commitments() external view returns (bytes32[] memory hashes) {
|
|
290
|
+
uint256 n = _commitments.length();
|
|
291
|
+
hashes = new bytes32[](n);
|
|
292
|
+
for (uint256 i; i < n; ++i) {
|
|
293
|
+
(hashes[i],) = _commitments.at(i);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/// @notice How many distinct approvals are outstanding: a hash approved twice counts once
|
|
298
|
+
/// here and twice in `outstanding`.
|
|
299
|
+
function pendingCount() external view returns (uint256) {
|
|
300
|
+
return _commitments.length();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/* ================================== delivery ================================ */
|
|
304
|
+
|
|
305
|
+
/// @notice ERC-7786 delivery: the gateway hands over an authenticated message.
|
|
306
|
+
///
|
|
307
|
+
/// @dev Both checks are required on an `external` entry point: the gateway role says the
|
|
308
|
+
/// transport is trusted, and `_onMessageFrom` requires the sender to be this
|
|
309
|
+
/// receiver's own transmitter on its home, so a shared gateway cannot land one
|
|
310
|
+
/// conduit's payload in another's receiver. Compared against the stored
|
|
311
|
+
/// `sourceTransmitter`, not `address(this)`, mirroring
|
|
312
|
+
/// `TransmitterBase._requireOwnRecipient`.
|
|
313
|
+
///
|
|
314
|
+
/// @dev `receiveId` is ignored: nothing here correlates messages, and replay is the
|
|
315
|
+
/// transport's to prevent (`docs/provider-spec.md` R3.5).
|
|
316
|
+
function receiveMessage(bytes32, bytes calldata sender, bytes calldata payload)
|
|
317
|
+
external
|
|
318
|
+
payable
|
|
319
|
+
override
|
|
320
|
+
onlyRole(GATEWAY_ROLE)
|
|
321
|
+
returns (bytes4)
|
|
322
|
+
{
|
|
323
|
+
Erc7930.Interop memory io = Erc7930.parseStrict(sender);
|
|
324
|
+
if (io.addr.length != 20) revert UnauthenticatedSender(sender);
|
|
325
|
+
// forge-lint: disable-next-line(unsafe-typecast) length checked above
|
|
326
|
+
_onMessageFrom(Erc7930.chainKey(sender), address(bytes20(io.addr)), payload);
|
|
327
|
+
return IERC7786Recipient.receiveMessage.selector;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/// @notice Deliver `payload` if `origin` and `sender`, as the provider reported them, are
|
|
331
|
+
/// this conduit's home and its transmitter there.
|
|
332
|
+
/// @param origin The source chain in `homeOrigin`'s naming.
|
|
333
|
+
function _onMessageFrom(bytes32 origin, address sender, bytes calldata payload) internal {
|
|
334
|
+
if (origin != homeOrigin) revert NotHomeOrigin(origin);
|
|
335
|
+
if (!isSourceTransmitter(sender)) revert NotSourceTransmitter();
|
|
336
|
+
_onMessage(payload);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/// @notice Run a payload that arrived over the wire.
|
|
340
|
+
///
|
|
341
|
+
/// @dev Decoded here, not by the binding. It runs inside the delivery callback, so a
|
|
342
|
+
/// reverting payload fails the message, which every provider lets anyone re-execute.
|
|
343
|
+
/// A payload that should wait carries a self-call to `commit`; nothing on the wire
|
|
344
|
+
/// distinguishes it.
|
|
345
|
+
///
|
|
346
|
+
/// @dev `nonReentrant`, shared with `finalize`, since it calls arbitrary targets from a
|
|
347
|
+
/// provider callback.
|
|
348
|
+
function _onMessage(bytes calldata payload) internal nonReentrant {
|
|
349
|
+
Call[] memory calls = Payload.decodeCalls(payload);
|
|
350
|
+
emit Delivered(calls.length);
|
|
351
|
+
_execute(calls);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/// @notice Accept ETH, so a receiver can be funded ahead of a `finalize` that spends it.
|
|
355
|
+
/// @dev `finalize` is permissionless and carries no value, so a payload with value
|
|
356
|
+
/// finalized by a third party spends this balance. The address is known before the
|
|
357
|
+
/// receiver exists, so a shortfall is fixed by topping up and retrying.
|
|
358
|
+
receive() external payable {}
|
|
359
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.0;
|
|
3
|
+
|
|
4
|
+
import {Erc7930} from "../../addressing/Erc7930.sol";
|
|
5
|
+
import {ChainKey} from "../../addressing/ChainKey.sol";
|
|
6
|
+
|
|
7
|
+
/// @title OutboundBase
|
|
8
|
+
/// @notice The sending half: who this contract's counterpart is on each chain, how to
|
|
9
|
+
/// address it, and the two primitives that put a message on the wire.
|
|
10
|
+
///
|
|
11
|
+
/// @dev Split from `TransmitterBase` because a transceiver needs the mechanics but must not
|
|
12
|
+
/// have a conduit's owner. The setters are `internal` and ungated; each side wraps them
|
|
13
|
+
/// in its own authority (`onlyOwner` on a transceiver, `onlyConduitOwner` on a conduit).
|
|
14
|
+
///
|
|
15
|
+
/// @dev One table serves every sender: an ERC-7930 recipient is the route (chain) joined to
|
|
16
|
+
/// the counterpart (address). A transceiver's counterpart is the provider's transceiver
|
|
17
|
+
/// on that chain, and a conduit's is its own receiver, stored rather than assumed equal
|
|
18
|
+
/// to this address, which is wrong wherever the CREATE2 formula differs from Ethereum's.
|
|
19
|
+
///
|
|
20
|
+
/// @dev Leads the inheritance list on `TransmitterBase` and `TransceiverBase`, so its fields
|
|
21
|
+
/// take the first slots of both layouts (R8.2). `ReceiverBase` does not inherit it.
|
|
22
|
+
///
|
|
23
|
+
/// @dev Entry points validate their arguments and call `_sendMessage` directly. A gateway
|
|
24
|
+
/// source emits `MessageSent`; path B is not one, so `TransceiverBase` emits
|
|
25
|
+
/// `BootstrapSent` instead.
|
|
26
|
+
abstract contract OutboundBase {
|
|
27
|
+
/// chainKey => that chain's canonical ERC-7930 chain identifier.
|
|
28
|
+
/// @dev Held by the sender, not the registry: on the execute-on-arrival path nothing else
|
|
29
|
+
/// binds the destination, so a registry that could misroute could run a payload on
|
|
30
|
+
/// the wrong chain.
|
|
31
|
+
mapping(bytes32 => bytes) private _routes;
|
|
32
|
+
|
|
33
|
+
/// chainKey => this contract's counterpart there, in that chain's own address format.
|
|
34
|
+
/// @dev Raw bytes: the counterpart is not at this address on zkSync or Tron, and a 32-byte
|
|
35
|
+
/// Solana or Move key cannot be narrowed to 20.
|
|
36
|
+
mapping(bytes32 => bytes) private _counterparts;
|
|
37
|
+
|
|
38
|
+
event RouteSet(bytes32 indexed chainKey, bytes route);
|
|
39
|
+
event CounterpartSet(bytes32 indexed chainKey, bytes counterpart);
|
|
40
|
+
|
|
41
|
+
error NoDestination();
|
|
42
|
+
error EmptyPayload();
|
|
43
|
+
error ZeroRoute();
|
|
44
|
+
error ZeroCounterpart();
|
|
45
|
+
error NoRouteFor(bytes32 chainKey);
|
|
46
|
+
/// @dev The route is not the canonical chain identifier that `chainKey` hashes from.
|
|
47
|
+
error RouteKeyMismatch(bytes32 chainKey);
|
|
48
|
+
error UnknownRoute();
|
|
49
|
+
error NoCounterpartFor(bytes32 chainKey);
|
|
50
|
+
/// @dev For a binding whose provider cannot quote on-chain (P9). Zero would read as free.
|
|
51
|
+
error QuoteNotImplemented();
|
|
52
|
+
|
|
53
|
+
/* ================================== routing ================================ */
|
|
54
|
+
|
|
55
|
+
/// @notice Record how a destination chain is named. Write-once and ungated: the caller
|
|
56
|
+
/// applies its own authority.
|
|
57
|
+
///
|
|
58
|
+
/// @dev The route must be the chain's canonical ERC-7930 chain identifier, and
|
|
59
|
+
/// `keccak256(route)` must be the chainKey: otherwise the reverse index would
|
|
60
|
+
/// attribute one chain's messages to another, and a conduit homed there would be
|
|
61
|
+
/// looked for under the wrong key (#25). A key therefore has exactly one valid route,
|
|
62
|
+
/// which makes the table write-once and injective without further checks: re-writing
|
|
63
|
+
/// it is a no-op, and any other route for the key is refused above.
|
|
64
|
+
function _setRoute(bytes32 chainKey, bytes memory route) internal {
|
|
65
|
+
if (chainKey == bytes32(0)) revert NoDestination();
|
|
66
|
+
if (route.length == 0) revert ZeroRoute();
|
|
67
|
+
_requireNames(route, chainKey);
|
|
68
|
+
|
|
69
|
+
if (_routes[chainKey].length != 0) return;
|
|
70
|
+
|
|
71
|
+
_routes[chainKey] = route;
|
|
72
|
+
emit RouteSet(chainKey, route);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// @notice Refuse `route` unless it is the canonical bare chain identifier `chainKey` hashes
|
|
76
|
+
/// from.
|
|
77
|
+
/// @dev `fromIdentifier` parses strictly and reduces to the bare identifier, so both together
|
|
78
|
+
/// admit only the canonical bare form, which is how an inbound route arrives.
|
|
79
|
+
function _requireNames(bytes memory route, bytes32 chainKey) internal pure {
|
|
80
|
+
if (keccak256(route) != chainKey || ChainKey.fromIdentifier(route) != chainKey) {
|
|
81
|
+
revert RouteKeyMismatch(chainKey);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/// @notice Record this contract's counterpart on a chain. Ungated: the caller applies its
|
|
86
|
+
/// own authority and decides whether a second write is allowed.
|
|
87
|
+
/// @dev Rebindable here, unlike a route: a counterpart names one address on an
|
|
88
|
+
/// already-fixed chain, and on a chain this one cannot recompute it is learned after
|
|
89
|
+
/// the fact.
|
|
90
|
+
function _setCounterpart(bytes32 chainKey, bytes memory counterpart) internal {
|
|
91
|
+
if (chainKey == bytes32(0)) revert NoDestination();
|
|
92
|
+
if (counterpart.length == 0) revert ZeroCounterpart();
|
|
93
|
+
|
|
94
|
+
_counterparts[chainKey] = counterpart;
|
|
95
|
+
emit CounterpartSet(chainKey, counterpart);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// @notice The chain a route refers to: its hash, provided that route is configured here.
|
|
99
|
+
function chainKeyOfRoute(bytes memory route) public view returns (bytes32 chainKey) {
|
|
100
|
+
chainKey = keccak256(route);
|
|
101
|
+
if (_routes[chainKey].length == 0) revert UnknownRoute();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// @notice How a chain is named here. Reverts when unset.
|
|
105
|
+
function routeFor(bytes32 chainKey) public view returns (bytes memory route) {
|
|
106
|
+
route = _routes[chainKey];
|
|
107
|
+
if (route.length == 0) revert NoRouteFor(chainKey);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function hasRoute(bytes32 chainKey) public view returns (bool) {
|
|
111
|
+
return _routes[chainKey].length != 0;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function hasCounterpart(bytes32 chainKey) public view returns (bool) {
|
|
115
|
+
return _counterparts[chainKey].length != 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/// @notice How the chain itself is named.
|
|
119
|
+
function _routeTo(bytes32 chainKey) internal view virtual returns (bytes memory) {
|
|
120
|
+
return routeFor(chainKey);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// @notice Where the counterpart lives.
|
|
124
|
+
/// @dev A transceiver overrides it to apply the registry's provenance bar, and to answer its
|
|
125
|
+
/// own address on a `Predetermined` chain with no counterpart recorded; see
|
|
126
|
+
/// `TransceiverBase`.
|
|
127
|
+
function _counterpartOn(bytes32 chainKey) internal view virtual returns (bytes memory counterpart) {
|
|
128
|
+
counterpart = _counterparts[chainKey];
|
|
129
|
+
if (counterpart.length == 0) revert NoCounterpartFor(chainKey);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/// @notice Where this contract's counterpart lives on `chainKey`.
|
|
133
|
+
function counterpartOn(bytes32 chainKey) public view returns (bytes memory) {
|
|
134
|
+
return _counterpartOn(chainKey);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// @notice The chain identifier `chainKey` is configured under.
|
|
138
|
+
function routeTo(bytes32 chainKey) public view returns (bytes memory) {
|
|
139
|
+
return _routeTo(chainKey);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/// @notice Revert unless this contract has both a counterpart it trusts and a route on
|
|
143
|
+
/// `chainKey`.
|
|
144
|
+
/// @dev Called for its reverts; both lookups' values are discarded.
|
|
145
|
+
function _requireRoutable(bytes32 chainKey) internal view {
|
|
146
|
+
_counterpartOn(chainKey);
|
|
147
|
+
_routeTo(chainKey);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/// @notice The counterpart on `chainKey`, as the ERC-7930 address an ERC-7786 gateway
|
|
151
|
+
/// takes as a recipient.
|
|
152
|
+
/// @dev Runs both lookups, so building a recipient enforces whatever bar each applies.
|
|
153
|
+
function _recipientOn(bytes32 chainKey) internal view returns (bytes memory) {
|
|
154
|
+
Erc7930.Interop memory io = Erc7930.parseStrict(_routeTo(chainKey));
|
|
155
|
+
return Erc7930.encode(io.chainType, io.chainRef, _counterpartOn(chainKey));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/* ================================== sending ================================ */
|
|
159
|
+
|
|
160
|
+
/// @notice Where a provider's excess fee goes back to: whoever paid it.
|
|
161
|
+
/// @dev On a transceiver's `bootstrap` that is `msg.sender`, the only conduit it accepts.
|
|
162
|
+
/// Never the transceiver's `address(this)`, which would pool every user's excess.
|
|
163
|
+
/// `TransmitterBase` pays from its own balance and overrides this to itself, as
|
|
164
|
+
/// `TransceiverBase` does while a report pays from its float.
|
|
165
|
+
function _refundTo() internal view virtual returns (address) {
|
|
166
|
+
return msg.sender;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/// @notice Put the payload on the wire. No default, so a binding that omits it does not
|
|
170
|
+
/// compile.
|
|
171
|
+
///
|
|
172
|
+
/// @dev One primitive for every channel: a payload to a conduit, a bootstrap to a transceiver,
|
|
173
|
+
/// and a receiver report home are all `bytes` to an ERC-7930 address.
|
|
174
|
+
///
|
|
175
|
+
/// @dev Spend `value`, never `msg.value`. On a transmitter `value` is the quote and
|
|
176
|
+
/// `msg.value` only tops up the balance; a transceiver takes a bootstrap fee off the top; on
|
|
177
|
+
/// a nested send `msg.value` is zero.
|
|
178
|
+
///
|
|
179
|
+
/// @param attributes Selector-prefixed values the gateway understands; it must refuse one
|
|
180
|
+
/// it does not (see `supportsAttribute`).
|
|
181
|
+
/// @return sendId Zero once the provider has the message, ERC-7786's "sent"; never the
|
|
182
|
+
/// provider's own message id. Over an ERC-7786 gateway it is that gateway's, and a
|
|
183
|
+
/// non-zero one is a further step the binding either performs or refuses.
|
|
184
|
+
function _sendMessage(bytes memory recipient, bytes memory payload, bytes[] memory attributes, uint256 value)
|
|
185
|
+
internal
|
|
186
|
+
virtual
|
|
187
|
+
returns (bytes32 sendId);
|
|
188
|
+
|
|
189
|
+
/// @notice The destination gas `payload` is sent with when its attributes name none.
|
|
190
|
+
/// @dev A function of the payload alone, so a quote and its send always agree.
|
|
191
|
+
function _defaultGas(bytes memory payload) internal pure virtual returns (uint256);
|
|
192
|
+
|
|
193
|
+
/// @notice What `_sendMessage` would cost, in this chain's native currency.
|
|
194
|
+
///
|
|
195
|
+
/// @dev ERC-7786 defines no quote. It is `view` so it can be `eth_call`ed before the send,
|
|
196
|
+
/// and takes the built `payload` and the send's arguments, since providers price the
|
|
197
|
+
/// exact bytes. A provider that also takes its own token is quoted on the native path.
|
|
198
|
+
///
|
|
199
|
+
/// @dev `_sendMessage` never consults it. A transmitter's entry points call it in the same
|
|
200
|
+
/// transaction as the send and pay exactly the answer, so it has no time to go stale.
|
|
201
|
+
/// A transceiver's receiver report does the same.
|
|
202
|
+
function _quoteMessage(bytes memory recipient, bytes memory payload, bytes[] memory attributes)
|
|
203
|
+
internal
|
|
204
|
+
view
|
|
205
|
+
virtual
|
|
206
|
+
returns (uint256 nativeFee);
|
|
207
|
+
|
|
208
|
+
/// @notice What sending `payload` to `recipient` would cost, in this chain's native
|
|
209
|
+
/// currency.
|
|
210
|
+
/// @dev On every sender, not only conduits: a transceiver's receiver report is paid from
|
|
211
|
+
/// its float, which someone has to price in order to fund. Ungated, since it spends
|
|
212
|
+
/// and writes nothing; `TransmitterBase` overrides it to apply its send's checks.
|
|
213
|
+
function quoteMessage(bytes calldata recipient, bytes calldata payload, bytes[] calldata attributes)
|
|
214
|
+
external
|
|
215
|
+
view
|
|
216
|
+
virtual
|
|
217
|
+
returns (uint256 nativeFee)
|
|
218
|
+
{
|
|
219
|
+
return _quoteMessage(recipient, payload, attributes);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.20;
|
|
3
|
+
|
|
4
|
+
import {TransmitterBase} from "./TransmitterBase.sol";
|
|
5
|
+
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
|
|
6
|
+
|
|
7
|
+
/// @notice `TransmitterBase` with OpenZeppelin's `Ownable` as its authority, which every
|
|
8
|
+
/// binding uses. `TransmitterBase` itself stays ownership-agnostic (see its note).
|
|
9
|
+
abstract contract OwnableTransmitter is TransmitterBase, OwnableUpgradeable {
|
|
10
|
+
/// @dev The `ITransmitterInit` shape `TransceiverBase` encodes.
|
|
11
|
+
function initialize(address owner_, address transceiver_, bytes32 salt_) external virtual initializer {
|
|
12
|
+
__OwnableTransmitter_init(owner_, transceiver_, salt_);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function __OwnableTransmitter_init(address owner_, address transceiver_, bytes32 salt_) internal onlyInitializing {
|
|
16
|
+
__Ownable_init(owner_);
|
|
17
|
+
__TransmitterBase_init(owner_, transceiver_, salt_);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function _checkOwner() internal view virtual override(TransmitterBase, OwnableUpgradeable) {
|
|
21
|
+
OwnableUpgradeable._checkOwner();
|
|
22
|
+
}
|
|
23
|
+
}
|