@across-protocol/contracts 5.0.16 → 5.0.19

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 (40) hide show
  1. package/contracts/interfaces/ICounterfactualBeacon.sol +91 -0
  2. package/contracts/interfaces/ICounterfactualDeposit.sol +21 -0
  3. package/contracts/libraries/TronClones.sol +13 -0
  4. package/contracts/periphery/counterfactual/CounterfactualBeacon.sol +115 -0
  5. package/contracts/periphery/counterfactual/CounterfactualBeaconBase.sol +121 -0
  6. package/contracts/periphery/counterfactual/CounterfactualBeaconBootstrap.sol +39 -0
  7. package/contracts/periphery/counterfactual/CounterfactualDeposit.sol +109 -28
  8. package/contracts/periphery/counterfactual/CounterfactualDepositCCTP.sol +102 -57
  9. package/contracts/periphery/counterfactual/CounterfactualDepositFactory.sol +61 -83
  10. package/contracts/periphery/counterfactual/CounterfactualDepositFactoryTron.sol +14 -24
  11. package/contracts/periphery/counterfactual/CounterfactualDepositOFT.sol +119 -58
  12. package/contracts/periphery/counterfactual/CounterfactualDepositSpokePool.sol +122 -90
  13. package/contracts/periphery/counterfactual/CounterfactualDepositSpokePoolTr.sol +10 -21
  14. package/contracts/periphery/counterfactual/CounterfactualDepositVanillaCCTP.sol +170 -0
  15. package/contracts/periphery/counterfactual/CounterfactualImplementationBase.sol +51 -0
  16. package/dist/broadcast/deployed-addresses.json +34 -10
  17. package/dist/evm/artifacts/AdminWithdrawManager.sol/AdminWithdrawManager.json +1 -1
  18. package/dist/evm/artifacts/CounterfactualBeacon.sol/CounterfactualBeacon.json +1 -0
  19. package/dist/evm/artifacts/CounterfactualBeaconBase.sol/CounterfactualBeaconBase.json +1 -0
  20. package/dist/evm/artifacts/CounterfactualBeaconBase.sol/IBeaconTarget.json +1 -0
  21. package/dist/evm/artifacts/CounterfactualBeaconBootstrap.sol/CounterfactualBeaconBootstrap.json +1 -0
  22. package/dist/evm/artifacts/CounterfactualDeposit.sol/CounterfactualDeposit.json +1 -1
  23. package/dist/evm/artifacts/CounterfactualDepositCCTP.sol/CounterfactualDepositCCTP.json +1 -1
  24. package/dist/evm/artifacts/CounterfactualDepositFactory.sol/CounterfactualDepositFactory.json +1 -1
  25. package/dist/evm/artifacts/CounterfactualDepositFactoryTron.sol/CounterfactualDepositFactoryTron.json +1 -1
  26. package/dist/evm/artifacts/CounterfactualDepositOFT.sol/CounterfactualDepositOFT.json +1 -1
  27. package/dist/evm/artifacts/CounterfactualDepositOFT.sol/ISponsoredOFTSrcPeriphery.json +1 -1
  28. package/dist/evm/artifacts/CounterfactualDepositSpokePool.sol/CounterfactualDepositSpokePool.json +1 -1
  29. package/dist/evm/artifacts/CounterfactualDepositSpokePoolTr.sol/CounterfactualDepositSpokePoolTr.json +1 -1
  30. package/dist/evm/artifacts/CounterfactualDepositVanillaCCTP.sol/CounterfactualDepositVanillaCCTP.json +1 -0
  31. package/dist/evm/artifacts/CounterfactualImplementationBase.sol/CounterfactualImplementationBase.json +1 -0
  32. package/dist/evm/artifacts/CounterfactualTestBase.sol/CounterfactualTestBase.json +1 -0
  33. package/dist/evm/artifacts/ICounterfactualBeacon.sol/ICounterfactualBeacon.json +1 -0
  34. package/dist/evm/artifacts/ICounterfactualDeposit.sol/ICounterfactualDeposit.json +1 -1
  35. package/dist/evm/artifacts/Ownable2StepUpgradeable.sol/Ownable2StepUpgradeable.json +1 -0
  36. package/dist/evm/artifacts/OwnableUpgradeable.sol/OwnableUpgradeable.json +1 -1
  37. package/dist/evm/artifacts/UUPSUpgradeable.sol/UUPSUpgradeable.json +1 -1
  38. package/dist/src/consts.js +4 -0
  39. package/package.json +2 -2
  40. package/dist/evm/artifacts/Clones.sol/Clones.json +0 -1
@@ -0,0 +1,91 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ pragma solidity ^0.8.0;
3
+
4
+ import { IBeacon } from "@openzeppelin/contracts/proxy/beacon/IBeacon.sol";
5
+
6
+ /**
7
+ * @title ICounterfactualBeacon
8
+ * @notice Global, per-chain registry and **beacon** for every counterfactual `BeaconProxy`:
9
+ * `implementation()` is the single implementation all proxies run; `upgradeRoot()` is the
10
+ * `(proxy, latestRoot)` tree authorizing best-effort per-proxy root updates. It is also the single
11
+ * source of every chain-specific value the leaves need (bridge endpoints, domains/EIDs, fee
12
+ * `signer`, token addresses), exposed as `public immutable` getters so leaves stay byte-identical
13
+ * across chains. Changing any value (or adding a token) is a UUPS upgrade; the proxy address never
14
+ * changes.
15
+ * @dev `implementation()` is the **counterfactual** implementation (beacon target), not the registry's own.
16
+ * @custom:security-contact bugs@across.to
17
+ */
18
+ interface ICounterfactualBeacon is IBeacon {
19
+ /// @notice Emitted when the admin sets the global implementation (the beacon target).
20
+ event ImplementationSet(address indexed implementation);
21
+
22
+ /// @notice Emitted when the admin sets the upgrade-tree root.
23
+ event UpgradeRootSet(bytes32 indexed upgradeRoot);
24
+
25
+ // `implementation()` is inherited from `IBeacon` — the canonical implementation every counterfactual
26
+ // proxy runs (resolved live by each `BeaconProxy`).
27
+
28
+ /// @notice Root of the `(proxy, latestRoot)` merkle tree authorizing per-proxy root updates.
29
+ function upgradeRoot() external view returns (bytes32);
30
+
31
+ // --- Chain-specific config (immutable; read by leaf implementations under delegatecall) ---
32
+
33
+ /// @notice Off-chain signer that authorizes runtime execution fees for every leaf implementation.
34
+ function signer() external view returns (address);
35
+
36
+ /// @notice Across SpokePool on this chain.
37
+ function spokePool() external view returns (address);
38
+
39
+ /// @notice Wrapped native token (e.g. WETH) used as the SpokePool input token for native deposits.
40
+ function wrappedNativeToken() external view returns (address);
41
+
42
+ /// @notice Input token for the "native" SpokePool route: the native sentinel
43
+ /// (`0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE`) where the deposit is `msg.value` (wrapped to
44
+ /// `wrappedNativeToken()`), or an ERC-20 on chains with no native gas token. The SpokePool leaf
45
+ /// names this via `inputTokenGetter` and branches on the sentinel, so one leaf serves both.
46
+ function nativeToken() external view returns (address);
47
+
48
+ /// @notice SponsoredCCTPSrcPeriphery on this chain (sponsored CCTP route).
49
+ function cctpSrcPeriphery() external view returns (address);
50
+
51
+ /// @notice Circle CCTP v2 TokenMessenger on this chain (vanilla CCTP route).
52
+ function cctpTokenMessenger() external view returns (address);
53
+
54
+ /// @notice Circle CCTP source domain id for this chain (sponsored CCTP route).
55
+ function cctpSourceDomain() external view returns (uint32);
56
+
57
+ /// @notice SponsoredOFTSrcPeriphery (USDT0 today). OFT peripheries are single-token; the OFT leaf picks
58
+ /// which to use by this getter's selector, so another OFT token = another getter (beacon upgrade).
59
+ function oftSrcPeriphery() external view returns (address);
60
+
61
+ /// @notice LayerZero OFT source endpoint id for this chain.
62
+ function oftSrcEid() external view returns (uint32);
63
+
64
+ /// @notice USDC token address on this chain.
65
+ function usdc() external view returns (address);
66
+
67
+ /// @notice USDT token address on this chain.
68
+ function usdt() external view returns (address);
69
+
70
+ // --- Per-(token, bridge) execution-fee caps (input-token units). A leaf names which to enforce via its
71
+ // `maxExecutionFeeGetter` selector. Illustrative set; for SpokePool this is the fixed fee component. ---
72
+
73
+ /// @notice Max execution fee for the USDC CCTP route(s).
74
+ function usdcCctpMaxExecutionFee() external view returns (uint256);
75
+
76
+ /// @notice Cap on the submitter-chosen Circle fast-transfer fee (vanilla CCTP route), in bps of the
77
+ /// burned amount; 0 ⇒ standard transfers only.
78
+ function usdcCctpMaxFeeBps() external view returns (uint256);
79
+
80
+ /// @notice Max execution fee for the USDT OFT route.
81
+ function usdtOftMaxExecutionFee() external view returns (uint256);
82
+
83
+ /// @notice Max (fixed) fee for the USDC SpokePool route.
84
+ function usdcSpokePoolMaxExecutionFee() external view returns (uint256);
85
+
86
+ /// @notice Max (fixed) fee for the USDT SpokePool route.
87
+ function usdtSpokePoolMaxExecutionFee() external view returns (uint256);
88
+
89
+ /// @notice Max (fixed) fee for the WETH/native SpokePool route.
90
+ function wethSpokePoolMaxExecutionFee() external view returns (uint256);
91
+ }
@@ -23,4 +23,25 @@ interface ICounterfactualDeposit {
23
23
  bytes calldata submitterData,
24
24
  bytes32[] calldata proof
25
25
  ) external payable;
26
+
27
+ /**
28
+ * @notice Update `activeRoot` to `newRoot` (if not already there), then execute, atomically.
29
+ * @dev Lets an executor activate a newly-added route and use it in one transaction. The root
30
+ * update is skipped when the proxy is already at `newRoot`, so this never reverts
31
+ * `RootUnchanged` for an already-current proxy.
32
+ * @param newRoot The root to bring the proxy to before executing.
33
+ * @param updateProof Merkle proof for the (proxy, newRoot) leaf in the beacon's upgrade tree.
34
+ * @param implementation The implementation contract to delegatecall.
35
+ * @param params ABI-encoded route parameters (hashed into the merkle leaf).
36
+ * @param submitterData ABI-encoded data supplied by the caller at execution time.
37
+ * @param executeProof Merkle proof for the (implementation, keccak256(params)) leaf.
38
+ */
39
+ function updateRootAndExecute(
40
+ bytes32 newRoot,
41
+ bytes32[] calldata updateProof,
42
+ address implementation,
43
+ bytes calldata params,
44
+ bytes calldata submitterData,
45
+ bytes32[] calldata executeProof
46
+ ) external payable;
26
47
  }
@@ -50,6 +50,19 @@ library TronClones {
50
50
  return _computeTronAddress(salt, bytecodeHash, deployer);
51
51
  }
52
52
 
53
+ /**
54
+ * @notice Generic Tron CREATE2 address prediction (0x41 prefix) for an arbitrary init-code hash.
55
+ * @dev Mirrors OZ `Create2.computeAddress` but uses Tron's 0x41 prefix. Use for non-clone init code
56
+ * (e.g. an `ERC1967Proxy`) where the caller already has `keccak256(initCode)`.
57
+ * @param salt The CREATE2 salt.
58
+ * @param bytecodeHash The keccak256 hash of the init code.
59
+ * @param deployer The deploying contract address.
60
+ * @return The predicted address on Tron.
61
+ */
62
+ function computeAddress(bytes32 salt, bytes32 bytecodeHash, address deployer) internal pure returns (address) {
63
+ return _computeTronAddress(salt, bytecodeHash, deployer);
64
+ }
65
+
53
66
  /**
54
67
  * @dev Tron CREATE2 address computation using 0x41 prefix.
55
68
  * Memory layout mirrors OZ Create2.computeAddress but replaces 0xff with 0x41.
@@ -0,0 +1,115 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ pragma solidity ^0.8.0;
3
+
4
+ import { ICounterfactualBeacon } from "../../interfaces/ICounterfactualBeacon.sol";
5
+ import { CounterfactualBeaconBase } from "./CounterfactualBeaconBase.sol";
6
+
7
+ /**
8
+ * @notice Chain-specific config baked into a `CounterfactualBeacon` implementation as `public immutable`s,
9
+ * so leaves read endpoints/tokens/signer from the registry and stay byte-identical across chains.
10
+ * Each chain deploys its own implementation with its own config.
11
+ */
12
+ struct CounterfactualChainConfig {
13
+ address signer;
14
+ address spokePool;
15
+ address wrappedNativeToken;
16
+ /// @dev "Native" SpokePool route: the native sentinel where the deposit is `msg.value` (wrapped to
17
+ /// `wrappedNativeToken`), or an ERC-20 on chains with no native gas token. See `nativeToken()`.
18
+ address nativeToken;
19
+ address cctpSrcPeriphery;
20
+ address cctpTokenMessenger;
21
+ uint32 cctpSourceDomain;
22
+ /// @dev Single-token OFT periphery (USDT0). The OFT leaf picks the periphery by its getter selector, so
23
+ /// another OFT token is a beacon upgrade adding another getter.
24
+ address oftSrcPeriphery;
25
+ uint32 oftSrcEid;
26
+ address usdc;
27
+ address usdt;
28
+ /// @dev Per-(token, bridge) execution-fee caps, in input-token units. A leaf names which to enforce via
29
+ /// a `bytes4` selector (its `maxExecutionFeeGetter`). Illustrative set — add more as routes need them.
30
+ /// For SpokePool this is the fixed component of the fee cap (added to the leaf's `maxFeeBps` term).
31
+ uint256 usdcCctpMaxExecutionFee;
32
+ /// @dev Cap on the submitter-chosen Circle fast-transfer fee (vanilla CCTP route), in bps of the
33
+ /// burned amount (0 ⇒ standard transfers only).
34
+ uint256 usdcCctpMaxFeeBps;
35
+ uint256 usdtOftMaxExecutionFee;
36
+ uint256 usdcSpokePoolMaxExecutionFee;
37
+ uint256 usdtSpokePoolMaxExecutionFee;
38
+ uint256 wethSpokePoolMaxExecutionFee;
39
+ }
40
+
41
+ /**
42
+ * @title CounterfactualBeacon
43
+ * @notice The **configuration** of the per-chain counterfactual registry/beacon: every chain-specific value
44
+ * (bridge endpoints, domains/EIDs, fee signer, token addresses, fee caps) as a `public immutable`,
45
+ * named getter. All logic — root/implementation management, UUPS, ownership — lives in
46
+ * `CounterfactualBeaconBase`.
47
+ * @dev The config is `public immutable` (in code, readable through the proxy under delegatecall), so changing
48
+ * a value or adding a token/cap means deploying a new implementation and `upgradeToAndCall`-ing to it.
49
+ * For an identical proxy address across chains, deploy against a uniform bootstrap then upgrade to the
50
+ * chain-specific implementation.
51
+ *
52
+ * NOTE: these `immutable` values are **pure configuration**. A new implementation that changes only
53
+ * them (this contract's constructor wiring, with no change to `CounterfactualBeaconBase`) is a
54
+ * configuration change and is **not subject to audit** — only changes to the base's *logic* are audited.
55
+ * @custom:security-contact bugs@across.to
56
+ */
57
+ contract CounterfactualBeacon is CounterfactualBeaconBase {
58
+ /// @inheritdoc ICounterfactualBeacon
59
+ address public immutable signer;
60
+ /// @inheritdoc ICounterfactualBeacon
61
+ address public immutable spokePool;
62
+ /// @inheritdoc ICounterfactualBeacon
63
+ address public immutable wrappedNativeToken;
64
+ /// @inheritdoc ICounterfactualBeacon
65
+ address public immutable nativeToken;
66
+ /// @inheritdoc ICounterfactualBeacon
67
+ address public immutable cctpSrcPeriphery;
68
+ /// @inheritdoc ICounterfactualBeacon
69
+ address public immutable cctpTokenMessenger;
70
+ /// @inheritdoc ICounterfactualBeacon
71
+ uint32 public immutable cctpSourceDomain;
72
+ /// @inheritdoc ICounterfactualBeacon
73
+ address public immutable oftSrcPeriphery;
74
+ /// @inheritdoc ICounterfactualBeacon
75
+ uint32 public immutable oftSrcEid;
76
+ /// @inheritdoc ICounterfactualBeacon
77
+ address public immutable usdc;
78
+ /// @inheritdoc ICounterfactualBeacon
79
+ address public immutable usdt;
80
+ /// @inheritdoc ICounterfactualBeacon
81
+ uint256 public immutable usdcCctpMaxExecutionFee;
82
+ /// @inheritdoc ICounterfactualBeacon
83
+ uint256 public immutable usdcCctpMaxFeeBps;
84
+ /// @inheritdoc ICounterfactualBeacon
85
+ uint256 public immutable usdtOftMaxExecutionFee;
86
+ /// @inheritdoc ICounterfactualBeacon
87
+ uint256 public immutable usdcSpokePoolMaxExecutionFee;
88
+ /// @inheritdoc ICounterfactualBeacon
89
+ uint256 public immutable usdtSpokePoolMaxExecutionFee;
90
+ /// @inheritdoc ICounterfactualBeacon
91
+ uint256 public immutable wethSpokePoolMaxExecutionFee;
92
+
93
+ /// @param config The chain-specific configuration baked into this implementation (see
94
+ /// `CounterfactualChainConfig`). Each field becomes an immutable, named getter.
95
+ constructor(CounterfactualChainConfig memory config) {
96
+ signer = config.signer;
97
+ spokePool = config.spokePool;
98
+ wrappedNativeToken = config.wrappedNativeToken;
99
+ nativeToken = config.nativeToken;
100
+ cctpSrcPeriphery = config.cctpSrcPeriphery;
101
+ cctpTokenMessenger = config.cctpTokenMessenger;
102
+ cctpSourceDomain = config.cctpSourceDomain;
103
+ oftSrcPeriphery = config.oftSrcPeriphery;
104
+ oftSrcEid = config.oftSrcEid;
105
+ usdc = config.usdc;
106
+ usdt = config.usdt;
107
+ usdcCctpMaxExecutionFee = config.usdcCctpMaxExecutionFee;
108
+ usdcCctpMaxFeeBps = config.usdcCctpMaxFeeBps;
109
+ usdtOftMaxExecutionFee = config.usdtOftMaxExecutionFee;
110
+ usdcSpokePoolMaxExecutionFee = config.usdcSpokePoolMaxExecutionFee;
111
+ usdtSpokePoolMaxExecutionFee = config.usdtSpokePoolMaxExecutionFee;
112
+ wethSpokePoolMaxExecutionFee = config.wethSpokePoolMaxExecutionFee;
113
+ _disableInitializers();
114
+ }
115
+ }
@@ -0,0 +1,121 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ pragma solidity ^0.8.0;
3
+
4
+ import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
5
+ import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
6
+ import { Ownable2StepUpgradeable } from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol";
7
+ import { IBeacon } from "@openzeppelin/contracts/proxy/beacon/IBeacon.sol";
8
+ import { ICounterfactualBeacon } from "../../interfaces/ICounterfactualBeacon.sol";
9
+
10
+ /// @dev Minimal view used to verify a candidate beacon target is bound to this beacon — every
11
+ /// counterfactual implementation embeds its beacon as the immutable `BEACON` (for `updateRoot`).
12
+ interface IBeaconTarget {
13
+ function BEACON() external view returns (address);
14
+ }
15
+
16
+ /**
17
+ * @title CounterfactualBeaconBase
18
+ * @notice The **logic** of the counterfactual registry/beacon: the mutable `implementation` (beacon target)
19
+ * every proxy runs, the `upgradeRoot` authorizing per-proxy root updates, UUPS upgradeability and the
20
+ * `Ownable2Step` admin. The chain-specific configuration (endpoints, tokens, fee signer, fee caps)
21
+ * lives in the derived `CounterfactualBeacon` as immutables. Splitting it out keeps the audit
22
+ * boundary clean: this base is the reviewable logic; a config-only change is a new derived contract
23
+ * that touches no logic here (see `CounterfactualBeacon`).
24
+ * @dev Abstract — the config getters declared in `ICounterfactualBeacon` are implemented by the derived
25
+ * contract's immutables. A UUPS proxy, so the registry address is permanent (anchoring every
26
+ * `BeaconProxy`) while logic/config evolve. `Ownable2Step` admin (no timelock) — it can retarget every
27
+ * proxy instantly, so use a trusted multisig. `implementation()` is the beacon target, not the
28
+ * registry's own UUPS implementation.
29
+ * @custom:security-contact bugs@across.to
30
+ */
31
+ abstract contract CounterfactualBeaconBase is
32
+ ICounterfactualBeacon,
33
+ Initializable,
34
+ UUPSUpgradeable,
35
+ Ownable2StepUpgradeable
36
+ {
37
+ /// @custom:storage-location erc7201:across.counterfactual.beacon.storage
38
+ struct RegistryStorage {
39
+ address implementation;
40
+ bytes32 upgradeRoot;
41
+ }
42
+
43
+ /// @dev Implementation target is not a contract.
44
+ error NotAContract();
45
+ /// @dev Implementation target's `BEACON()` does not point back at this beacon.
46
+ error WrongBeacon();
47
+
48
+ // keccak256(abi.encode(uint256(keccak256("across.counterfactual.beacon.storage")) - 1)) & ~bytes32(uint256(0xff))
49
+ bytes32 private constant STORAGE_LOCATION = 0xb8f0bb8c74633417634f6191ee000dac3f927914fa2e1d714b73a72668a01500;
50
+
51
+ /**
52
+ * @notice Initialize the registry's mutable storage (chain config comes from the derived constructor).
53
+ * @dev With the bootstrap→upgrade deploy the proxy is initialized by the bootstrap and this is never
54
+ * reached (initializer already consumed); set `implementation`/`upgradeRoot` via the owner setters.
55
+ * @param owner_ The admin (use a multisig).
56
+ * @param implementation_ Initial beacon target (may be address(0), set later via `setImplementation`).
57
+ * @param upgradeRoot_ Initial upgrade-tree root (may be 0, set later).
58
+ */
59
+ function initialize(address owner_, address implementation_, bytes32 upgradeRoot_) external initializer {
60
+ __Ownable_init(owner_);
61
+ __Ownable2Step_init();
62
+ // Allow `address(0)` for lazy init (the standard deploy flow is beacon → impl → setImplementation);
63
+ // otherwise the target must be a contract bound to this beacon, matching `setImplementation`.
64
+ if (implementation_ != address(0)) _validateImplementation(implementation_);
65
+ _setImplementation(implementation_);
66
+ _setUpgradeRoot(upgradeRoot_);
67
+ }
68
+
69
+ /// @inheritdoc IBeacon
70
+ /// @dev The counterfactual implementation every `BeaconProxy` resolves and delegatecalls.
71
+ function implementation() external view returns (address) {
72
+ return _getStorage().implementation;
73
+ }
74
+
75
+ /// @inheritdoc ICounterfactualBeacon
76
+ function upgradeRoot() external view returns (bytes32) {
77
+ return _getStorage().upgradeRoot;
78
+ }
79
+
80
+ /// @notice Set the global implementation (the beacon target) every proxy runs. Must be a contract
81
+ /// bound to this beacon; setting it instantly retargets all counterfactual proxies.
82
+ function setImplementation(address newImplementation) external onlyOwner {
83
+ _validateImplementation(newImplementation);
84
+ _setImplementation(newImplementation);
85
+ }
86
+
87
+ /// @notice Set the root of the `(proxy, latestRoot)` upgrade tree.
88
+ function setUpgradeRoot(bytes32 newUpgradeRoot) external onlyOwner {
89
+ _setUpgradeRoot(newUpgradeRoot);
90
+ }
91
+
92
+ /// @dev A valid target is a contract whose immutable `BEACON()` points back here — guarding against
93
+ /// retargeting every proxy to logic bound to a different beacon (which would brick `updateRoot`).
94
+ /// The `try` tolerates non-conforming targets: they leave `boundBeacon == 0` and revert below.
95
+ function _validateImplementation(address impl) private view {
96
+ if (impl.code.length == 0) revert NotAContract();
97
+ address boundBeacon;
98
+ try IBeaconTarget(impl).BEACON() returns (address b) {
99
+ boundBeacon = b;
100
+ } catch {}
101
+ if (boundBeacon != address(this)) revert WrongBeacon();
102
+ }
103
+
104
+ function _setImplementation(address newImplementation) internal {
105
+ _getStorage().implementation = newImplementation;
106
+ emit ImplementationSet(newImplementation);
107
+ }
108
+
109
+ function _setUpgradeRoot(bytes32 newUpgradeRoot) internal {
110
+ _getStorage().upgradeRoot = newUpgradeRoot;
111
+ emit UpgradeRootSet(newUpgradeRoot);
112
+ }
113
+
114
+ function _authorizeUpgrade(address) internal override onlyOwner {}
115
+
116
+ function _getStorage() private pure returns (RegistryStorage storage $) {
117
+ assembly {
118
+ $.slot := STORAGE_LOCATION
119
+ }
120
+ }
121
+ }
@@ -0,0 +1,39 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ pragma solidity ^0.8.0;
3
+
4
+ import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
5
+ import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
6
+ import { Ownable2StepUpgradeable } from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol";
7
+
8
+ /**
9
+ * @title CounterfactualBeaconBootstrap
10
+ * @notice Minimal, chain-identical UUPS implementation used only to deploy the `CounterfactualBeacon`
11
+ * proxy at the **same address on every chain**.
12
+ * @dev The real `CounterfactualBeacon` implementation bakes chain-specific immutables in, so its address —
13
+ * and therefore the proxy's (the implementation is in the proxy init code) — differs per chain. This
14
+ * bootstrap has no immutables, so it has one deterministic address everywhere; the proxy is created
15
+ * against it then `upgradeToAndCall`-ed to the chain-specific `CounterfactualBeacon`. After that, set
16
+ * `implementation`/`upgradeRoot` via the owner setters (the initializer slot is already consumed, so
17
+ * `CounterfactualBeacon.initialize` can't rerun). Shares the OZ `Ownable2Step`/`UUPS` storage layout,
18
+ * so the admin set here is preserved.
19
+ *
20
+ * **Bytecode pinning:** the chain-invariant proxy address depends on this contract's exact creation
21
+ * code, so any bytecode change (solc/optimizer/imports/AST) moves the Bootstrap and every dependent
22
+ * address. Once the canonical Bootstrap ships on the first chain, deploy later chains from the saved
23
+ * canonical creation code, not a recompile — treat it as frozen and flag any PR touching it, its OZ
24
+ * imports, or its compiler profile.
25
+ * @custom:security-contact bugs@across.to
26
+ */
27
+ contract CounterfactualBeaconBootstrap is Initializable, UUPSUpgradeable, Ownable2StepUpgradeable {
28
+ constructor() {
29
+ _disableInitializers();
30
+ }
31
+
32
+ /// @notice Set the admin that will perform the upgrade to the chain-specific implementation.
33
+ function initialize(address owner_) external initializer {
34
+ __Ownable_init(owner_);
35
+ __Ownable2Step_init();
36
+ }
37
+
38
+ function _authorizeUpgrade(address) internal override onlyOwner {}
39
+ }
@@ -1,53 +1,119 @@
1
1
  // SPDX-License-Identifier: BUSL-1.1
2
2
  pragma solidity ^0.8.0;
3
3
 
4
- import { Clones } from "@openzeppelin/contracts/proxy/Clones.sol";
4
+ import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
5
5
  import { MerkleProof } from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
6
- import { ICounterfactualImplementation } from "../../interfaces/ICounterfactualImplementation.sol";
6
+ import { ICounterfactualBeacon } from "../../interfaces/ICounterfactualBeacon.sol";
7
7
  import { ICounterfactualDeposit } from "../../interfaces/ICounterfactualDeposit.sol";
8
+ import { ICounterfactualImplementation } from "../../interfaces/ICounterfactualImplementation.sol";
8
9
 
9
10
  /**
10
11
  * @title CounterfactualDeposit
11
- * @notice Merkle-dispatched entrypoint for counterfactual deposit clones. All clones are instances of this contract.
12
- * @dev The clone's immutable arg is a merkle root. Each leaf is `keccak256(bytes.concat(keccak256(abi.encode(implementation, keccak256(params)))))`.
13
- * Callers prove leaf inclusion, then the dispatcher delegatecalls the implementation.
12
+ * @notice The counterfactual implementation the merkle-dispatched entry point every counterfactual
13
+ * `BeaconProxy` runs (the registry/beacon's `implementation()`). It also owns the proxy's
14
+ * mutable state: `activeRoot` (the route tree), held in an ERC-7201 namespaced slot. Verifies a
15
+ * leaf against `activeRoot`, then delegatecalls the per-bridge implementation the leaf authorizes
16
+ * (which decodes the destination identity from `params` and bridges).
17
+ * @dev Resolved live from the beacon (`CounterfactualBeacon.implementation()`) on every call, so all proxies
18
+ * always run the current implementation — there is no per-proxy upgrade and no bootstrap. Runs
19
+ * under the proxy's delegatecall, so `address(this)` is the proxy (correct for EIP-712 domains and
20
+ * token balances), `msg.sender` is the original caller, `msg.value` the original value.
14
21
  *
15
- * Call chain: Caller CALL Clone (EIP-1167 proxy) DELEGATECALL → Dispatcher → DELEGATECALL → Implementation
16
- * - address(this) = clone address throughout (correct for EIP-712, token balances)
17
- * - msg.sender = original caller throughout
18
- * - msg.value = original value throughout
22
+ * The implementation is upgraded **globally** by the admin setting the beacon's `implementation`;
23
+ * only the per-proxy `activeRoot` is mutable here, via the permissionless `updateRoot` (proven
24
+ * against the registry's `(proxy, latestRoot)` tree). Root updates are **best-effort** — a proxy
25
+ * keeps its `activeRoot` until someone updates it; there is no on-chain version/min-version gate.
26
+ * **Every future implementation version MUST preserve this ERC-7201 storage layout.**
19
27
  *
20
- * Note: Some implementations such as CounterfactualDepositSpokePool use authorization signatures
21
- * that cover execution-time parameters (amounts, deadlines, etc.) but do not commit to the leaf's
22
- * route-specific `params` (destination chain, tokens, recipient, etc.). If two leaves share the same
23
- * implementation address, a caller could prove leaf A's route params while submitting an authorization
24
- * signature intended for leaf B's route, since the signature is valid for either leaf. The system is
25
- * intended to be used such that a clone's merkle tree never contains multiple leaves with the same
26
- * implementation address.
28
+ * Note: every leaf implementation's fee signature binds the leaf's route via `routeParamsHash` (the
29
+ * EIP-712 typehash for all four — SpokePool, CCTP, VanillaCCTP, OFT commits it). So a fee signature
30
+ * authored for one leaf cannot be replayed against another, and a clone's tree MAY safely contain
31
+ * multiple leaves that share an implementation address (e.g. two OFT routes for different input tokens
32
+ * to one destination identity). Cross-chain replay is independently prevented by the `chainId` in the
33
+ * EIP-712 domain, and cross-clone replay by `verifyingContract = address(this)`.
34
+ * @custom:security-contact bugs@across.to
27
35
  */
28
- contract CounterfactualDeposit is ICounterfactualDeposit {
29
- /// @dev Accept native ETH sent to the clone (e.g. user deposits or refunds).
36
+ contract CounterfactualDeposit is Initializable, ICounterfactualDeposit {
37
+ /// @custom:storage-location erc7201:across.counterfactual.upgradeable.storage
38
+ struct CounterfactualStorage {
39
+ bytes32 activeRoot;
40
+ }
41
+
42
+ // keccak256(abi.encode(uint256(keccak256("across.counterfactual.upgradeable.storage")) - 1)) & ~bytes32(uint256(0xff))
43
+ bytes32 private constant STORAGE_LOCATION = 0x5b89d334b964a560e5498fb6b9c95b4213116f116bbd1e59c9c85ba952217700;
44
+
45
+ /// @notice The `CounterfactualBeacon` — the beacon every counterfactual `BeaconProxy` resolves its
46
+ /// implementation from, and the source of the `upgradeRoot` used by `updateRoot`.
47
+ ICounterfactualBeacon public immutable BEACON;
48
+
49
+ /// @notice Emitted when `activeRoot` is updated via the upgrade tree.
50
+ event RootUpdated(bytes32 newRoot);
51
+
52
+ /// @dev Merkle proof against the registry's `(proxy, latestRoot)` tree failed.
53
+ error InvalidUpgradeProof();
54
+ /// @dev New root equals the current `activeRoot` (no-op).
55
+ error RootUnchanged();
56
+
57
+ constructor(ICounterfactualBeacon beacon) {
58
+ BEACON = beacon;
59
+ _disableInitializers();
60
+ }
61
+
62
+ /// @notice Initialize the proxy's `activeRoot` from `initialRoot`.
63
+ /// @dev Delegatecalled once by the `BeaconProxy` constructor (its `data` carries `initialRoot`, which
64
+ /// thereby enters the CREATE2 preimage — binding the address to `initialRoot`).
65
+ function initialize(bytes32 initialRoot) external initializer {
66
+ _getStorage().activeRoot = initialRoot;
67
+ }
68
+
69
+ /// @dev Accept native value sent to the proxy (deposits before/after deployment, refunds).
30
70
  receive() external payable {}
31
71
 
32
- /**
33
- * @notice Execute an implementation by proving its inclusion in the clone's merkle tree.
34
- * @param implementation The implementation contract to delegatecall.
35
- * @param params ABI-encoded route parameters (hashed into the merkle leaf).
36
- * @param submitterData ABI-encoded data supplied by the caller at execution time.
37
- * @param proof Merkle proof for the (implementation, keccak256(params)) leaf.
38
- */
72
+ /// @notice The merkle root authorizing this proxy's deposit routes.
73
+ function activeRoot() public view returns (bytes32) {
74
+ return _getStorage().activeRoot;
75
+ }
76
+
77
+ /// @inheritdoc ICounterfactualDeposit
39
78
  function execute(
40
79
  address implementation,
41
80
  bytes calldata params,
42
81
  bytes calldata submitterData,
43
82
  bytes32[] calldata proof
44
83
  ) external payable {
45
- bytes32 merkleRoot = abi.decode(Clones.fetchCloneArgs(address(this)), (bytes32));
84
+ _execute(implementation, params, submitterData, proof);
85
+ }
46
86
 
87
+ /// @inheritdoc ICounterfactualDeposit
88
+ function updateRootAndExecute(
89
+ bytes32 newRoot,
90
+ bytes32[] calldata updateProof,
91
+ address implementation,
92
+ bytes calldata params,
93
+ bytes calldata submitterData,
94
+ bytes32[] calldata executeProof
95
+ ) external payable {
96
+ // Skip the update (and its `RootUnchanged` revert) when the proxy is already current. NOTE:
97
+ // `updateProof` is therefore NOT validated in that case — there is no root change to authorize.
98
+ if (newRoot != activeRoot()) _updateRoot(newRoot, updateProof);
99
+ _execute(implementation, params, submitterData, executeProof);
100
+ }
101
+
102
+ /// @notice Update `activeRoot`, proving `(address(this), newRoot)` is in the registry's upgrade tree.
103
+ /// @dev Permissionless. Root updates are best-effort — a proxy keeps its `activeRoot` until updated.
104
+ function updateRoot(bytes32 newRoot, bytes32[] calldata proof) external {
105
+ _updateRoot(newRoot, proof);
106
+ }
107
+
108
+ function _execute(
109
+ address implementation,
110
+ bytes calldata params,
111
+ bytes calldata submitterData,
112
+ bytes32[] calldata proof
113
+ ) private {
47
114
  // Double-hash to prevent leaf/internal-node ambiguity (OpenZeppelin standard).
48
115
  bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(implementation, keccak256(params)))));
49
-
50
- if (!MerkleProof.verify(proof, merkleRoot, leaf)) revert InvalidProof();
116
+ if (!MerkleProof.verify(proof, activeRoot(), leaf)) revert InvalidProof();
51
117
 
52
118
  (bool success, bytes memory result) = implementation.delegatecall(
53
119
  abi.encodeCall(ICounterfactualImplementation.execute, (params, submitterData))
@@ -58,4 +124,19 @@ contract CounterfactualDeposit is ICounterfactualDeposit {
58
124
  }
59
125
  }
60
126
  }
127
+
128
+ function _updateRoot(bytes32 newRoot, bytes32[] calldata proof) private {
129
+ CounterfactualStorage storage $ = _getStorage();
130
+ if (newRoot == $.activeRoot) revert RootUnchanged();
131
+ bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(address(this), newRoot))));
132
+ if (!MerkleProof.verify(proof, BEACON.upgradeRoot(), leaf)) revert InvalidUpgradeProof();
133
+ $.activeRoot = newRoot;
134
+ emit RootUpdated(newRoot);
135
+ }
136
+
137
+ function _getStorage() private pure returns (CounterfactualStorage storage $) {
138
+ assembly {
139
+ $.slot := STORAGE_LOCATION
140
+ }
141
+ }
61
142
  }