@crossecute/protocol 0.0.0-stage → 0.1.0

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.
Files changed (118) hide show
  1. package/README.md +26 -2
  2. package/abi/Blake2b256.json +1 -0
  3. package/abi/Blake2bScheme.json +1 -0
  4. package/abi/ChainRegistry.json +1 -0
  5. package/abi/Commitment.json +1 -0
  6. package/abi/Conduits.json +1 -0
  7. package/abi/DivergentConduits.json +1 -0
  8. package/abi/DivergentTransceiver.json +1 -0
  9. package/abi/DivergentTransceiverExtension.json +1 -0
  10. package/abi/Envelope.json +1 -0
  11. package/abi/Erc7930.json +1 -0
  12. package/abi/Executor.json +1 -0
  13. package/abi/IBootstrapAllowlist.json +1 -0
  14. package/abi/ICancel.json +1 -0
  15. package/abi/IChainRegistryRefs.json +1 -0
  16. package/abi/ICommitFinalize.json +1 -0
  17. package/abi/ICommitmentScheme.json +1 -0
  18. package/abi/IConduitReceiverReport.json +1 -0
  19. package/abi/IConduitTransceiver.json +1 -0
  20. package/abi/IERC7786GatewaySource.json +1 -0
  21. package/abi/IERC7786Recipient.json +1 -0
  22. package/abi/IExecute.json +1 -0
  23. package/abi/ILzOftConduitFactory.json +1 -0
  24. package/abi/IProviderIdTable.json +1 -0
  25. package/abi/IProxy.json +1 -0
  26. package/abi/IReceiverInit.json +1 -0
  27. package/abi/IRefValidator.json +1 -0
  28. package/abi/IReportFloat.json +1 -0
  29. package/abi/ITransmitterInit.json +1 -0
  30. package/abi/IVmDeriver.json +1 -0
  31. package/abi/KeccakScheme.json +1 -0
  32. package/abi/Move.json +1 -0
  33. package/abi/MoveValidator.json +1 -0
  34. package/abi/OpStackTransmitterBase.json +1 -0
  35. package/abi/OutboundBase.json +1 -0
  36. package/abi/OwnableTransmitter.json +1 -0
  37. package/abi/ProviderAddress.json +1 -0
  38. package/abi/ProviderAttribute.json +1 -0
  39. package/abi/ProviderChainId.json +1 -0
  40. package/abi/ProviderReceiver.json +1 -0
  41. package/abi/ProviderTransceiver.json +1 -0
  42. package/abi/Proxy.json +1 -0
  43. package/abi/ProxyDeployer.json +1 -0
  44. package/abi/ProxyInitCode.json +1 -0
  45. package/abi/ReceiverBase.json +1 -0
  46. package/abi/Roles.json +1 -0
  47. package/abi/RolesEnumerable.json +1 -0
  48. package/abi/SchemeFold.json +1 -0
  49. package/abi/Sha256Scheme.json +1 -0
  50. package/abi/StarknetDerive.json +1 -0
  51. package/abi/StarknetValidator.json +1 -0
  52. package/abi/SuiDerive.json +1 -0
  53. package/abi/TransceiverBase.json +1 -0
  54. package/abi/TransceiverCore.json +1 -0
  55. package/abi/TransceiverExtension.json +1 -0
  56. package/abi/TransmitterBase.json +1 -0
  57. package/abi/Treasury.json +1 -0
  58. package/abi/TronConduits.json +1 -0
  59. package/abi/TronTransceiver.json +1 -0
  60. package/abi/TronTransceiverExtension.json +1 -0
  61. package/abi/VmDeriver.json +1 -0
  62. package/abi/ZkSyncConduits.json +1 -0
  63. package/abi/ZkSyncTransceiver.json +1 -0
  64. package/abi/ZkSyncTransceiverExtension.json +1 -0
  65. package/abi/index.d.ts +63 -0
  66. package/abi/index.js +63 -0
  67. package/contracts/addressing/ChainKey.sol +45 -0
  68. package/contracts/addressing/ChainType.sol +50 -0
  69. package/contracts/addressing/Erc7930.sol +209 -0
  70. package/contracts/addressing/Move.sol +212 -0
  71. package/contracts/conduit/Conduits.sol +43 -0
  72. package/contracts/conduit/Proxy.sol +144 -0
  73. package/contracts/conduit/ProxyDeployer.sol +42 -0
  74. package/contracts/conduit/ProxyInitCode.sol +47 -0
  75. package/contracts/derivation/AddressDerive.sol +326 -0
  76. package/contracts/derivation/BitcoinDerive.sol +33 -0
  77. package/contracts/derivation/Blake2b256.sol +122 -0
  78. package/contracts/derivation/IVmDeriver.sol +35 -0
  79. package/contracts/derivation/StarknetDerive.sol +93 -0
  80. package/contracts/derivation/SuiDerive.sol +102 -0
  81. package/contracts/derivation/VmDeriver.sol +291 -0
  82. package/contracts/messaging/Call.sol +43 -0
  83. package/contracts/messaging/Commitment.sol +131 -0
  84. package/contracts/messaging/DeliveryGas.sol +18 -0
  85. package/contracts/messaging/Envelope.sol +123 -0
  86. package/contracts/messaging/Executor.sol +52 -0
  87. package/contracts/messaging/IErc7786.sol +68 -0
  88. package/contracts/messaging/Payload.sol +90 -0
  89. package/contracts/messaging/Roles.sol +113 -0
  90. package/contracts/messaging/inbound/ReceiverBase.sol +359 -0
  91. package/contracts/messaging/outbound/OutboundBase.sol +221 -0
  92. package/contracts/messaging/outbound/OwnableTransmitter.sol +23 -0
  93. package/contracts/messaging/outbound/TransmitterBase.sol +604 -0
  94. package/contracts/messaging/transceiver/DivergentConduits.sol +92 -0
  95. package/contracts/messaging/transceiver/DivergentTransceiver.sol +97 -0
  96. package/contracts/messaging/transceiver/IBootstrapAllowlist.sol +8 -0
  97. package/contracts/messaging/transceiver/TransceiverBase.sol +541 -0
  98. package/contracts/messaging/transceiver/TransceiverCore.sol +307 -0
  99. package/contracts/messaging/transceiver/TransceiverExtension.sol +278 -0
  100. package/contracts/protocols/ProviderAddress.sol +32 -0
  101. package/contracts/protocols/ProviderAttribute.sol +65 -0
  102. package/contracts/protocols/ProviderChainId.sol +115 -0
  103. package/contracts/protocols/ProviderReceiver.sol +24 -0
  104. package/contracts/protocols/ProviderTransceiver.sol +48 -0
  105. package/contracts/protocols/layerzero/ILzOftConduitFactory.sol +10 -0
  106. package/contracts/protocols/op-stack/OpStackTransmitterBase.sol +11 -0
  107. package/contracts/registry/ChainRegistry.sol +667 -0
  108. package/contracts/registry/IChainRegistryRefs.sol +81 -0
  109. package/contracts/registry/ICommitmentScheme.sol +46 -0
  110. package/contracts/registry/IRefValidator.sol +13 -0
  111. package/contracts/registry/Provenance.sol +25 -0
  112. package/contracts/schemes/Blake2bScheme.sol +18 -0
  113. package/contracts/schemes/KeccakScheme.sol +19 -0
  114. package/contracts/schemes/Sha256Scheme.sol +15 -0
  115. package/contracts/treasury/Treasury.sol +76 -0
  116. package/contracts/validators/MoveValidator.sol +24 -0
  117. package/contracts/validators/StarknetValidator.sol +20 -0
  118. package/package.json +36 -4
@@ -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
+ }