Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions foundry.lock
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
{
"lib/chainlink-ccip": {
"rev": "aa3224792927b281308ee73a56e08eaaf1a9c2c2"
},
"lib/chainlink-evm": {
"rev": "7a535ccf3fa0e585a70e21ca41ceb1d5c44e6f8b"
},
"lib/chainlink-local": {
"rev": "f8c0efe8685660dac07e08f4558f1b578ae991aa"
},
"lib/ens-contracts": {
"rev": "f5f2ededccbb7e52be44925c0050620d71762e32"
},
Expand Down
245 changes: 59 additions & 186 deletions src/common/crosschain/CrossChainController.sol

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make sure we add a way to pause the send/receive/retry functions

Large diffs are not rendered by default.

196 changes: 28 additions & 168 deletions src/common/crosschain/adapters/BaseAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,60 +9,30 @@ import {IBaseAdapter} from "./IBaseAdapter.sol";

/// @title BaseAdapter
/// @notice Shared logic for bridge adapters owned by a `CrossChainController`.
/// @dev TWO EXECUTION CONTEXTS. Read this before touching anything here.
///
/// 1. SEND — `delegatecall` from the controller. `address(this)` is the
/// CONTROLLER. Any `SLOAD`/`SSTORE` executed here reads or writes the
/// CONTROLLER's storage, so the send path MUST be storage-free:
/// `immutable`s only (they are baked into the adapter's bytecode and
/// therefore resolve correctly under `delegatecall`) plus arguments
/// passed in by the controller. `onlyDelegatecallFromController`
/// enforces the context.
///
/// 2. RECEIVE — a NORMAL call from the bridge router into the adapter.
/// `address(this)` is the adapter, so the adapter's own storage
/// applies and mutable, permissioned configuration is fine. That is
/// where `_trustedRemotes` and the chain-id maps live.
///
/// TRUSTED REMOTE == REMOTE **CONTROLLER**. Because the send is a
/// `delegatecall`, the account that calls the bridge router is the
/// controller, and the sender address delivered on the far side is the
/// remote chain's CONTROLLER. `_trustedRemotes[chainId]` must therefore
/// hold the remote CONTROLLER address, while the controller's
/// `chainToAdapter[chainId].remoteAdapter` holds the remote ADAPTER
/// address (the bridge-level receiver). These are two DIFFERENT
/// addresses; setting the remote adapter as the trusted remote is a
/// silent, total loss of inbound liveness. See
/// `assertTrustedRemotesMatchControllers`.
///
/// Adapter administration is authorized through the same DAO that owns
/// the controller, so no separate ownership system is introduced. Note
/// that `auth()` MUST NOT be used on the send path: `DaoAuthorizable`
/// holds the DAO as an `immutable` (fine) but permission lookups are
/// keyed on `address(this)`, which differs between the two contexts.
/// @custom:security-contact sirt@aragon.org
abstract contract BaseAdapter is IBaseAdapter, DaoAuthorizable {
abstract contract BaseAdapter is IBaseAdapter {
/// @notice Permission to change adapter configuration (trusted remotes,
/// chain-id mappings). Receive-path configuration only; the send
/// path is configured on the controller.
bytes32 public constant UPDATE_ADAPTER_CONFIG_PERMISSION_ID =
keccak256("UPDATE_ADAPTER_CONFIG_PERMISSION");

/// @notice The address of crosschain controller.
/// @dev `immutable`, so it is readable both from the adapter's own context
/// and from the controller's context under `delegatecall`.
address public immutable override CROSS_CHAIN_CONTROLLER;

/// @notice This adapter's own address, captured at construction.
/// @dev `immutable`, so it is baked into the bytecode and keeps its value
/// even when that bytecode is executed in someone else's context.
/// Comparing it to `address(this)` is how the receive path detects
/// that it is running under `delegatecall`.
address private immutable _selfAddress;

/// @notice standard chain id -> remote CONTROLLER address allowed to
/// originate messages for that chain.
/// @dev NOT the remote adapter. See the contract-level docs.
/// @notice A standard chain id paired with the remote trusted sender.
/// @param standardChainId The standard chain id of remote chain.
/// @param trustedRemote The remote trusted address(i.e origin forwarder)
struct TrustedRemoteConfig {
uint256 standardChainId;
address trustedRemote;
}

/// @notice standard chain id -> remote trusted address allowed
/// to originate messages for that chain.
mapping(uint256 => address) internal _trustedRemotes;

/// @notice Emitted when a trusted remote is set or cleared.
Expand Down Expand Up @@ -94,22 +64,18 @@ abstract contract BaseAdapter is IBaseAdapter, DaoAuthorizable {

/// @param _crossChainController The controller that owns this adapter. Its
/// DAO is adopted as the adapter's permission manager.
/// @param _remoteChainIds The standard chain ids of the remote lanes.
/// @param _remoteTrustedSenders The remote CONTROLLER address per lane.
/// @param _trustedRemoteConfigs The remote trusted config.
constructor(
address _crossChainController,
uint256[] memory _remoteChainIds,
address[] memory _remoteTrustedSenders
) DaoAuthorizable(CrossChainController(payable(_crossChainController)).dao()) {
TrustedRemoteConfig[] memory _trustedRemoteConfigs
) {
CROSS_CHAIN_CONTROLLER = _crossChainController;
_selfAddress = address(this);

_setTrustedRemotes(_remoteChainIds, _remoteTrustedSenders);
_setTrustedRemotes(_trustedRemoteConfigs);
}

/// @inheritdoc IBaseAdapter
/// @dev Redeclared as `public` so the consistency helpers below can call it
/// internally; implemented by the concrete adapter.
function toNativeChainId(
uint256 _chainId
) public view virtual override returns (uint256);
Expand All @@ -121,128 +87,23 @@ abstract contract BaseAdapter is IBaseAdapter, DaoAuthorizable {
return _trustedRemotes[_chainId];
}

/// @notice Sets or clears trusted remotes.
/// @dev Pass `address(0)` for a sender to clear a lane. The values are
/// remote CONTROLLER addresses, NOT remote adapter addresses.
/// @param _remoteChainIds The standard chain ids.
/// @param _remoteTrustedSenders The remote controller addresses.
function setTrustedRemotes(
uint256[] memory _remoteChainIds,
address[] memory _remoteTrustedSenders
) public auth(UPDATE_ADAPTER_CONFIG_PERMISSION_ID) {
_setTrustedRemotes(_remoteChainIds, _remoteTrustedSenders);
}

/// @notice Deployment-time check that this adapter's trusted remotes are
/// the expected remote CONTROLLER addresses, and specifically that
/// they are NOT the remote ADAPTER addresses the controller has
/// configured as bridge receivers.
/// @dev The expected values must be supplied by the deployer: the local
/// controller stores the remote ADAPTER (the bridge receiver), not the
/// remote controller, so there is nothing on-chain to cross-check
/// against. What CAN be checked mechanically — and is — is that the
/// two were not confused for one another.
/// @param _chainIds The standard chain ids to check.
/// @param _expectedRemoteControllers The remote CONTROLLER per chain id.
function assertTrustedRemotesMatchControllers(
uint256[] memory _chainIds,
address[] memory _expectedRemoteControllers
) public view {
if (_chainIds.length != _expectedRemoteControllers.length) {
revert Errors.INVALID_LENGTH_MISMATCH();
}

for (uint256 i = 0; i < _chainIds.length; i++) {
uint256 chainId = _chainIds[i];
address trusted = _trustedRemotes[chainId];

if (trusted == address(0) || trusted != _expectedRemoteControllers[i]) {
revert Errors.TRUSTED_REMOTE_MISMATCH(
chainId,
trusted,
_expectedRemoteControllers[i]
);
}

(, address remoteAdapter, ) = CrossChainController(
payable(CROSS_CHAIN_CONTROLLER)
).chainToAdapter(chainId);

if (remoteAdapter != address(0) && trusted == remoteAdapter) {
revert Errors.TRUSTED_REMOTE_IS_REMOTE_ADAPTER(
chainId,
trusted
);
}
}
}

/// @notice Deployment-time check that the controller's send-side
/// `bridgeChainId` agrees with this adapter's receive-side
/// chain-id map for the same chains.
/// @dev The same mapping necessarily exists twice: the send path may not
/// read storage, so the controller carries chainId -> bridge id in its
/// lane config, while the receive path needs bridge id -> chainId in
/// adapter storage. A desync silently sends to the wrong lane (or to
/// a lane whose inbound messages the far side cannot attribute), so
/// run this after every `updateConfig`/`setChainSelectors`.
/// @param _chainIds The standard chain ids to check.
function assertChainSelectorsMatchController(
uint256[] memory _chainIds
) public view {
for (uint256 i = 0; i < _chainIds.length; i++) {
uint256 chainId = _chainIds[i];

(, , uint64 controllerBridgeChainId) = CrossChainController(
payable(CROSS_CHAIN_CONTROLLER)
).chainToAdapter(chainId);

if (controllerBridgeChainId == 0) {
revert Errors.ADAPTER_NOT_CONFIGURED(chainId);
}

// Reverts `UNKNOWN_CHAIN_ID` if the adapter has no entry at all.
uint64 adapterBridgeChainId = uint64(toNativeChainId(chainId));

if (controllerBridgeChainId != adapterBridgeChainId) {
revert Errors.CHAIN_ID_DESYNC(
chainId,
controllerBridgeChainId,
adapterBridgeChainId
);
}
}
}

/// @notice The address this adapter was deployed at.
/// @return The adapter's own address, from bytecode.
function selfAddress() public view returns (address) {
return _selfAddress;
}

/// @notice Once adapter receives a message, this must be called
/// to redirect/forward it to CrossChainController.
/// @dev MUST stay `internal`: it is the unauthenticated side of the receive
/// path, reachable only after the bridge-specific caller and
/// trusted-remote checks have passed.
///
/// GUARDS THE SEND/RECEIVE ASYMMETRY. This is the last common point of
/// the receive path, and everything upstream of it (trusted remotes,
/// chain-id maps) is STORAGE — which is only meaningful when this code
/// runs in the adapter's own context. If it were ever reached under
/// `delegatecall` (from the controller's send path, or from anything
/// else), those reads would resolve against foreign slots and the
/// authentication they perform would be meaningless. `address(this)`
/// is compared against the `immutable` `_selfAddress` to make that
/// impossible rather than merely unlikely.
/// @notice Once adapter receives a message, this forwards it to the CrossChainController.
/// @param _messageId The bridge-level message identifier.
/// @param _payload The encoded Action[] message.
/// @param _payload The encoded payload message.
/// @param _originChainId The standard chain id the message came from.
function _forwardMessage(
bytes32 _messageId,
bytes memory _payload,
uint256 _originChainId
) internal {
// Extra defense to ensure that caller on controller will always be
// Adapter and not the contract that called adapter with delegatecall.
if (address(this) != _selfAddress) {
revert Errors.DELEGATE_CALL_FORBIDDEN(address(this), _selfAddress);
}
Expand All @@ -254,21 +115,20 @@ abstract contract BaseAdapter is IBaseAdapter, DaoAuthorizable {
);
}

/// @notice Sets the trusted remotes for receiving messages.
/// Generally, it should be the cross chain controller
/// of source chain.
function _setTrustedRemotes(
uint256[] memory _remoteChainIds,
address[] memory _remoteTrustedSenders
TrustedRemoteConfig[] memory _trustedRemoteConfigs
) internal {
if (_remoteChainIds.length != _remoteTrustedSenders.length) {
revert Errors.INVALID_LENGTH_MISMATCH();
}

for (uint256 i = 0; i < _remoteChainIds.length; i++) {
uint256 chainId = _remoteChainIds[i];
for (uint256 i = 0; i < _trustedRemoteConfigs.length; i++) {
uint256 chainId = _trustedRemoteConfigs[i].standardChainId;
if (chainId == 0) revert Errors.INVALID_CHAIN_ID();

_trustedRemotes[chainId] = _remoteTrustedSenders[i];
address trustedRemote_ = _trustedRemoteConfigs[i].trustedRemote;
_trustedRemotes[chainId] = trustedRemote_;

emit TrustedRemoteSet(chainId, _remoteTrustedSenders[i]);
emit TrustedRemoteSet(chainId, trustedRemote_);
}
}
}
Loading
Loading