@crossecute/protocol 0.0.0 → 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 +25 -1
  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 +34 -3
@@ -0,0 +1,122 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.20;
3
+
4
+ /// @title Blake2b256
5
+ /// @notice Full BLAKE2b-256 built on the EIP-152 `blake2f` compression precompile (0x09).
6
+ ///
7
+ /// @dev The precompile provides only the F compression function, not a hash. This library
8
+ /// supplies the parameter block, the message padding, the byte-offset counters, and
9
+ /// the final-block flag around it.
10
+ ///
11
+ /// Endianness. EIP-152 takes and returns the state `h` and message `m` as
12
+ /// little-endian 64-bit words. Because we carry `h` as opaque 64 raw bytes and pass
13
+ /// message blocks through verbatim, no word swapping is ever needed: only the
14
+ /// parameter-block XOR and the `t` counter are written in explicit little-endian.
15
+ /// This is the single most common source of wrong BLAKE2b ports; it is avoided here
16
+ /// rather than handled.
17
+ ///
18
+ /// Mutability. Functions here are `view`, not `pure`. Solidity forbids `staticcall`
19
+ /// inside `pure`, and there is no `blake2b` builtin the way there is for `sha256`
20
+ /// and `ripemd160`. Callers that need `pure` cannot use this; pass the digest in.
21
+ ///
22
+ /// Gas. The precompile costs 1 gas per round, 12 rounds per 128-byte block, so the
23
+ /// compression itself is free in practice. Cost is dominated by memory and the
24
+ /// staticcall overhead: measured at roughly 3k gas for a single-block input.
25
+ library Blake2b256 {
26
+ /// @dev BLAKE2b IV, little-endian bytes, with h[0] already XORed by the parameter
27
+ /// block 0x01010020 (fanout=1, depth=1, key length=0, digest length=32).
28
+ /// 0x6a09e667f3bcc908 ^ 0x01010020 == 0x6a09e667f2bdc928.
29
+ bytes private constant IV_PARAM = hex"28c9bdf267e6096a" hex"3ba7ca8485ae67bb" hex"2bf894fe72f36e3c"
30
+ hex"f1361d5f3af54fa5" hex"d182e6ad7f520e51" hex"1f6c3e2b8c68059b" hex"6bbd41fbabd9831f" hex"79217e1319cde05b";
31
+
32
+ uint32 private constant ROUNDS = 12;
33
+ uint256 private constant BLOCK_SIZE = 128;
34
+
35
+ error PrecompileFailed();
36
+
37
+ /// @notice BLAKE2b-256 over an arbitrary-length input, unkeyed.
38
+ /// @dev Handles the empty input, exact-multiple-of-128 inputs, and multi-block
39
+ /// inputs. All three are separately covered by the test vectors.
40
+ function hash(bytes memory input) internal view returns (bytes32) {
41
+ bytes memory h = IV_PARAM; // 64 bytes, mutated in place across blocks
42
+
43
+ uint256 len = input.length;
44
+ if (len == 0) {
45
+ // Single all-zero final block with counter 0.
46
+ h = _compress(h, new bytes(BLOCK_SIZE), 0, true);
47
+ } else {
48
+ uint256 nBlocks = (len + BLOCK_SIZE - 1) / BLOCK_SIZE;
49
+ for (uint256 i; i < nBlocks; ++i) {
50
+ bool last = (i == nBlocks - 1);
51
+ uint256 offset = i * BLOCK_SIZE;
52
+ // Counter is bytes compressed so far including this block. For the
53
+ // final block that is the true message length, not a padded length.
54
+ // forge-lint: disable-next-line(unsafe-typecast) a memory length fits in 64 bits
55
+ uint64 t = uint64(last ? len : offset + BLOCK_SIZE);
56
+ h = _compress(h, _blockAt(input, offset, len), t, last);
57
+ }
58
+ }
59
+
60
+ // Digest is the first 32 bytes of the state, already in output byte order.
61
+ bytes32 out;
62
+ assembly {
63
+ out := mload(add(h, 32))
64
+ }
65
+ return out;
66
+ }
67
+
68
+ /// @dev Extracts a 128-byte block starting at `offset`, zero-padded if the message
69
+ /// ends early. Zero padding is part of the BLAKE2b spec, not an artifact.
70
+ function _blockAt(bytes memory input, uint256 offset, uint256 len) private pure returns (bytes memory blk) {
71
+ // forge-lint: disable-next-line(calls-loop) allocation, not a call
72
+ blk = new bytes(BLOCK_SIZE);
73
+ uint256 n = len - offset;
74
+ if (n > BLOCK_SIZE) n = BLOCK_SIZE;
75
+ for (uint256 i; i < n; ++i) {
76
+ blk[i] = input[offset + i];
77
+ }
78
+ }
79
+
80
+ /// @dev One call to the EIP-152 F compression function.
81
+ /// Input layout is exactly 213 bytes:
82
+ /// rounds 4 bytes, big-endian uint32
83
+ /// h 64 bytes, 8 little-endian uint64
84
+ /// m 128 bytes, 16 little-endian uint64
85
+ /// t 16 bytes, t0 then t1, each little-endian uint64
86
+ /// f 1 byte, 0x01 on the final block
87
+ function _compress(bytes memory h, bytes memory blk, uint64 t0, bool last) private view returns (bytes memory out) {
88
+ // Every field is fixed-width.
89
+ // forge-lint: disable-start(encode-packed-collision)
90
+ bytes memory input = abi.encodePacked(
91
+ ROUNDS, // 4, big-endian by abi.encodePacked
92
+ h, // 64
93
+ blk, // 128
94
+ _le64(t0), // 8 (t0)
95
+ bytes8(0), // 8 (t1: messages beyond 2^64 bytes are not representable)
96
+ last ? bytes1(0x01) : bytes1(0x00) // 1
97
+ );
98
+ // forge-lint: disable-end(encode-packed-collision)
99
+
100
+ // forge-lint: disable-next-line(calls-loop) allocation, not a call
101
+ out = new bytes(64);
102
+ bool ok;
103
+ assembly {
104
+ // Without EIP-152, 0x09 may be empty (success, no data), revert (zkSync Era), or
105
+ // hold another precompile (Tron's BatchValidateSign returns 32 bytes), so the size
106
+ // is checked too, or a digest could be zeros or foreign output.
107
+ ok := staticcall(gas(), 0x09, add(input, 32), 213, add(out, 32), 64)
108
+ ok := and(ok, eq(returndatasize(), 64))
109
+ }
110
+ if (!ok) revert PrecompileFailed();
111
+ }
112
+
113
+ /// @dev uint64 -> 8 little-endian bytes.
114
+ function _le64(uint64 x) private pure returns (bytes8 r) {
115
+ uint64 v;
116
+ for (uint256 i; i < 8; ++i) {
117
+ v = (v << 8) | (x & 0xff);
118
+ x >>= 8;
119
+ }
120
+ r = bytes8(v);
121
+ }
122
+ }
@@ -0,0 +1,35 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.20;
3
+
4
+ /// @notice Uniform address derivation across every VM the protocol targets.
5
+ ///
6
+ /// @dev The normalization problem. Every VM's derivation takes different inputs: CREATE2
7
+ /// wants (deployer, salt, initCodeHash), a Solana PDA wants (seeds[], bump,
8
+ /// programId), CosmWasm wants (checksum, creator, salt, initMsg). There is no
9
+ /// argument list that fits all of them, so the parameters stay opaque: the caller
10
+ /// passes an ABI-encoded blob and the deriver decodes the shape its scheme requires.
11
+ ///
12
+ /// What is uniform is the two ends. Every implementation takes a canonical ERC-7930
13
+ /// chain identifier plus a params blob, and returns a canonical ERC-7930 conduit
14
+ /// envelope for the same chain. The registry can therefore store the result without
15
+ /// knowing which VM produced it.
16
+ ///
17
+ /// @dev Kept apart from `VmDeriver` so that `ChainRegistry`, which every chain deploys, does
18
+ /// not import `BitcoinDerive`: zksolc rejects any unit containing `ripemd160`.
19
+ ///
20
+ /// @dev Must be `view`, not `pure`. Sui's BLAKE2b-256 goes through precompile 0x09, which
21
+ /// is a `staticcall`. One non-pure member forces the whole interface to `view`.
22
+ interface IVmDeriver {
23
+ /// @param chainIdentifier Canonical ERC-7930 chain identifier (zero-length address).
24
+ /// @param params `abi.encode(Scheme, bytes)`: see `VmDeriver.Scheme`.
25
+ /// @return interop Canonical ERC-7930 conduit envelope on that same chain.
26
+ function deriveAddress(bytes calldata chainIdentifier, bytes calldata params)
27
+ external
28
+ view
29
+ returns (bytes memory interop);
30
+
31
+ /// @notice Whether this deriver can serve `scheme` on `chainType`.
32
+ /// @dev Lets the registry reject a misconfiguration at wiring time rather than at
33
+ /// the first resolve.
34
+ function supportsScheme(uint16 chainType, uint8 scheme) external pure returns (bool);
35
+ }
@@ -0,0 +1,93 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.20;
3
+
4
+ /// @title StarknetDerive
5
+ /// @notice What can and cannot be computed about Starknet from the EVM.
6
+ ///
7
+ /// @dev Constants are sourced, not recalled. All values below come from
8
+ /// starkware-libs/cairo-lang:
9
+ /// - starknet/definitions/constants.py :: L2_ADDRESS_UPPER_BOUND
10
+ /// - starknet/public/abi.py :: ADDR_BOUND, MASK_250, starknet_keccak
11
+ /// - starknet/core/os/contract_address/contract_address.py
12
+ ///
13
+ /// @dev What is derivable here
14
+ /// - `starknetKeccak` / `selector`: masked keccak256. Native opcode, ~free.
15
+ /// This is how Starknet entry-point selectors are computed, so a payload
16
+ /// targeting a Starknet function can commit to its selector on-chain.
17
+ /// - Range validation of addresses, class hashes, and felts.
18
+ ///
19
+ /// @dev What is not
20
+ /// Contract addresses. The derivation is a Pedersen hash chain:
21
+ /// raw = H(H(H(H(H(0, prefix), deployer), salt), classHash), ctorHash), 5)
22
+ /// address = raw mod L2_ADDRESS_UPPER_BOUND
23
+ /// where prefix is the felt encoding of the ASCII "STARKNET_CONTRACT_ADDRESS"
24
+ /// and H is Pedersen over the STARK curve.
25
+ ///
26
+ /// Pedersen is not merely expensive here, it is structurally awkward: it needs
27
+ /// roughly 500 precomputed curve points (~32kb), which exceeds EIP-170 outright,
28
+ /// so a real implementation means SSTORE2-chunked tables plus projective EC
29
+ /// arithmetic over a 252-bit prime, at 1e5-1e6 gas with a silent-failure mode.
30
+ ///
31
+ /// Recommendation: do not build it. The payoff is upgrading Starknet from
32
+ /// `Unique` to `Predetermined`, and the address can be recorded from a signed payload
33
+ /// instead, where the grade states the weaker guarantee rather than hiding it.
34
+ library StarknetDerive {
35
+ /// @dev FIELD_PRIME = 2^251 + 17 * 2^192 + 1
36
+ uint256 internal constant FIELD_PRIME = 0x0800000000000011000000000000000000000000000000000000000000000001;
37
+
38
+ /// @dev L2_ADDRESS_UPPER_BOUND = 2^251 - 256. Note the "- 256": a naive `< 2^251`
39
+ /// check admits 256 values that can never be a Starknet contract address,
40
+ /// because `calculate_contract_address_from_hash` reduces mod this bound.
41
+ uint256 internal constant L2_ADDRESS_UPPER_BOUND = (1 << 251) - 256;
42
+
43
+ /// @dev MASK_250 = 2^250 - 1
44
+ uint256 internal constant MASK_250 = (1 << 250) - 1;
45
+
46
+ /// @dev CONTRACT_ADDRESS_PREFIX = felt encoding of ASCII "STARKNET_CONTRACT_ADDRESS"
47
+ /// (25 bytes, big-endian). Present so an off-chain derivation can be checked
48
+ /// against the same constant the contract enforces.
49
+ uint256 internal constant CONTRACT_ADDRESS_PREFIX = 0x535441524b4e45545f434f4e54524143545f41444452455353;
50
+
51
+ error AddressOutOfRange();
52
+ error FeltOutOfRange();
53
+ error ClassHashOutOfRange();
54
+ error BadAddressWidth();
55
+
56
+ /// @notice Starknet's keccak variant: keccak256 truncated to 250 bits so the
57
+ /// result fits a field element.
58
+ /// @dev cairo-lang: `from_bytes(keccak(data)) & MASK_250`.
59
+ function starknetKeccak(bytes memory data) internal pure returns (uint256) {
60
+ return uint256(keccak256(data)) & MASK_250;
61
+ }
62
+
63
+ /// @notice Entry-point selector for a function name.
64
+ /// @dev Selectors for `__default__` / `__l1_default__` are 0 by special case;
65
+ /// that is a caller concern, not encoded here.
66
+ function selector(string memory functionName) internal pure returns (uint256) {
67
+ return starknetKeccak(bytes(functionName));
68
+ }
69
+
70
+ /// @notice A contract address must be strictly below L2_ADDRESS_UPPER_BOUND.
71
+ function validateAddress(uint256 addr) internal pure {
72
+ if (addr >= L2_ADDRESS_UPPER_BOUND) revert AddressOutOfRange();
73
+ }
74
+
75
+ /// @notice A generic field element must be below FIELD_PRIME. Class hashes use
76
+ /// this wider bound (CLASS_HASH_UPPER_BOUND == FIELD_SIZE), not the
77
+ /// address bound.
78
+ function validateFelt(uint256 v) internal pure {
79
+ if (v >= FIELD_PRIME) revert FeltOutOfRange();
80
+ }
81
+
82
+ function validateClassHash(uint256 v) internal pure {
83
+ if (v >= FIELD_PRIME) revert ClassHashOutOfRange();
84
+ }
85
+
86
+ /// @notice Read a 32-byte Starknet address out of ERC-7930 address bytes.
87
+ function toFelt(bytes memory addr) internal pure returns (uint256 v) {
88
+ if (addr.length != 32) revert BadAddressWidth();
89
+ assembly {
90
+ v := mload(add(addr, 32))
91
+ }
92
+ }
93
+ }
@@ -0,0 +1,102 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.20;
3
+
4
+ import {Blake2b256} from "./Blake2b256.sol";
5
+
6
+ /// @title SuiDerive
7
+ /// @notice Sui address and object-ID derivation, executable on Ethereum.
8
+ /// @dev Every constant below was read from MystenLabs/sui rather than recalled.
9
+ library SuiDerive {
10
+ using Blake2b256 for bytes;
11
+
12
+ /* ------------------------------- flags -------------------------------- */
13
+ // crates/sui-types/src/crypto.rs :: SignatureScheme::flag()
14
+ uint8 internal constant FLAG_ED25519 = 0x00;
15
+ uint8 internal constant FLAG_SECP256K1 = 0x01;
16
+ uint8 internal constant FLAG_SECP256R1 = 0x02;
17
+ uint8 internal constant FLAG_MULTISIG = 0x03;
18
+ uint8 internal constant FLAG_BLS12381 = 0x04; // not valid for a user address
19
+ uint8 internal constant FLAG_ZKLOGIN = 0x05;
20
+ uint8 internal constant FLAG_PASSKEY = 0x06;
21
+
22
+ /* --------------------------- intent scopes ---------------------------- */
23
+ // crates/shared-crypto/src/intent.rs :: HashingIntentScope
24
+ // Note: these are 0xf0/0xf1, not 0/1. Assuming a zero-based enum here silently
25
+ // produces well-formed but wrong object IDs.
26
+ uint8 internal constant SCOPE_CHILD_OBJECT_ID = 0xf0;
27
+ uint8 internal constant SCOPE_REGULAR_OBJECT_ID = 0xf1;
28
+
29
+ error InvalidFlag();
30
+ error LengthMismatch();
31
+
32
+ /// @notice Sui address = blake2b256(flag ‖ pubkey).
33
+ /// @dev crates/sui-types/src/base_types.rs :: impl From<&PublicKey> for SuiAddress
34
+ /// @param flag One of the FLAG_* constants.
35
+ /// @param pubkey Raw public key bytes. Ed25519 is 32; secp256k1/r1 are 33
36
+ /// (SEC1 compressed). Length is checked where it is fixed.
37
+ function addressFromPubkey(uint8 flag, bytes memory pubkey) internal view returns (bytes32) {
38
+ if (flag == FLAG_ED25519) {
39
+ if (pubkey.length != 32) revert LengthMismatch();
40
+ } else if (flag == FLAG_SECP256K1 || flag == FLAG_SECP256R1 || flag == FLAG_PASSKEY) {
41
+ if (pubkey.length != 33) revert LengthMismatch();
42
+ } else if (flag != FLAG_ZKLOGIN) {
43
+ // MultiSig has its own constructor; BLS is not a valid user address.
44
+ revert InvalidFlag();
45
+ }
46
+ return Blake2b256.hash(abi.encodePacked(flag, pubkey));
47
+ }
48
+
49
+ /// @notice MultiSig address.
50
+ /// @dev blake2b256( 0x03 ‖ le16(threshold) ‖ (flag_i ‖ pk_i ‖ weight_i)* )
51
+ /// Threshold is u16 little-endian; each weight is u8. Order of the member
52
+ /// list is significant: it must match the on-chain MultiSigPublicKey.
53
+ /// @param flags Per-member scheme flags.
54
+ /// @param pubkeys Per-member raw public keys, same order as `flags`.
55
+ /// @param weights Per-member weights, same order.
56
+ function addressFromMultisig(uint16 threshold, uint8[] memory flags, bytes[] memory pubkeys, uint8[] memory weights)
57
+ internal
58
+ view
59
+ returns (bytes32)
60
+ {
61
+ uint256 n = flags.length;
62
+ if (pubkeys.length != n || weights.length != n) revert LengthMismatch();
63
+
64
+ bytes memory pre = abi.encodePacked(FLAG_MULTISIG, _le16(threshold));
65
+ for (uint256 i; i < n; ++i) {
66
+ // forge-lint: disable-next-line(encode-packed-collision) Sui's multisig preimage; each key's length is fixed by its flag
67
+ pre = abi.encodePacked(pre, flags[i], pubkeys[i], weights[i]);
68
+ }
69
+ return Blake2b256.hash(pre);
70
+ }
71
+
72
+ /// @notice Object ID created by a transaction.
73
+ /// @dev crates/sui-types/src/base_types.rs :: ObjectID::derive_id
74
+ /// blake2b256( 0xf1 ‖ txDigest ‖ le64(creationNum) ), truncated to 32 bytes
75
+ /// (already 32, so the truncation is a no-op).
76
+ ///
77
+ /// This is derivable but not counterfactual: `txDigest` does not exist until
78
+ /// the transaction is built and signed, and `creationNum` depends on how many
79
+ /// IDs that transaction already created. Use it to verify an object ID
80
+ /// reported back by the destination, not to predict one in advance.
81
+ function deriveObjectId(bytes32 txDigest, uint64 creationNum) internal view returns (bytes32) {
82
+ return Blake2b256.hash(abi.encodePacked(SCOPE_REGULAR_OBJECT_ID, txDigest, _le64(creationNum)));
83
+ }
84
+
85
+ /// @notice Dynamic-field / child object ID variant.
86
+ function deriveChildObjectId(bytes32 txDigest, uint64 creationNum) internal view returns (bytes32) {
87
+ return Blake2b256.hash(abi.encodePacked(SCOPE_CHILD_OBJECT_ID, txDigest, _le64(creationNum)));
88
+ }
89
+
90
+ function _le16(uint16 x) private pure returns (bytes2) {
91
+ return bytes2(uint16((uint16(x & 0xff) << 8) | (x >> 8)));
92
+ }
93
+
94
+ function _le64(uint64 x) private pure returns (bytes8 r) {
95
+ uint64 v;
96
+ for (uint256 i; i < 8; ++i) {
97
+ v = (v << 8) | (x & 0xff);
98
+ x >>= 8;
99
+ }
100
+ r = bytes8(v);
101
+ }
102
+ }
@@ -0,0 +1,291 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.20;
3
+
4
+ import {ChainType} from "../addressing/ChainType.sol";
5
+ import {AddressDerive} from "./AddressDerive.sol";
6
+ import {BitcoinDerive} from "./BitcoinDerive.sol";
7
+ import {SuiDerive} from "./SuiDerive.sol";
8
+ import {Erc7930} from "../addressing/Erc7930.sol";
9
+ import {IVmDeriver} from "./IVmDeriver.sol";
10
+
11
+ /// @title VmDeriver
12
+ /// @notice One standalone, stateless contract implementing every derivation the protocol
13
+ /// can perform on-chain, dispatched by (ChainType, Scheme).
14
+ ///
15
+ /// @dev Why one contract and not one per VM. These are pure functions over calldata with
16
+ /// no storage and no privileges, so there is nothing to isolate between them. A
17
+ /// single deployment means one address to audit and one address to register for
18
+ /// every chain, and the registry's `deriverOf` mapping still allows a per-chain
19
+ /// override later if a VM ever needs its own.
20
+ ///
21
+ /// @dev Dispatch is two-dimensional, and it has to be. ChainType alone is not enough:
22
+ /// Ethereum, zkSync Era, and Tron are all `eip155`, with three different CREATE2
23
+ /// formulas. Scheme alone is not enough either, since the same scheme must produce
24
+ /// envelopes on different chains. Every scheme is therefore checked against the
25
+ /// chain type it is legal for, in `supportsScheme`.
26
+ ///
27
+ /// @dev Not derivable here, by construction:
28
+ /// - Aptos / Movement: SHA3-256 is not keccak256 (padding domain 0x06 vs 0x01).
29
+ /// A hand-rolled Keccak-f[1600] is ~1e5 gas.
30
+ /// - Starknet: Pedersen/Poseidon over the STARK curve. No precompile, and the
31
+ /// point tables exceed EIP-170 outright.
32
+ /// - Bitcoin P2TR: taproot output keys need secp256k1 point addition; ecrecover
33
+ /// is not a general curve operation.
34
+ /// These revert with `UnsupportedScheme`. Route them through the registry's
35
+ /// committed or `Unique` path: that is exactly what the provenance grades are for.
36
+ contract VmDeriver is IVmDeriver {
37
+ /// @notice Which derivation to run. The chain type says which VM; this says which
38
+ /// formula within it.
39
+ enum Scheme {
40
+ Unset,
41
+ /// EIP-1014. (address deployer, bytes32 salt, bytes32 initCodeHash)
42
+ EvmCreate2,
43
+ /// (address factory, bytes32 salt): bytecode-independent.
44
+ EvmCreate3,
45
+ /// Nonce-based. (address deployer, uint256 nonce)
46
+ EvmCreate,
47
+ /// (address sender, bytes32 salt, bytes32 bytecodeHash, bytes32 ctorInputHash)
48
+ ZkSyncCreate2,
49
+ /// (address sender, uint256 nonce)
50
+ ZkSyncCreate,
51
+ /// (address deployer, bytes32 salt, bytes32 initCodeHash)
52
+ TronCreate2,
53
+ /// (bytes32 checksum, bytes creator, bytes salt, bytes initMsg, uint8 addrLen)
54
+ CosmosInstantiate2,
55
+ /// (bytes[] seeds, uint8 bump, bytes32 programId)
56
+ SolanaPda,
57
+ /// (bytes32 owner, bytes32 mint, bytes32 tokenProgram, bytes32 ataProgram, uint8 bump)
58
+ SolanaAta,
59
+ /// (bytes32 base, bytes seed, bytes32 programId)
60
+ SolanaCreateWithSeed,
61
+ /// (bytes borshStateInit) -> 20 bytes, rendered "0s"+hex off-chain
62
+ NearDeterministic,
63
+ /// (bytes uncompressedPubkey64) -> 20 bytes
64
+ NearEthImplicit,
65
+ /// (bytes32 ed25519Pubkey) -> 32 bytes
66
+ NearImplicit,
67
+ /// (bytes pubkey) -> 20-byte HASH160 witness program
68
+ BitcoinP2wpkh,
69
+ /// (bytes witnessScript) -> 32-byte SHA256 witness program
70
+ BitcoinP2wsh,
71
+ /// (bytes redeemScript) -> 20-byte HASH160
72
+ BitcoinP2sh,
73
+ /// (uint8 flag, bytes pubkey)
74
+ SuiAddress,
75
+ /// (uint16 threshold, uint8[] flags, bytes[] pubkeys, uint8[] weights)
76
+ SuiMultisig,
77
+ /// (bytes32 txDigest, uint64 creationNum): verification only, see note.
78
+ SuiObjectId,
79
+ /// EIP-1167 clone. (address deployer, address implementation, bytes32 salt)
80
+ /// This is how a receiver address is predicted: deployer is the destination
81
+ /// transceiver, salt is `keccak256(abi.encode(transmitter))`.
82
+ EvmClone,
83
+ /// EIP-1167 clone under Tron's 0x41 domain byte. Same params as EvmClone.
84
+ TronClone
85
+ }
86
+
87
+ error UnsupportedScheme(uint16 chainType, uint8 scheme);
88
+
89
+ error NotChainIdentifier();
90
+ error BadCosmosAddressLength();
91
+
92
+ /// @notice Creation-code hash of an EIP-1167 clone. Exposed so the same value can be
93
+ /// computed off-chain and pinned in a signed payload.
94
+ function cloneInitCodeHash(address implementation) external pure returns (bytes32) {
95
+ return AddressDerive.cloneInitCodeHash(implementation);
96
+ }
97
+
98
+ /* ================================ dispatch ================================= */
99
+
100
+ /// @inheritdoc IVmDeriver
101
+ function deriveAddress(bytes calldata chainIdentifier, bytes calldata params)
102
+ external
103
+ view
104
+ override
105
+ returns (bytes memory interop)
106
+ {
107
+ Erc7930.Interop memory io = Erc7930.parseStrict(chainIdentifier);
108
+ // A chain identifier carries no conduit. Being handed a populated envelope means
109
+ // the caller is confused about which end of the derivation it is on.
110
+ if (io.addr.length != 0) revert NotChainIdentifier();
111
+
112
+ (Scheme scheme, bytes memory inner) = abi.decode(params, (Scheme, bytes));
113
+ if (!supportsScheme(io.chainType, uint8(scheme))) {
114
+ revert UnsupportedScheme(io.chainType, uint8(scheme));
115
+ }
116
+
117
+ return Erc7930.encode(io.chainType, io.chainRef, _derive(scheme, inner));
118
+ }
119
+
120
+ /// @inheritdoc IVmDeriver
121
+ /// @dev The legality table. Ethereum/zkSync/Tron all being `eip155` is why this is
122
+ /// keyed on the pair and not on either half alone: the registry pins the exact
123
+ /// scheme per chainKey, so a zkSync chainKey cannot be resolved with Ethereum's
124
+ /// formula just because both are eip155.
125
+ function supportsScheme(uint16 chainType, uint8 scheme) public pure override returns (bool) {
126
+ Scheme s = Scheme(scheme);
127
+
128
+ if (chainType == ChainType.EIP155) {
129
+ return s == Scheme.EvmCreate2 || s == Scheme.EvmCreate3 || s == Scheme.EvmCreate
130
+ || s == Scheme.ZkSyncCreate2 || s == Scheme.ZkSyncCreate || s == Scheme.TronCreate2
131
+ || s == Scheme.EvmClone || s == Scheme.TronClone;
132
+ }
133
+ if (chainType == ChainType.SOLANA) {
134
+ return s == Scheme.SolanaPda || s == Scheme.SolanaAta || s == Scheme.SolanaCreateWithSeed;
135
+ }
136
+ if (chainType == ChainType.BIP122) {
137
+ return s == Scheme.BitcoinP2wpkh || s == Scheme.BitcoinP2wsh || s == Scheme.BitcoinP2sh;
138
+ }
139
+ if (chainType == ChainType.COSMOS) {
140
+ return s == Scheme.CosmosInstantiate2;
141
+ }
142
+ if (chainType == ChainType.NEAR) {
143
+ return s == Scheme.NearDeterministic || s == Scheme.NearEthImplicit || s == Scheme.NearImplicit;
144
+ }
145
+ if (chainType == ChainType.SUI) {
146
+ return s == Scheme.SuiAddress || s == Scheme.SuiMultisig || s == Scheme.SuiObjectId;
147
+ }
148
+ // ChainType.Aptos and ChainType.Starknet are deliberately absent.
149
+ return false;
150
+ }
151
+
152
+ /* =============================== derivations =============================== */
153
+
154
+ /// @dev Returns the raw address bytes for the scheme, at the width that chain's
155
+ /// ERC-7930 profile expects. The caller wraps them in the envelope.
156
+ function _derive(Scheme s, bytes memory p) private view returns (bytes memory) {
157
+ /* ---------------------------------- EVM --------------------------------- */
158
+ if (s == Scheme.EvmCreate2) {
159
+ (address deployer, bytes32 salt, bytes32 initCodeHash) = abi.decode(p, (address, bytes32, bytes32));
160
+ return abi.encodePacked(AddressDerive.create2(deployer, salt, initCodeHash));
161
+ }
162
+ if (s == Scheme.EvmCreate3) {
163
+ (address factory, bytes32 salt) = abi.decode(p, (address, bytes32));
164
+ return abi.encodePacked(AddressDerive.create3(factory, salt));
165
+ }
166
+ if (s == Scheme.EvmCreate) {
167
+ (address deployer, uint256 nonce) = abi.decode(p, (address, uint256));
168
+ return abi.encodePacked(AddressDerive.create(deployer, nonce));
169
+ }
170
+
171
+ /* -------------------------------- clones -------------------------------- */
172
+ // Note on zkSYNC: EIP-1167 has no zkSync equivalent. EraVM will only deploy
173
+ // bytecode whose hash has been published, and a 55-byte EVM proxy is not valid
174
+ // EraVM bytecode at all. A zkSync "clone" is a real proxy contract compiled with
175
+ // zksolc, so it is derived with `ZkSyncCreate2` using that proxy's versioned
176
+ // bytecode hash: no separate scheme, because it is not a clone in this sense.
177
+ if (s == Scheme.EvmClone) {
178
+ (address deployer, address implementation, bytes32 salt) = abi.decode(p, (address, address, bytes32));
179
+ return abi.encodePacked(AddressDerive.clone2(deployer, implementation, salt));
180
+ }
181
+ if (s == Scheme.TronClone) {
182
+ (address deployer, address implementation, bytes32 salt) = abi.decode(p, (address, address, bytes32));
183
+ return abi.encodePacked(
184
+ AddressDerive.tronCreate2(deployer, salt, AddressDerive.cloneInitCodeHash(implementation))
185
+ );
186
+ }
187
+
188
+ /* -------------------------------- zkSync -------------------------------- */
189
+ if (s == Scheme.ZkSyncCreate2) {
190
+ (address sender, bytes32 salt, bytes32 bytecodeHash, bytes32 ctorInputHash) =
191
+ abi.decode(p, (address, bytes32, bytes32, bytes32));
192
+ return abi.encodePacked(AddressDerive.zksyncCreate2(sender, salt, bytecodeHash, ctorInputHash));
193
+ }
194
+ if (s == Scheme.ZkSyncCreate) {
195
+ (address sender, uint256 nonce) = abi.decode(p, (address, uint256));
196
+ return abi.encodePacked(AddressDerive.zksyncCreate(sender, nonce));
197
+ }
198
+
199
+ /* --------------------------------- Tron --------------------------------- */
200
+ if (s == Scheme.TronCreate2) {
201
+ (address deployer, bytes32 salt, bytes32 initCodeHash) = abi.decode(p, (address, bytes32, bytes32));
202
+ return abi.encodePacked(AddressDerive.tronCreate2(deployer, salt, initCodeHash));
203
+ }
204
+
205
+ /* -------------------------------- Cosmos -------------------------------- */
206
+ if (s == Scheme.CosmosInstantiate2) {
207
+ (bytes32 checksum, bytes memory creator, bytes memory salt, bytes memory initMsg, uint8 addrLen) =
208
+ abi.decode(p, (bytes32, bytes, bytes, bytes, uint8));
209
+ bytes32 full = AddressDerive.cosmosInstantiate2(checksum, creator, salt, initMsg);
210
+ // Canonical ADR-028 is 32 bytes. Forked wasmd chains that use 20 for
211
+ // Ethereum-ecosystem compatibility (Injective) truncate. Only set addrLen
212
+ // below 32 if you have confirmed the target forked ContractAddrLen:
213
+ // getting it wrong yields a well-formed address nothing will ever occupy.
214
+ if (addrLen == 0 || addrLen > 32) revert BadCosmosAddressLength();
215
+ if (addrLen == 32) return abi.encodePacked(full);
216
+ bytes memory out = new bytes(addrLen);
217
+ for (uint256 i; i < addrLen; ++i) {
218
+ out[i] = full[i];
219
+ }
220
+ return out;
221
+ }
222
+
223
+ /* -------------------------------- Solana -------------------------------- */
224
+ if (s == Scheme.SolanaPda) {
225
+ (bytes[] memory seeds, uint8 bump, bytes32 programId) = abi.decode(p, (bytes[], uint8, bytes32));
226
+ return abi.encodePacked(AddressDerive.solanaCreateProgramAddress(seeds, bump, programId));
227
+ }
228
+ if (s == Scheme.SolanaAta) {
229
+ (bytes32 owner, bytes32 mint, bytes32 tokenProgram, bytes32 ataProgram, uint8 bump) =
230
+ abi.decode(p, (bytes32, bytes32, bytes32, bytes32, uint8));
231
+ return
232
+ abi.encodePacked(
233
+ AddressDerive.solanaAssociatedTokenAccount(owner, mint, tokenProgram, ataProgram, bump)
234
+ );
235
+ }
236
+ if (s == Scheme.SolanaCreateWithSeed) {
237
+ (bytes32 base, bytes memory seed, bytes32 programId) = abi.decode(p, (bytes32, bytes, bytes32));
238
+ return abi.encodePacked(AddressDerive.solanaCreateWithSeed(base, seed, programId));
239
+ }
240
+
241
+ /* --------------------------------- NEAR --------------------------------- */
242
+ if (s == Scheme.NearDeterministic) {
243
+ bytes memory borshStateInit = abi.decode(p, (bytes));
244
+ return abi.encodePacked(AddressDerive.nearDeterministicAccount(borshStateInit));
245
+ }
246
+ if (s == Scheme.NearEthImplicit) {
247
+ bytes memory pubkey64 = abi.decode(p, (bytes));
248
+ return abi.encodePacked(AddressDerive.nearEthImplicitAccount(pubkey64));
249
+ }
250
+ if (s == Scheme.NearImplicit) {
251
+ // The ed25519 pubkey is the conduit id. Identity, kept explicit so the
252
+ // envelope width is unambiguous.
253
+ bytes32 pubkey = abi.decode(p, (bytes32));
254
+ return abi.encodePacked(pubkey);
255
+ }
256
+
257
+ /* -------------------------------- Bitcoin ------------------------------- */
258
+ if (s == Scheme.BitcoinP2wpkh) {
259
+ bytes memory pubkey = abi.decode(p, (bytes));
260
+ return abi.encodePacked(BitcoinDerive.hash160(pubkey));
261
+ }
262
+ if (s == Scheme.BitcoinP2wsh) {
263
+ bytes memory witnessScript = abi.decode(p, (bytes));
264
+ return abi.encodePacked(BitcoinDerive.p2wshProgram(witnessScript));
265
+ }
266
+ if (s == Scheme.BitcoinP2sh) {
267
+ bytes memory redeemScript = abi.decode(p, (bytes));
268
+ return abi.encodePacked(BitcoinDerive.p2shScriptHash(redeemScript));
269
+ }
270
+
271
+ /* ---------------------------------- Sui --------------------------------- */
272
+ if (s == Scheme.SuiAddress) {
273
+ (uint8 flag, bytes memory pubkey) = abi.decode(p, (uint8, bytes));
274
+ return abi.encodePacked(SuiDerive.addressFromPubkey(flag, pubkey));
275
+ }
276
+ if (s == Scheme.SuiMultisig) {
277
+ (uint16 threshold, uint8[] memory flags, bytes[] memory pubkeys, uint8[] memory weights) =
278
+ abi.decode(p, (uint16, uint8[], bytes[], uint8[]));
279
+ return abi.encodePacked(SuiDerive.addressFromMultisig(threshold, flags, pubkeys, weights));
280
+ }
281
+ if (s == Scheme.SuiObjectId) {
282
+ // Derivable but not counterfactual: txDigest does not exist until the
283
+ // transaction is built and signed. Use this to verify an object id the
284
+ // destination reported, never to predict one in advance.
285
+ (bytes32 txDigest, uint64 creationNum) = abi.decode(p, (bytes32, uint64));
286
+ return abi.encodePacked(SuiDerive.deriveObjectId(txDigest, creationNum));
287
+ }
288
+
289
+ revert UnsupportedScheme(0, uint8(s));
290
+ }
291
+ }