@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,43 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.0;
|
|
3
|
+
|
|
4
|
+
/// @notice One call in a payload.
|
|
5
|
+
/// @dev The ERC-7579 `Execution` tuple, also used by ERC-7821, so existing payload builders
|
|
6
|
+
/// work unchanged. Target and value are in the approved array, so a payload cannot be
|
|
7
|
+
/// redirected or re-priced at execution.
|
|
8
|
+
struct Call {
|
|
9
|
+
address target;
|
|
10
|
+
uint256 value;
|
|
11
|
+
bytes data;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/// @title Calls
|
|
15
|
+
/// @notice Conversion between the typed `Call` and the canonical opaque element.
|
|
16
|
+
/// @dev The two forms must hash identically, so an approval made in one form can be
|
|
17
|
+
/// discharged in the other.
|
|
18
|
+
library Calls {
|
|
19
|
+
/// @notice The canonical opaque element for a typed call.
|
|
20
|
+
/// @dev Encodes the fields, not the struct: `abi.encode(c)` prepends an offset word for the
|
|
21
|
+
/// dynamic tuple and would hash differently.
|
|
22
|
+
function encode(Call memory c) internal pure returns (bytes memory) {
|
|
23
|
+
return abi.encode(c.target, c.value, c.data);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/// @notice The element hash. Equals `keccak256(encode(c))`.
|
|
27
|
+
function hash(Call memory c) internal pure returns (bytes32) {
|
|
28
|
+
return keccak256(abi.encode(c.target, c.value, c.data));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// @notice Split a canonical opaque element back into a typed call. Reverts if it is not
|
|
32
|
+
/// `abi.encode(address, uint256, bytes)`.
|
|
33
|
+
function decode(bytes memory element) internal pure returns (Call memory c) {
|
|
34
|
+
(c.target, c.value, c.data) = abi.decode(element, (address, uint256, bytes));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function encodeAll(Call[] memory calls) internal pure returns (bytes[] memory out) {
|
|
38
|
+
out = new bytes[](calls.length);
|
|
39
|
+
for (uint256 i; i < calls.length; ++i) {
|
|
40
|
+
out[i] = encode(calls[i]);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.0;
|
|
3
|
+
|
|
4
|
+
import {ChainKey} from "../addressing/ChainKey.sol";
|
|
5
|
+
import {Call, Calls} from "./Call.sol";
|
|
6
|
+
import {Blake2b256} from "../derivation/Blake2b256.sol";
|
|
7
|
+
|
|
8
|
+
/// @notice The hash a destination computes its commitments with.
|
|
9
|
+
///
|
|
10
|
+
/// @dev The fold is fixed and only the primitive varies, so the home can reproduce any
|
|
11
|
+
/// destination's commitment for a signer. The cost is that a non-EVM receiver implements
|
|
12
|
+
/// a byte fold rather than its native digest (TON's cell hash, Starknet's
|
|
13
|
+
/// `poseidon_hash_span`).
|
|
14
|
+
///
|
|
15
|
+
/// @dev An EVM receiver only ever uses keccak256; the others are for the home building a
|
|
16
|
+
/// commitment another VM will recompute.
|
|
17
|
+
enum Scheme {
|
|
18
|
+
/// EVM. The opcode.
|
|
19
|
+
Keccak256,
|
|
20
|
+
/// Ton. `sha256` is an EVM builtin, so this stays computable on both sides.
|
|
21
|
+
Sha256,
|
|
22
|
+
/// Cardano. EIP-152 precompile 0x09: see `Blake2b256`. The reason dispatching
|
|
23
|
+
/// functions are `view` rather than `pure`.
|
|
24
|
+
Blake2b256Scheme,
|
|
25
|
+
/// Starknet. Declared, not implemented: Poseidon needs the exact round constants and MDS
|
|
26
|
+
/// matrix, checked against vectors (`docs/todo.md` §4). Until then a Starknet commitment
|
|
27
|
+
/// is computed off-chain and carried in an element calling that receiver's `commit`.
|
|
28
|
+
Poseidon
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// @title Commitment
|
|
32
|
+
/// @notice The commitment over a call array, bound to exactly one destination.
|
|
33
|
+
///
|
|
34
|
+
/// @dev Seeded with the destination's chainKey, since conduits share addresses across chains
|
|
35
|
+
/// and a commitment must not replay between them. A chainKey rather than a chain id
|
|
36
|
+
/// because non-EVM chains have none; on the EVM it still derives from `block.chainid`.
|
|
37
|
+
///
|
|
38
|
+
/// @dev Defined over opaque elements, which it never looks inside, so no VM's call format is
|
|
39
|
+
/// parsed in Solidity. The typed overloads produce the same hash, since `Calls.encode`
|
|
40
|
+
/// yields exactly the opaque element.
|
|
41
|
+
library Commitment {
|
|
42
|
+
/// @dev The scheme has no implementation on this chain, so a commitment for it must be
|
|
43
|
+
/// computed off-chain and approved as a digest.
|
|
44
|
+
error SchemeNotComputable(Scheme scheme);
|
|
45
|
+
/// @dev An empty array would hash to the seed alone, an approval that runs nothing.
|
|
46
|
+
error EmptyCommitment();
|
|
47
|
+
|
|
48
|
+
/// @notice The hash a receiver on this chain will require, for typed calls.
|
|
49
|
+
function hashCalls(Call[] memory calls) internal view returns (bytes32) {
|
|
50
|
+
return hashCalls(ChainKey.local(), calls);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// @notice The same value, from typed calls.
|
|
54
|
+
/// @dev Equal to the opaque overload over `Calls.encodeAll(calls)`, asserted in
|
|
55
|
+
/// `test/PayloadEncoding.t.sol`. Array parameters are `memory` throughout because
|
|
56
|
+
/// Solidity will not overload on data location.
|
|
57
|
+
function hashCalls(bytes32 destinationChainKey, Call[] memory calls) internal pure returns (bytes32 hashed) {
|
|
58
|
+
uint256 len = calls.length;
|
|
59
|
+
if (len == 0) revert EmptyCommitment();
|
|
60
|
+
hashed = _seed(destinationChainKey);
|
|
61
|
+
for (uint256 i = 0; i < len; i++) {
|
|
62
|
+
hashed = _fold(hashed, Calls.hash(calls[i]));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// @notice Canonical: the hash a receiver on `destinationChainKey` requires, over the
|
|
67
|
+
/// portable opaque elements.
|
|
68
|
+
/// @dev The source passes the destination's key; the local one yields a commitment the
|
|
69
|
+
/// far side never matches.
|
|
70
|
+
function hashCalls(bytes32 destinationChainKey, bytes[] memory elements) internal pure returns (bytes32 hashed) {
|
|
71
|
+
uint256 len = elements.length;
|
|
72
|
+
if (len == 0) revert EmptyCommitment();
|
|
73
|
+
hashed = _seed(destinationChainKey);
|
|
74
|
+
for (uint256 i = 0; i < len; i++) {
|
|
75
|
+
hashed = _fold(hashed, keccak256(elements[i]));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* ============================ non-EVM destinations =========================== */
|
|
80
|
+
|
|
81
|
+
/// @notice The commitment a destination using `scheme` will require.
|
|
82
|
+
/// @dev Source side only; EVM receivers use the keccak overloads above. `view` for
|
|
83
|
+
/// `Blake2b256`'s precompile `staticcall`, read through `eth_call` by a signer.
|
|
84
|
+
function hashCalls(Scheme scheme, bytes32 destinationChainKey, bytes[] memory elements)
|
|
85
|
+
internal
|
|
86
|
+
view
|
|
87
|
+
returns (bytes32 hashed)
|
|
88
|
+
{
|
|
89
|
+
uint256 len = elements.length;
|
|
90
|
+
if (len == 0) revert EmptyCommitment();
|
|
91
|
+
hashed = _hash(scheme, abi.encode(destinationChainKey));
|
|
92
|
+
for (uint256 i = 0; i < len; i++) {
|
|
93
|
+
hashed = _hash(scheme, abi.encodePacked(hashed, _hash(scheme, elements[i])));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// @notice The same value, from typed calls.
|
|
98
|
+
/// @dev Delegates to the opaque overload over `Calls.encodeAll`, so both spellings share
|
|
99
|
+
/// one fold.
|
|
100
|
+
function hashCalls(Scheme scheme, bytes32 destinationChainKey, Call[] memory calls)
|
|
101
|
+
internal
|
|
102
|
+
view
|
|
103
|
+
returns (bytes32)
|
|
104
|
+
{
|
|
105
|
+
return hashCalls(scheme, destinationChainKey, Calls.encodeAll(calls));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// @notice Whether this chain can compute `scheme` at all. False only for `Poseidon`.
|
|
109
|
+
function isComputable(Scheme scheme) internal pure returns (bool) {
|
|
110
|
+
return scheme != Scheme.Poseidon;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* ================================== internals =============================== */
|
|
114
|
+
|
|
115
|
+
function _seed(bytes32 destinationChainKey) private pure returns (bytes32) {
|
|
116
|
+
return keccak256(abi.encode(destinationChainKey));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function _fold(bytes32 acc, bytes32 elementHash) private pure returns (bytes32) {
|
|
120
|
+
return keccak256(abi.encodePacked(acc, elementHash));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// @dev Reverts for a scheme with no implementation here rather than falling back to a
|
|
124
|
+
/// keccak256 digest the destination could never match.
|
|
125
|
+
function _hash(Scheme scheme, bytes memory data) private view returns (bytes32) {
|
|
126
|
+
if (scheme == Scheme.Keccak256) return keccak256(data);
|
|
127
|
+
if (scheme == Scheme.Sha256) return sha256(data);
|
|
128
|
+
if (scheme == Scheme.Blake2b256Scheme) return Blake2b256.hash(data);
|
|
129
|
+
revert SchemeNotComputable(scheme);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.0;
|
|
3
|
+
|
|
4
|
+
/// @notice The destination gas a message is sent with when its attributes name none, by what
|
|
5
|
+
/// it does on arrival.
|
|
6
|
+
/// @dev Measured cold on Forge's EVM with every binding's fixture (deploy/CHECKS.md §4), and
|
|
7
|
+
/// held at least 10% above that by `ProviderDeliveryGasSpec`: a bootstrap that creates a
|
|
8
|
+
/// conduit with no calls 442k to 497k, a report arriving home up to 123k on the bindings
|
|
9
|
+
/// that carry one (2026-11-08).
|
|
10
|
+
/// A bootstrap on a reporting chain also pays for the report's send. On EraVM a
|
|
11
|
+
/// bootstrap's execution is about 385k to 389k; its pubdata is not measured. A gas
|
|
12
|
+
/// attribute replaces these with no floor: a delivery short of gas reverts and is retried
|
|
13
|
+
/// with more (provider-spec R1.5).
|
|
14
|
+
library DeliveryGas {
|
|
15
|
+
uint256 internal constant BOOTSTRAP = 1_000_000;
|
|
16
|
+
uint256 internal constant REPORT = 250_000;
|
|
17
|
+
uint256 internal constant PAYLOAD = 200_000;
|
|
18
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.0;
|
|
3
|
+
|
|
4
|
+
import {Call} from "./Call.sol";
|
|
5
|
+
|
|
6
|
+
/// @title Envelope
|
|
7
|
+
/// @notice The message bodies that cross between transceivers, each led by its kind.
|
|
8
|
+
///
|
|
9
|
+
/// @dev Tagged: once every transceiver both sends and receives bootstraps and reports, the
|
|
10
|
+
/// direction no longer decides the shape, and decoding the wrong shape can misread
|
|
11
|
+
/// silently. A decoder checks the tag before it reads anything else.
|
|
12
|
+
///
|
|
13
|
+
/// @dev Kinds start at 1, so a zero word, or the leading owner word of an untagged v1 body,
|
|
14
|
+
/// is refused rather than read as a kind.
|
|
15
|
+
///
|
|
16
|
+
/// @dev `abi.encode`, not `encodePacked`, so no two messages share a byte string.
|
|
17
|
+
library Envelope {
|
|
18
|
+
uint8 internal constant BOOTSTRAP = 1;
|
|
19
|
+
uint8 internal constant BOOTSTRAP_ELEMENTS = 2;
|
|
20
|
+
uint8 internal constant RECEIVER_REPORT = 3;
|
|
21
|
+
|
|
22
|
+
/// @dev Shorter than the one word that holds the kind.
|
|
23
|
+
error EnvelopeTooShort();
|
|
24
|
+
/// @dev The leading word is not a kind this protocol defines.
|
|
25
|
+
error UnknownEnvelopeKind(uint256 kind);
|
|
26
|
+
/// @dev A known kind, but not the one this decoder reads.
|
|
27
|
+
error UnexpectedEnvelopeKind(uint8 expected, uint8 got);
|
|
28
|
+
|
|
29
|
+
/// @notice The kind of `message`, reverting unless it is one this protocol defines.
|
|
30
|
+
function kindOf(bytes calldata message) internal pure returns (uint8) {
|
|
31
|
+
if (message.length < 32) revert EnvelopeTooShort();
|
|
32
|
+
uint256 kind = abi.decode(message[:32], (uint256));
|
|
33
|
+
if (kind == 0 || kind > RECEIVER_REPORT) revert UnknownEnvelopeKind(kind);
|
|
34
|
+
// forge-lint: disable-next-line(unsafe-typecast) bounded by the check above
|
|
35
|
+
return uint8(kind);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function _expect(bytes calldata message, uint8 kind) private pure {
|
|
39
|
+
uint8 got = kindOf(message);
|
|
40
|
+
if (got != kind) revert UnexpectedEnvelopeKind(kind, got);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* ================================= bootstrap =============================== */
|
|
44
|
+
|
|
45
|
+
/// @notice The payload that stands a receiver up, credited to the transmitter it
|
|
46
|
+
/// will answer to.
|
|
47
|
+
///
|
|
48
|
+
/// @dev Calls, not a commitment: the transceiver creates and initializes the receiver and
|
|
49
|
+
/// is never in the path again. A payload that should wait carries a self-call to
|
|
50
|
+
/// `commit`.
|
|
51
|
+
///
|
|
52
|
+
/// @dev Names the owner and salt, from which the destination derives the conduit address.
|
|
53
|
+
/// A transceiver is shared, so nothing the bridge reports says who authorized the
|
|
54
|
+
/// message.
|
|
55
|
+
///
|
|
56
|
+
/// @dev Carries the transmitter the receiver will answer to. The origin transceiver sends
|
|
57
|
+
/// only for the conduit `(owner, salt)` resolves to there, so the value is vouched for
|
|
58
|
+
/// by the same authenticated message, and no destination needs another chain's
|
|
59
|
+
/// address formula to derive it. A fixed word rather than `bytes`, which would add an
|
|
60
|
+
/// offset and a length word to every bootstrap: an EVM address left-padded, or a
|
|
61
|
+
/// 32-byte key on a VM that has one.
|
|
62
|
+
function encodeBootstrap(address owner, bytes32 salt, bytes32 transmitter, Call[] memory calls)
|
|
63
|
+
internal
|
|
64
|
+
pure
|
|
65
|
+
returns (bytes memory)
|
|
66
|
+
{
|
|
67
|
+
return abi.encode(BOOTSTRAP, owner, salt, transmitter, calls);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function decodeBootstrap(bytes calldata message)
|
|
71
|
+
internal
|
|
72
|
+
pure
|
|
73
|
+
returns (address owner, bytes32 salt, bytes32 transmitter, Call[] memory calls)
|
|
74
|
+
{
|
|
75
|
+
_expect(message, BOOTSTRAP);
|
|
76
|
+
(, owner, salt, transmitter, calls) = abi.decode(message, (uint8, address, bytes32, bytes32, Call[]));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// @notice The same message, for a destination whose calls this chain cannot express.
|
|
80
|
+
/// @dev No Solidity decoder: the EVM transceiver refuses this kind, and a non-EVM
|
|
81
|
+
/// transceiver decodes it in its own language.
|
|
82
|
+
function encodeBootstrapElements(address owner, bytes32 salt, bytes32 transmitter, bytes[] memory elements)
|
|
83
|
+
internal
|
|
84
|
+
pure
|
|
85
|
+
returns (bytes memory)
|
|
86
|
+
{
|
|
87
|
+
return abi.encode(BOOTSTRAP_ELEMENTS, owner, salt, transmitter, elements);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* ============================== receiver report ============================= */
|
|
91
|
+
|
|
92
|
+
/// @notice Where the destination created a transmitter's receiver.
|
|
93
|
+
///
|
|
94
|
+
/// @dev Only chains whose addresses the home cannot recompute send this: zkSync and Tron,
|
|
95
|
+
/// whose CREATE2 formulas differ, and non-EVM chains such as Starknet.
|
|
96
|
+
///
|
|
97
|
+
/// @dev No request id: the chain comes from the authenticated origin and `(owner, salt)`
|
|
98
|
+
/// is stated here, which identifies the conduit the home's transceiver forwards the
|
|
99
|
+
/// report to. The conduit refuses a second report.
|
|
100
|
+
/// @param interop Canonical ERC-7930 bytes for the receiver on the reporting chain.
|
|
101
|
+
function encodeReceiverReport(address owner, bytes32 salt, bytes memory interop)
|
|
102
|
+
internal
|
|
103
|
+
pure
|
|
104
|
+
returns (bytes memory)
|
|
105
|
+
{
|
|
106
|
+
return abi.encode(RECEIVER_REPORT, owner, salt, interop);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/// @notice Whether `message` is led by the report kind. Reads nothing past the kind.
|
|
110
|
+
function isReceiverReport(bytes memory message) internal pure returns (bool) {
|
|
111
|
+
// forge-lint: disable-next-line(unsafe-typecast) the leading word, after the length check
|
|
112
|
+
return message.length >= 32 && uint256(bytes32(message)) == RECEIVER_REPORT;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function decodeReceiverReport(bytes calldata message)
|
|
116
|
+
internal
|
|
117
|
+
pure
|
|
118
|
+
returns (address owner, bytes32 salt, bytes memory interop)
|
|
119
|
+
{
|
|
120
|
+
_expect(message, RECEIVER_REPORT);
|
|
121
|
+
(, owner, salt, interop) = abi.decode(message, (uint8, address, bytes32, bytes));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.0;
|
|
3
|
+
|
|
4
|
+
import {Call} from "./Call.sol";
|
|
5
|
+
|
|
6
|
+
/// @title Executor
|
|
7
|
+
/// @notice Running a verified call array, and the policy that gates it.
|
|
8
|
+
/// @dev Shared by every contract that runs payloads: a receiver runs what arrives, and a
|
|
9
|
+
/// transmitter what its owner hands it. A transceiver runs none. No storage.
|
|
10
|
+
abstract contract Executor {
|
|
11
|
+
error SelectorNotAllowed(address target, bytes4 selector);
|
|
12
|
+
/// @dev Carries the index of the element that failed.
|
|
13
|
+
error CallFailed(uint256 index, bytes reason);
|
|
14
|
+
/// @dev An empty array proves no intent, so it is refused rather than a successful no-op.
|
|
15
|
+
error EmptyExecution();
|
|
16
|
+
|
|
17
|
+
/// @notice Call policy. The default allows everything.
|
|
18
|
+
///
|
|
19
|
+
/// @dev Open by default: conduits are full-power and answer to one owner, as its own account
|
|
20
|
+
/// would, and anything that can deliver an authorized payload could forge a commitment
|
|
21
|
+
/// as easily as a call. Failing closed would revert every payload, the bootstrap payload
|
|
22
|
+
/// included, until overridden.
|
|
23
|
+
///
|
|
24
|
+
/// @dev Merkle-verified calls are not in v1. If added, the model is Veda's
|
|
25
|
+
/// `ManagerWithMerkleVerification`:
|
|
26
|
+
/// https://github.com/Veda-Labs/boring-vault/blob/main/src/base/Roles/ManagerWithMerkleVerification.sol
|
|
27
|
+
function isAllowed(address, bytes4) public view virtual returns (bool) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// @notice Run the verified calls, in order, all or nothing.
|
|
32
|
+
/// @dev An approval covers the array as a unit, so a prefix must not stand. The failing
|
|
33
|
+
/// call's revert reason is carried in `CallFailed`.
|
|
34
|
+
function _execute(Call[] memory calls) internal virtual {
|
|
35
|
+
uint256 len = calls.length;
|
|
36
|
+
for (uint256 i; i < len; ++i) {
|
|
37
|
+
Call memory c = calls[i];
|
|
38
|
+
|
|
39
|
+
// Under four bytes there is no selector (a value transfer or a fallback hit).
|
|
40
|
+
// `bytes4` of short data right-pads with zeros and would invent one.
|
|
41
|
+
// forge-lint: disable-next-line(unsafe-typecast) the first 4 bytes; length checked
|
|
42
|
+
bytes4 selector = c.data.length >= 4 ? bytes4(c.data) : bytes4(0);
|
|
43
|
+
if (!isAllowed(c.target, selector)) {
|
|
44
|
+
revert SelectorNotAllowed(c.target, selector);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// forge-lint: disable-next-line(arbitrary-send-eth,calls-loop) owner-approved calls, all or nothing
|
|
48
|
+
(bool ok, bytes memory reason) = c.target.call{value: c.value}(c.data);
|
|
49
|
+
if (!ok) revert CallFailed(i, reason);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity >=0.8.4;
|
|
3
|
+
|
|
4
|
+
/// @title IErc7786
|
|
5
|
+
/// @notice ERC-7786's two interfaces, vendored: the send surface `TransmitterBase` implements
|
|
6
|
+
/// and the receive surface `ReceiverBase` implements.
|
|
7
|
+
///
|
|
8
|
+
/// @dev Copied from `@openzeppelin/contracts/interfaces/draft-IERC7786.sol` at v5.5.0;
|
|
9
|
+
/// only this header and the file name differ. Vendored because the ERC is a draft and
|
|
10
|
+
/// these selectors and the event topic are this protocol's ABI: an upstream change becomes
|
|
11
|
+
/// a reviewed edit rather than a side effect of a dependency bump.
|
|
12
|
+
///
|
|
13
|
+
/// @dev OpenZeppelin's types and these are distinct to Solidity. Integrators import from
|
|
14
|
+
/// here; a binding whose SDK returns OpenZeppelin's variant casts through the address.
|
|
15
|
+
|
|
16
|
+
/// @dev Interface for ERC-7786 source gateways.
|
|
17
|
+
interface IERC7786GatewaySource {
|
|
18
|
+
/// @dev Emitted when a message is created. If `sendId` is zero, no further processing is
|
|
19
|
+
/// necessary. If `sendId` is not zero, then further (gateway specific, and
|
|
20
|
+
/// non-standardized) action is required.
|
|
21
|
+
event MessageSent(
|
|
22
|
+
bytes32 indexed sendId,
|
|
23
|
+
bytes sender, // Binary Interoperable Address
|
|
24
|
+
bytes recipient, // Binary Interoperable Address
|
|
25
|
+
bytes payload,
|
|
26
|
+
uint256 value,
|
|
27
|
+
bytes[] attributes
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
/// @dev Thrown when a message creation fails because of an unsupported attribute.
|
|
31
|
+
error UnsupportedAttribute(bytes4 selector);
|
|
32
|
+
|
|
33
|
+
/// @dev Whether an attribute is supported or not.
|
|
34
|
+
function supportsAttribute(bytes4 selector) external view returns (bool);
|
|
35
|
+
|
|
36
|
+
/// @dev Endpoint for creating a new message. If the message requires further (gateway
|
|
37
|
+
/// specific) processing before it can be sent to the destination chain, then a
|
|
38
|
+
/// non-zero `sendId` must be returned. Otherwise, the message MUST be sent and this
|
|
39
|
+
/// function must return 0.
|
|
40
|
+
///
|
|
41
|
+
/// MUST emit a {MessageSent} event.
|
|
42
|
+
///
|
|
43
|
+
/// If any of the `attributes` is not supported, this function SHOULD revert with an
|
|
44
|
+
/// {UnsupportedAttribute} error. Other errors SHOULD revert with errors not
|
|
45
|
+
/// specified in ERC-7786.
|
|
46
|
+
function sendMessage(
|
|
47
|
+
bytes calldata recipient, // Binary Interoperable Address
|
|
48
|
+
bytes calldata payload,
|
|
49
|
+
bytes[] calldata attributes
|
|
50
|
+
)
|
|
51
|
+
external
|
|
52
|
+
payable
|
|
53
|
+
returns (bytes32 sendId);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// @dev Interface for the ERC-7786 client contract (receiver).
|
|
57
|
+
interface IERC7786Recipient {
|
|
58
|
+
/// @dev Endpoint for receiving a cross-chain message. May be called directly by the
|
|
59
|
+
/// gateway.
|
|
60
|
+
function receiveMessage(
|
|
61
|
+
bytes32 receiveId,
|
|
62
|
+
bytes calldata sender, // Binary Interoperable Address
|
|
63
|
+
bytes calldata payload
|
|
64
|
+
)
|
|
65
|
+
external
|
|
66
|
+
payable
|
|
67
|
+
returns (bytes4);
|
|
68
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.0;
|
|
3
|
+
|
|
4
|
+
import {Call} from "./Call.sol";
|
|
5
|
+
import {Erc7930} from "../addressing/Erc7930.sol";
|
|
6
|
+
|
|
7
|
+
/// @title Payload
|
|
8
|
+
/// @notice The wire encoding of a call array, in the one shape its destination speaks.
|
|
9
|
+
///
|
|
10
|
+
/// @dev An EVM destination receives `Call[]`; every other VM receives opaque `bytes[]` in its
|
|
11
|
+
/// own call encoding. The sender picks by destination and the receiver decodes the one
|
|
12
|
+
/// shape its VM implies, so there is no form tag.
|
|
13
|
+
///
|
|
14
|
+
/// @dev That relies on no path sending opaque elements to an EVM receiver, not on
|
|
15
|
+
/// `abi.decode` rejecting the wrong shape. A destination that could accept both needs the
|
|
16
|
+
/// tag back.
|
|
17
|
+
///
|
|
18
|
+
/// @dev Both forms of one payload commit to one hash: `Commitment` folds per-element hashes
|
|
19
|
+
/// and `Calls.encode` produces exactly the opaque element.
|
|
20
|
+
library Payload {
|
|
21
|
+
/* ============================== EVM destinations ============================ */
|
|
22
|
+
|
|
23
|
+
/// @notice Wire bytes for an EVM destination.
|
|
24
|
+
function encodeCalls(Call[] memory calls) internal pure returns (bytes memory) {
|
|
25
|
+
return abi.encode(calls);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// @notice Decode a payload that arrived on an EVM chain. Reverts on anything that is not
|
|
29
|
+
/// `Call[]`.
|
|
30
|
+
function decodeCalls(bytes calldata wire) internal pure returns (Call[] memory) {
|
|
31
|
+
return abi.decode(wire, (Call[]));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// @notice `decodeCalls`, for a wire already in memory.
|
|
35
|
+
/// @dev Separate because Solidity will not overload on data location, and a receiver's
|
|
36
|
+
/// payload stays in calldata rather than paying a copy on every delivery.
|
|
37
|
+
function decodeCallsFromMemory(bytes memory wire) internal pure returns (Call[] memory) {
|
|
38
|
+
return abi.decode(wire, (Call[]));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* ============================ non-EVM destinations ========================== */
|
|
42
|
+
|
|
43
|
+
/// @notice Wire bytes for a destination whose calls this chain cannot express.
|
|
44
|
+
/// @dev Each element is that VM's own call encoding (a Solana instruction with its
|
|
45
|
+
/// accounts, a Starknet `(to, selector, calldata)`, an Aptos entry function), unparsed
|
|
46
|
+
/// here.
|
|
47
|
+
function encodeElements(bytes[] memory elements) internal pure returns (bytes memory) {
|
|
48
|
+
return abi.encode(elements);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function decodeElements(bytes calldata wire) internal pure returns (bytes[] memory) {
|
|
52
|
+
return abi.decode(wire, (bytes[]));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// @notice `decodeElements`, for a wire already in memory.
|
|
56
|
+
function decodeElementsFromMemory(bytes memory wire) internal pure returns (bytes[] memory) {
|
|
57
|
+
return abi.decode(wire, (bytes[]));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* ================================ bootstrap ================================= */
|
|
61
|
+
|
|
62
|
+
/// @notice The plain bootstrap payload: the destination's ERC-7930 chain identifier, and
|
|
63
|
+
/// the body the receiver runs on creation, in that destination's one form.
|
|
64
|
+
/// @dev The destination travels inside the payload because `bootstrap` takes no other
|
|
65
|
+
/// argument naming it.
|
|
66
|
+
function encodeBootstrap(bytes memory destinationChainIdentifier, bytes memory body)
|
|
67
|
+
internal
|
|
68
|
+
pure
|
|
69
|
+
returns (bytes memory)
|
|
70
|
+
{
|
|
71
|
+
return abi.encode(destinationChainIdentifier, body);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function decodeBootstrap(bytes calldata payload)
|
|
75
|
+
internal
|
|
76
|
+
pure
|
|
77
|
+
returns (bytes memory destinationChainIdentifier, bytes memory body)
|
|
78
|
+
{
|
|
79
|
+
return abi.decode(payload, (bytes, bytes));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* ================================ selection ================================= */
|
|
83
|
+
|
|
84
|
+
/// @notice Whether a destination takes the typed form.
|
|
85
|
+
/// @dev Takes the identifier, not the chainKey, which is a hash and cannot be asked its
|
|
86
|
+
/// chain type.
|
|
87
|
+
function isTypedDestination(bytes memory chainIdentifier) internal pure returns (bool) {
|
|
88
|
+
return Erc7930.parseStrict(chainIdentifier).chainType == Erc7930.CT_EIP155;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.20;
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
AccessControlEnumerableUpgradeable
|
|
6
|
+
} from "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol";
|
|
7
|
+
import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
|
|
8
|
+
import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol";
|
|
9
|
+
|
|
10
|
+
/// @title Roles
|
|
11
|
+
/// @notice Which transports may carry this contract's messages, in either direction.
|
|
12
|
+
///
|
|
13
|
+
/// @dev `GATEWAY_ROLE` names addresses, not powers: a gateway can neither configure a contract
|
|
14
|
+
/// nor grant anything. Configuration is `Ownable` on the transceiver, except bootstrap fees,
|
|
15
|
+
/// which an operator the owner names may also set. Sending and receiving check
|
|
16
|
+
/// the same role, so a contract cannot accept deliveries from one transport while sending
|
|
17
|
+
/// through another. `ReceiverBase` inherits this directly: it never sends, but the role
|
|
18
|
+
/// guards its external `receiveMessage`.
|
|
19
|
+
///
|
|
20
|
+
/// @dev Not enumerable here: a transceiver's members are fixed at initialization, each grant
|
|
21
|
+
/// emits `RoleGranted`, and enumeration would cost every transceiver a kilobyte it does
|
|
22
|
+
/// not have under EIP-170 (#29). Conduits, whose receivers can revoke, add it through
|
|
23
|
+
/// `RolesEnumerable`.
|
|
24
|
+
abstract contract Roles is AccessControlUpgradeable {
|
|
25
|
+
/// @notice May deliver a message to this contract and carry one out of it. The only role.
|
|
26
|
+
/// @dev Namespaced so a provider SDK's own role of the same name cannot share its members.
|
|
27
|
+
bytes32 public constant GATEWAY_ROLE = keccak256("crossecute.role.GATEWAY");
|
|
28
|
+
|
|
29
|
+
/// @dev The caller of `grantRole` was not this contract.
|
|
30
|
+
error GrantNotFromSelf(address caller);
|
|
31
|
+
|
|
32
|
+
/// @notice Grant a role from this contract's own bootstrap payload, so an owner's first
|
|
33
|
+
/// payload can name its conduit's gateway. Closes when initialization does.
|
|
34
|
+
///
|
|
35
|
+
/// @dev `onlyInitializing` replaces OZ's admin gate. No role has an admin, so after arming
|
|
36
|
+
/// no caller can add a member: not the owner, the governor, the creating transceiver, or a
|
|
37
|
+
/// member. The flag stays set while `__ReceiverBase_init` runs the payload, so every
|
|
38
|
+
/// contract the payload reaches would be inside the window too; only a self-call from
|
|
39
|
+
/// `_execute` is accepted (#68).
|
|
40
|
+
///
|
|
41
|
+
/// @dev `public` because it overrides OZ's `grantRole`; a separate entry point would leave
|
|
42
|
+
/// the inherited one reachable.
|
|
43
|
+
function grantRole(bytes32 role, address conduit) public virtual override onlyInitializing {
|
|
44
|
+
if (msg.sender != address(this)) revert GrantNotFromSelf(msg.sender);
|
|
45
|
+
_grantRole(role, conduit);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// @notice An initializer's own grant, such as the provider endpoint it names as a gateway.
|
|
49
|
+
function _grantInitialRole(bytes32 role, address conduit) internal onlyInitializing {
|
|
50
|
+
_grantRole(role, conduit);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// @notice Whether `conduit` holds `role`.
|
|
54
|
+
/// @dev Re-declared so an override below names one base.
|
|
55
|
+
function hasRole(bytes32 role, address conduit) public view virtual override returns (bool) {
|
|
56
|
+
return super.hasRole(role, conduit);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// @title RolesEnumerable
|
|
61
|
+
/// @notice `Roles`, with the member set readable on-chain, for conduits: a receiver can drop a
|
|
62
|
+
/// gateway through `revokeGateway`, so what remains is worth reading directly.
|
|
63
|
+
/// @dev `AccessControlEnumerableUpgradeable` compiles at `paris` only through OZ 5.4, since
|
|
64
|
+
/// 5.5's `Arrays` uses `mcopy`; a bump past 5.4 breaks this inheritance first (README,
|
|
65
|
+
/// Assumptions).
|
|
66
|
+
abstract contract RolesEnumerable is Roles, AccessControlEnumerableUpgradeable {
|
|
67
|
+
function grantRole(bytes32 role, address conduit)
|
|
68
|
+
public
|
|
69
|
+
virtual
|
|
70
|
+
override(Roles, AccessControlUpgradeable, IAccessControl)
|
|
71
|
+
onlyInitializing
|
|
72
|
+
{
|
|
73
|
+
super.grantRole(role, conduit);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function hasRole(bytes32 role, address conduit)
|
|
77
|
+
public
|
|
78
|
+
view
|
|
79
|
+
virtual
|
|
80
|
+
override(Roles, AccessControlUpgradeable, IAccessControl)
|
|
81
|
+
returns (bool)
|
|
82
|
+
{
|
|
83
|
+
return super.hasRole(role, conduit);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function _grantRole(bytes32 role, address conduit)
|
|
87
|
+
internal
|
|
88
|
+
virtual
|
|
89
|
+
override(AccessControlUpgradeable, AccessControlEnumerableUpgradeable)
|
|
90
|
+
returns (bool)
|
|
91
|
+
{
|
|
92
|
+
return super._grantRole(role, conduit);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function _revokeRole(bytes32 role, address conduit)
|
|
96
|
+
internal
|
|
97
|
+
virtual
|
|
98
|
+
override(AccessControlUpgradeable, AccessControlEnumerableUpgradeable)
|
|
99
|
+
returns (bool)
|
|
100
|
+
{
|
|
101
|
+
return super._revokeRole(role, conduit);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function supportsInterface(bytes4 interfaceId)
|
|
105
|
+
public
|
|
106
|
+
view
|
|
107
|
+
virtual
|
|
108
|
+
override(AccessControlUpgradeable, AccessControlEnumerableUpgradeable)
|
|
109
|
+
returns (bool)
|
|
110
|
+
{
|
|
111
|
+
return super.supportsInterface(interfaceId);
|
|
112
|
+
}
|
|
113
|
+
}
|