diff --git a/packages/contracts/contracts/client/Client.sol b/packages/contracts/contracts/client/Client.sol new file mode 100644 index 0000000000..c38b776285 --- /dev/null +++ b/packages/contracts/contracts/client/Client.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.13; + +// ============ Internal Imports ============ +import { IMessageRecipient } from "../interfaces/IMessageRecipient.sol"; +import { Home } from "../Home.sol"; + +/// @dev Stateless contract, that can be potentially used as a parent +/// for the upgradeable contract. +abstract contract Client is IMessageRecipient { + // ============ Immutable Variables ============ + + // local chain Home: used for sending messages + address public immutable home; + + // local chain ReplicaManager: used for receiving messages + address public immutable replicaManager; + + // ============ Constructor ============ + + constructor(address _home, address _replicaManager) { + home = _home; + replicaManager = _replicaManager; + } + + /** + * @notice Handles an incoming message. + * @dev Can only be called by chain's ReplicaManager. + * Can only be sent from a trusted sender on the remote chain. + * @param _origin Domain of the remote chain, where message originated + * @param _nonce Unique identifier for the message from origin to destination chain + * @param _sender Sender of the message on the origin chain + * @param _message The message + */ + function handle( + uint32 _origin, + uint32 _nonce, + bytes32 _sender, + bytes memory _message + ) external { + require(msg.sender == replicaManager, "!replica"); + require(_sender == trustedSender(_origin) && _sender != bytes32(0), "!trustedSender"); + _handle(_origin, _nonce, _sender, _message); + } + + // ============ Virtual Functions ============ + + /// @dev Internal logic for handling the message, assuming all security checks are passed + function _handle( + uint32 _origin, + uint32 _nonce, + bytes32 _sender, + bytes memory _message + ) internal virtual; + + /** + * @dev Sends a message to given destination chain. + * @param _destination Domain of the destination chain + * @param _message The message + */ + function _send(uint32 _destination, bytes memory _message) internal { + bytes32 recipient = trustedSender(_destination); + require(recipient != bytes32(0), "!recipient"); + Home(home).dispatch(_destination, recipient, _message); + } + + /** + * @dev Address of the trusted sender on the destination chain. + * The trusted sender will be able to: + * (1) send messages to this contract + * (2) receive messages from this contract + */ + function trustedSender(uint32 _destination) public view virtual returns (bytes32); +} diff --git a/packages/contracts/contracts/client/SynapseClient.sol b/packages/contracts/contracts/client/SynapseClient.sol new file mode 100644 index 0000000000..8cbc08413a --- /dev/null +++ b/packages/contracts/contracts/client/SynapseClient.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.13; + +// ============ Internal Imports ============ +import { Client } from "./Client.sol"; +// ============ External Imports ============ +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; + +abstract contract SynapseClient is Client, Ownable { + // ============ Internal Variables ============ + + /** + * @dev Contracts addresses on the remote chains, which can: + * (1) send messages to this contract + * (2) receive messages from this contract + */ + mapping(uint32 => bytes32) internal trustedSenders; + + // ============ Constructor ============ + + constructor(address _home, address _replicaManager) Client(_home, _replicaManager) {} + + // ============ Public Functions ============ + + /// @notice Returns the trusted sender for the given remote chain. + function trustedSender(uint32 _remoteDomain) public view override returns (bytes32) { + return trustedSenders[_remoteDomain]; + } + + // ============ Restricted Functions ============ + + /** + * @notice Sets the trusted sender for the given remote chain. + * @dev Only callable by owner (Governance). + * @param _remoteDomain The domain of the remote chain + * @param _trustedSender The trusted sender + */ + function setTrustedSender(uint32 _remoteDomain, bytes32 _trustedSender) external onlyOwner { + _setTrustedSender(_remoteDomain, _trustedSender); + } + + /** + * @notice Sets the trusted sender for a bunch of remote chains. + * @dev Only callable by owner (Governance). + * @param _remoteDomains List of domains for the remote chains + * @param _trustedSenders List of trusted senders for given chains + */ + function setTrustedSenders(uint32[] calldata _remoteDomains, bytes32[] calldata _trustedSenders) + external + onlyOwner + { + uint256 length = _trustedSenders.length; + require(_remoteDomains.length == length, "!arrays"); + for (uint256 i = 0; i < length; ) { + _setTrustedSender(_remoteDomains[i], _trustedSenders[i]); + unchecked { + ++i; + } + } + } + + // ============ Internal Functions ============ + + /// @dev Checks both domain and trusted sender, then updates the records. + function _setTrustedSender(uint32 _remoteDomain, bytes32 _trustedSender) internal { + require(_remoteDomain != 0, "!domain"); + require(_trustedSender != bytes32(0), "!sender"); + trustedSenders[_remoteDomain] = _trustedSender; + } +} diff --git a/packages/contracts/contracts/client/SynapseClientUpgradeable.sol b/packages/contracts/contracts/client/SynapseClientUpgradeable.sol new file mode 100644 index 0000000000..f30da4038e --- /dev/null +++ b/packages/contracts/contracts/client/SynapseClientUpgradeable.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.13; + +// ============ Internal Imports ============ +import { Client } from "./Client.sol"; +// ============ External Imports ============ + +import { + OwnableUpgradeable +} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +abstract contract SynapseClientUpgradeable is Client, OwnableUpgradeable { + // ============ Internal Variables ============ + + /** + * @dev Contracts addresses on the remote chains, which can: + * (1) send messages to this contract + * (2) receive messages from this contract + */ + mapping(uint32 => bytes32) internal trustedSenders; + + // ============ Upgrade gap ============ + + // gap for upgrade safety + uint256[49] private __GAP; + + // ============ Constructor ============ + + constructor(address _home, address _replicaManager) Client(_home, _replicaManager) {} + + // ============ Initializer ============ + + function __SynapseClient_init() internal onlyInitializing { + __Ownable_init_unchained(); + } + + function __SynapseClient_init_unchained() internal onlyInitializing {} + + // ============ Public Functions ============ + + /// @notice Returns the trusted sender for the given remote chain. + function trustedSender(uint32 _remoteDomain) public view override returns (bytes32) { + return trustedSenders[_remoteDomain]; + } + + // ============ Restricted Functions ============ + + /** + * @notice Sets the trusted sender for the given remote chain. + * @dev Only callable by owner (Governance). + * @param _remoteDomain The domain of the remote chain + * @param _trustedSender The trusted sender + */ + function setTrustedSender(uint32 _remoteDomain, bytes32 _trustedSender) external onlyOwner { + _setTrustedSender(_remoteDomain, _trustedSender); + } + + /** + * @notice Sets the trusted sender for a bunch of remote chains. + * @dev Only callable by owner (Governance). + * @param _remoteDomains List of domains for the remote chains + * @param _trustedSenders List of trusted senders for given chains + */ + function setTrustedSenders(uint32[] calldata _remoteDomains, bytes32[] calldata _trustedSenders) + external + onlyOwner + { + uint256 length = _trustedSenders.length; + require(_remoteDomains.length == length, "!arrays"); + for (uint256 i = 0; i < length; ) { + _setTrustedSender(_remoteDomains[i], _trustedSenders[i]); + unchecked { + ++i; + } + } + } + + // ============ Internal Functions ============ + + /// @dev Checks both domain and trusted sender, then updates the records. + function _setTrustedSender(uint32 _remoteDomain, bytes32 _trustedSender) internal { + require(_remoteDomain != 0, "!domain"); + require(_trustedSender != bytes32(0), "!sender"); + trustedSenders[_remoteDomain] = _trustedSender; + } +} diff --git a/packages/contracts/test/SynapseClient.t.sol b/packages/contracts/test/SynapseClient.t.sol new file mode 100644 index 0000000000..7db7d7819c --- /dev/null +++ b/packages/contracts/test/SynapseClient.t.sol @@ -0,0 +1,176 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.13; + +import "forge-std/Test.sol"; + +import { SynapseClientHarness } from "./harnesses/SynapseClientHarness.sol"; +import { HomeHarness } from "./harnesses/HomeHarness.sol"; + +import { SynapseTestWithUpdaterManager } from "./utils/SynapseTest.sol"; + +import { IUpdaterManager } from "../contracts/interfaces/IUpdaterManager.sol"; +import { Message } from "../contracts/libs/Message.sol"; + +contract SynapseClientTest is SynapseTestWithUpdaterManager { + SynapseClientHarness public client; + HomeHarness public home; + + address public constant replicaManager = address(1234567890); + address public constant owner = address(9876543210); + bytes32 public constant trustedSender = bytes32(uint256(1234554321)); + + function setUp() public override { + super.setUp(); + + home = new HomeHarness(localDomain); + home.initialize(IUpdaterManager(updaterManager)); + updaterManager.setHome(address(home)); + + vm.label(replicaManager, "replica"); + vm.label(owner, "owner"); + + client = new SynapseClientHarness(address(home), replicaManager); + client.transferOwnership(owner); + } + + function test_constructor() public { + assertEq(client.home(), address(home)); + assertEq(client.replicaManager(), replicaManager); + } + + function test_setTrustedSender() public { + vm.prank(owner); + client.setTrustedSender(remoteDomain, trustedSender); + assertEq(client.trustedSender(remoteDomain), trustedSender); + } + + function test_setTrustedSenderAsNotOwner(address _notOwner) public { + vm.assume(_notOwner != owner); + vm.expectRevert("Ownable: caller is not the owner"); + vm.prank(_notOwner); + client.setTrustedSender(remoteDomain, trustedSender); + } + + function test_setTrustedSenderEmptyDomain() public { + vm.prank(owner); + vm.expectRevert("!domain"); + client.setTrustedSender(0, trustedSender); + } + + function test_setTrustedSenderEmptySender() public { + vm.prank(owner); + vm.expectRevert("!sender"); + client.setTrustedSender(remoteDomain, bytes32(0)); + } + + function test_setTrustedSenders() public { + uint256 amount = 5; + uint32[] memory domains = new uint32[](amount); + bytes32[] memory senders = new bytes32[](amount); + for (uint256 i = 0; i < amount; i++) { + domains[i] = uint32(remoteDomain + i); + senders[i] = bytes32(uint256(trustedSender) + i); + } + vm.prank(owner); + client.setTrustedSenders(domains, senders); + for (uint256 i = 0; i < amount; i++) { + assertEq(client.trustedSender(domains[i]), senders[i]); + } + } + + function test_setTrustedSendersAsNotOwner(address _notOwner) public { + vm.assume(_notOwner != owner); + uint32[] memory domains = new uint32[](1); + bytes32[] memory senders = new bytes32[](1); + vm.expectRevert("Ownable: caller is not the owner"); + vm.prank(_notOwner); + client.setTrustedSenders(domains, senders); + } + + function test_setTrustedSendersBadArrays() public { + uint32[] memory domains = new uint32[](1); + bytes32[] memory senders = new bytes32[](2); + vm.expectRevert("!arrays"); + vm.prank(owner); + client.setTrustedSenders(domains, senders); + } + + function test_handle() public { + test_setTrustedSender(); + + vm.prank(replicaManager); + client.handle(remoteDomain, 0, trustedSender, bytes("")); + } + + function test_handleNotReplica(address _notReplica) public { + vm.assume(_notReplica != replicaManager); + test_setTrustedSender(); + + vm.prank(_notReplica); + vm.expectRevert("!replica"); + client.handle(remoteDomain, 0, trustedSender, bytes("")); + } + + function test_handleWrongDomain(uint32 _notRemote) public { + vm.assume(_notRemote != remoteDomain); + + test_setTrustedSender(); + + vm.prank(replicaManager); + vm.expectRevert("!trustedSender"); + client.handle(_notRemote, 0, trustedSender, bytes("")); + } + + function test_handleWrongSender(bytes32 _notSender) public { + vm.assume(_notSender != trustedSender); + + test_setTrustedSender(); + + vm.prank(replicaManager); + vm.expectRevert("!trustedSender"); + client.handle(remoteDomain, 0, _notSender, bytes("")); + } + + function test_handleFakeDomainAndSender(uint32 _notRemote) public { + vm.assume(_notRemote != remoteDomain); + + test_setTrustedSender(); + + vm.prank(replicaManager); + vm.expectRevert("!trustedSender"); + // trustedSender for unknown remote is bytes32(0), + // but this still has to revert + client.handle(_notRemote, 0, bytes32(0), bytes("")); + } + + event Dispatch( + bytes32 indexed messageHash, + uint256 indexed leafIndex, + uint64 indexed destinationAndNonce, + bytes32 committedRoot, + bytes message + ); + + function test_send() public { + test_setTrustedSender(); + bytes memory messageBody = hex"01030307"; + bytes memory message = Message.formatMessage( + localDomain, + bytes32(uint256(uint160(address(client)))), + 0, + remoteDomain, + trustedSender, + messageBody + ); + vm.expectEmit(true, true, true, true); + emit Dispatch(keccak256(message), 0, uint64(remoteDomain) << 32, bytes32(0), message); + client.send(remoteDomain, messageBody); + } + + function test_sendNoRecipient() public { + bytes memory messageBody = hex"01030307"; + vm.expectRevert("!recipient"); + client.send(remoteDomain, messageBody); + } +} diff --git a/packages/contracts/test/SynapseClientUpgradeable.t.sol b/packages/contracts/test/SynapseClientUpgradeable.t.sol new file mode 100644 index 0000000000..b499c03a1f --- /dev/null +++ b/packages/contracts/test/SynapseClientUpgradeable.t.sol @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.13; + +import "forge-std/Test.sol"; + +import { SynapseClientUpgradeableHarness } from "./harnesses/SynapseClientUpgradeableHarness.sol"; +import { HomeHarness } from "./harnesses/HomeHarness.sol"; + +import { SynapseTestWithUpdaterManager } from "./utils/SynapseTest.sol"; + +import { IUpdaterManager } from "../contracts/interfaces/IUpdaterManager.sol"; +import { Message } from "../contracts/libs/Message.sol"; + +import { + TransparentUpgradeableProxy +} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +contract SynapseClientTest is SynapseTestWithUpdaterManager { + SynapseClientUpgradeableHarness public client; + HomeHarness public home; + + address public constant replicaManager = address(1234567890); + address public constant owner = address(9876543210); + bytes32 public constant trustedSender = bytes32(uint256(1234554321)); + + function setUp() public override { + super.setUp(); + + home = new HomeHarness(localDomain); + home.initialize(IUpdaterManager(updaterManager)); + updaterManager.setHome(address(home)); + + vm.label(replicaManager, "replica"); + vm.label(owner, "owner"); + + SynapseClientUpgradeableHarness impl = new SynapseClientUpgradeableHarness( + address(home), + replicaManager + ); + + TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy( + address(impl), + address(1337), + bytes("") + ); + client = SynapseClientUpgradeableHarness(address(proxy)); + client.initialize(); + client.transferOwnership(owner); + } + + function test_constructor() public { + assertEq(client.home(), address(home)); + assertEq(client.replicaManager(), replicaManager); + } + + function test_cannotInitializeTwice() public { + vm.expectRevert("Initializable: contract is already initialized"); + client.initialize(); + } + + function test_setTrustedSender() public { + vm.prank(owner); + client.setTrustedSender(remoteDomain, trustedSender); + assertEq(client.trustedSender(remoteDomain), trustedSender); + } + + function test_setTrustedSenderAsNotOwner(address _notOwner) public { + vm.assume(_notOwner != owner); + vm.expectRevert("Ownable: caller is not the owner"); + vm.prank(_notOwner); + client.setTrustedSender(remoteDomain, trustedSender); + } + + function test_setTrustedSenderEmptyDomain() public { + vm.prank(owner); + vm.expectRevert("!domain"); + client.setTrustedSender(0, trustedSender); + } + + function test_setTrustedSenderEmptySender() public { + vm.prank(owner); + vm.expectRevert("!sender"); + client.setTrustedSender(remoteDomain, bytes32(0)); + } + + function test_setTrustedSenders() public { + uint256 amount = 5; + uint32[] memory domains = new uint32[](amount); + bytes32[] memory senders = new bytes32[](amount); + for (uint256 i = 0; i < amount; i++) { + domains[i] = uint32(remoteDomain + i); + senders[i] = bytes32(uint256(trustedSender) + i); + } + vm.prank(owner); + client.setTrustedSenders(domains, senders); + for (uint256 i = 0; i < amount; i++) { + assertEq(client.trustedSender(domains[i]), senders[i]); + } + } + + function test_setTrustedSendersAsNotOwner(address _notOwner) public { + vm.assume(_notOwner != owner); + uint32[] memory domains = new uint32[](1); + bytes32[] memory senders = new bytes32[](1); + vm.expectRevert("Ownable: caller is not the owner"); + vm.prank(_notOwner); + client.setTrustedSenders(domains, senders); + } + + function test_setTrustedSendersBadArrays() public { + uint32[] memory domains = new uint32[](1); + bytes32[] memory senders = new bytes32[](2); + vm.expectRevert("!arrays"); + vm.prank(owner); + client.setTrustedSenders(domains, senders); + } + + function test_handle() public { + test_setTrustedSender(); + + vm.prank(replicaManager); + client.handle(remoteDomain, 0, trustedSender, bytes("")); + } + + function test_handleNotReplica(address _notReplica) public { + vm.assume(_notReplica != replicaManager); + test_setTrustedSender(); + + vm.prank(_notReplica); + vm.expectRevert("!replica"); + client.handle(remoteDomain, 0, trustedSender, bytes("")); + } + + function test_handleWrongDomain(uint32 _notRemote) public { + vm.assume(_notRemote != remoteDomain); + + test_setTrustedSender(); + + vm.prank(replicaManager); + vm.expectRevert("!trustedSender"); + client.handle(_notRemote, 0, trustedSender, bytes("")); + } + + function test_handleFakeDomainAndSender(uint32 _notRemote) public { + vm.assume(_notRemote != remoteDomain); + + test_setTrustedSender(); + + vm.prank(replicaManager); + vm.expectRevert("!trustedSender"); + // trustedSender for unknown remote is bytes32(0), + // but this still has to revert + client.handle(_notRemote, 0, bytes32(0), bytes("")); + } + + function test_handleWrongSender(bytes32 _notSender) public { + vm.assume(_notSender != trustedSender); + + test_setTrustedSender(); + + vm.prank(replicaManager); + vm.expectRevert("!trustedSender"); + client.handle(remoteDomain, 0, _notSender, bytes("")); + } + + event Dispatch( + bytes32 indexed messageHash, + uint256 indexed leafIndex, + uint64 indexed destinationAndNonce, + bytes32 committedRoot, + bytes message + ); + + function test_send() public { + test_setTrustedSender(); + bytes memory messageBody = hex"01030307"; + bytes memory message = Message.formatMessage( + localDomain, + bytes32(uint256(uint160(address(client)))), + 0, + remoteDomain, + trustedSender, + messageBody + ); + vm.expectEmit(true, true, true, true); + emit Dispatch(keccak256(message), 0, uint64(remoteDomain) << 32, bytes32(0), message); + client.send(remoteDomain, messageBody); + } + + function test_sendNoRecipient() public { + bytes memory messageBody = hex"01030307"; + vm.expectRevert("!recipient"); + client.send(remoteDomain, messageBody); + } +} diff --git a/packages/contracts/test/harnesses/SynapseClientHarness.sol b/packages/contracts/test/harnesses/SynapseClientHarness.sol new file mode 100644 index 0000000000..0083e6cf6f --- /dev/null +++ b/packages/contracts/test/harnesses/SynapseClientHarness.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.13; + +import { SynapseClient } from "../../contracts/client/SynapseClient.sol"; + +contract SynapseClientHarness is SynapseClient { + constructor(address _home, address _replicaManager) SynapseClient(_home, _replicaManager) {} + + function _handle( + uint32, + uint32, + bytes32, + bytes memory + ) internal override {} + + function send(uint32 _destination, bytes memory _message) external { + _send(_destination, _message); + } +} diff --git a/packages/contracts/test/harnesses/SynapseClientUpgradeableHarness.sol b/packages/contracts/test/harnesses/SynapseClientUpgradeableHarness.sol new file mode 100644 index 0000000000..1a318f7a58 --- /dev/null +++ b/packages/contracts/test/harnesses/SynapseClientUpgradeableHarness.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.13; + +import { SynapseClientUpgradeable } from "../../contracts/client/SynapseClientUpgradeable.sol"; + +contract SynapseClientUpgradeableHarness is SynapseClientUpgradeable { + constructor(address _home, address _replicaManager) + SynapseClientUpgradeable(_home, _replicaManager) + {} + + function initialize() external initializer { + __SynapseClient_init(); + } + + function _handle( + uint32, + uint32, + bytes32, + bytes memory + ) internal override {} + + function send(uint32 _destination, bytes memory _message) external { + _send(_destination, _message); + } +}