From 668a648bc677740ab64eb26337de72544c4119ca Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 30 Mar 2022 16:41:04 +0200 Subject: [PATCH] Add utilities for CrossChain messaging (#3183) Co-authored-by: Francisco Giordano --- CHANGELOG.md | 1 + contracts/access/AccessControlCrossChain.sol | 44 ++++++++ contracts/access/README.adoc | 2 + contracts/crosschain/CrossChainEnabled.sol | 53 +++++++++ contracts/crosschain/README.adoc | 34 ++++++ .../crosschain/amb/CrossChainEnabledAMB.sol | 47 ++++++++ contracts/crosschain/amb/LibAMB.sol | 34 ++++++ .../arbitrum/CrossChainEnabledArbitrumL1.sol | 43 ++++++++ .../arbitrum/CrossChainEnabledArbitrumL2.sol | 34 ++++++ .../crosschain/arbitrum/LibArbitrumL1.sol | 42 ++++++++ .../crosschain/arbitrum/LibArbitrumL2.sol | 42 ++++++++ contracts/crosschain/errors.sol | 6 ++ .../optimism/CrossChainEnabledOptimism.sol | 40 +++++++ contracts/crosschain/optimism/LibOptimism.sol | 35 ++++++ .../polygon/CrossChainEnabledPolygonChild.sol | 71 ++++++++++++ .../extensions/GovernorTimelockCompound.sol | 55 +--------- .../mocks/AccessControlCrossChainMock.sol | 22 ++++ contracts/mocks/crosschain/bridges.sol | 102 ++++++++++++++++++ contracts/mocks/crosschain/receivers.sol | 46 ++++++++ contracts/vendor/amb/IAMB.sol | 48 +++++++++ contracts/vendor/arbitrum/IArbSys.sol | 98 +++++++++++++++++ contracts/vendor/arbitrum/IBridge.sol | 65 +++++++++++ contracts/vendor/arbitrum/IInbox.sol | 91 ++++++++++++++++ .../vendor/arbitrum/IMessageProvider.sol | 25 +++++ contracts/vendor/arbitrum/IOutbox.sol | 50 +++++++++ .../vendor/compound/ICompoundTimelock.sol | 85 +++++++++++++++ contracts/vendor/compound/LICENSE | 11 ++ .../vendor/optimism/ICrossDomainMessenger.sol | 37 +++++++ contracts/vendor/optimism/LICENSE | 22 ++++ .../vendor/polygon/IFxMessageProcessor.sol | 10 ++ package.json | 2 +- test/access/AccessControlCrossChain.test.js | 59 ++++++++++ test/crosschain/CrossChainEnabled.test.js | 83 ++++++++++++++ test/helpers/crosschain.js | 63 +++++++++++ test/helpers/customError.js | 24 +++++ test/utils/structs/DoubleEndedQueue.test.js | 24 ++--- 36 files changed, 1477 insertions(+), 73 deletions(-) create mode 100644 contracts/access/AccessControlCrossChain.sol create mode 100644 contracts/crosschain/CrossChainEnabled.sol create mode 100644 contracts/crosschain/README.adoc create mode 100644 contracts/crosschain/amb/CrossChainEnabledAMB.sol create mode 100644 contracts/crosschain/amb/LibAMB.sol create mode 100644 contracts/crosschain/arbitrum/CrossChainEnabledArbitrumL1.sol create mode 100644 contracts/crosschain/arbitrum/CrossChainEnabledArbitrumL2.sol create mode 100644 contracts/crosschain/arbitrum/LibArbitrumL1.sol create mode 100644 contracts/crosschain/arbitrum/LibArbitrumL2.sol create mode 100644 contracts/crosschain/errors.sol create mode 100644 contracts/crosschain/optimism/CrossChainEnabledOptimism.sol create mode 100644 contracts/crosschain/optimism/LibOptimism.sol create mode 100644 contracts/crosschain/polygon/CrossChainEnabledPolygonChild.sol create mode 100644 contracts/mocks/AccessControlCrossChainMock.sol create mode 100644 contracts/mocks/crosschain/bridges.sol create mode 100644 contracts/mocks/crosschain/receivers.sol create mode 100644 contracts/vendor/amb/IAMB.sol create mode 100644 contracts/vendor/arbitrum/IArbSys.sol create mode 100644 contracts/vendor/arbitrum/IBridge.sol create mode 100644 contracts/vendor/arbitrum/IInbox.sol create mode 100644 contracts/vendor/arbitrum/IMessageProvider.sol create mode 100644 contracts/vendor/arbitrum/IOutbox.sol create mode 100644 contracts/vendor/compound/ICompoundTimelock.sol create mode 100644 contracts/vendor/compound/LICENSE create mode 100644 contracts/vendor/optimism/ICrossDomainMessenger.sol create mode 100644 contracts/vendor/optimism/LICENSE create mode 100644 contracts/vendor/polygon/IFxMessageProcessor.sol create mode 100644 test/access/AccessControlCrossChain.test.js create mode 100644 test/crosschain/CrossChainEnabled.test.js create mode 100644 test/helpers/crosschain.js create mode 100644 test/helpers/customError.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fbf6f5cd3d..2ae2da67e9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased + * `crosschain`: Add a new set of contracts for cross-chain applications. `CrossChainEnabled` is a base contract with instantiations for several chains and bridges, and `AccessControlCrossChain` is an extension of access control that allows cross-chain operation. ([#3183](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3183)) * `AccessControl`: add a virtual `_checkRole(bytes32)` function that can be overridden to alter the `onlyRole` modifier behavior. ([#3137](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3137)) * `EnumerableMap`: add new `AddressToUintMap` map type. ([#3150](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3150)) * `EnumerableMap`: add new `Bytes32ToBytes32Map` map type. ([#3192](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3192)) diff --git a/contracts/access/AccessControlCrossChain.sol b/contracts/access/AccessControlCrossChain.sol new file mode 100644 index 00000000000..21e42515034 --- /dev/null +++ b/contracts/access/AccessControlCrossChain.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.4; + +import "./AccessControl.sol"; +import "../crosschain/CrossChainEnabled.sol"; + +/** + * @dev An extension to {AccessControl} with support for cross-chain access management. + * For each role, is extension implements an equivalent "aliased" role that is used for + * restricting calls originating from other chains. + * + * For example, if a function `myFunction` is protected by `onlyRole(SOME_ROLE)`, and + * if an address `x` has role `SOME_ROLE`, it would be able to call `myFunction` directly. + * A wallet or contract at the same address on another chain would however not be able + * to call this function. In order to do so, it would require to have the role + * `_crossChainRoleAlias(SOME_ROLE)`. + * + * This aliasing is required to protect against multiple contracts living at the same + * address on different chains but controlled by conflicting entities. + * + * _Available since v4.6._ + */ +abstract contract AccessControlCrossChain is AccessControl, CrossChainEnabled { + bytes32 public constant CROSSCHAIN_ALIAS = keccak256("CROSSCHAIN_ALIAS"); + + /** + * @dev See {AccessControl-_checkRole}. + */ + function _checkRole(bytes32 role) internal view virtual override { + if (_isCrossChain()) { + _checkRole(_crossChainRoleAlias(role), _crossChainSender()); + } else { + super._checkRole(role); + } + } + + /** + * @dev Returns the aliased role corresponding to `role`. + */ + function _crossChainRoleAlias(bytes32 role) internal pure virtual returns (bytes32) { + return role ^ CROSSCHAIN_ALIAS; + } +} diff --git a/contracts/access/README.adoc b/contracts/access/README.adoc index 2e84c09ad43..0959e1a7330 100644 --- a/contracts/access/README.adoc +++ b/contracts/access/README.adoc @@ -16,6 +16,8 @@ This directory provides ways to restrict who can access the functions of a contr {{AccessControl}} +{{AccessControlCrossChain}} + {{IAccessControlEnumerable}} {{AccessControlEnumerable}} diff --git a/contracts/crosschain/CrossChainEnabled.sol b/contracts/crosschain/CrossChainEnabled.sol new file mode 100644 index 00000000000..cd30a679a26 --- /dev/null +++ b/contracts/crosschain/CrossChainEnabled.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.4; + +import "./errors.sol"; + +/** + * @dev Provides information for building cross-chain aware contracts. This + * abstract contract provides accessors and modifiers to control the execution + * flow when receiving cross-chain messages. + * + * Actual implementations of cross-chain aware contracts, which are based on + * this abstraction, will have to inherit from a bridge-specific + * specialization. Such specializations are provided under + * `crosschain//CrossChainEnabled.sol`. + * + * _Available since v4.6._ + */ +abstract contract CrossChainEnabled { + /** + * @dev Throws if the current function call is not the result of a + * cross-chain execution. + */ + modifier onlyCrossChain() { + if (!_isCrossChain()) revert NotCrossChainCall(); + _; + } + + /** + * @dev Throws if the current function call is not the result of a + * cross-chain execution initiated by `account`. + */ + modifier onlyCrossChainSender(address expected) { + address actual = _crossChainSender(); + if (expected != actual) revert InvalidCrossChainSender(actual, expected); + _; + } + + /** + * @dev Returns whether the current function call is the result of a + * cross-chain message. + */ + function _isCrossChain() internal view virtual returns (bool); + + /** + * @dev Returns the address of the sender of the cross-chain message that + * triggered the current function call. + * + * IMPORTANT: Should revert with `NotCrossChainCall` if the current function + * call is not the result of a cross-chain message. + */ + function _crossChainSender() internal view virtual returns (address); +} diff --git a/contracts/crosschain/README.adoc b/contracts/crosschain/README.adoc new file mode 100644 index 00000000000..2fce20320f7 --- /dev/null +++ b/contracts/crosschain/README.adoc @@ -0,0 +1,34 @@ += Cross Chain Awareness + +[.readme-notice] +NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/api/crosschain + +This directory provides building blocks to improve cross-chain awareness of smart contracts. + +- {CrossChainEnabled} is an abstraction that contains accessors and modifiers to control the execution flow when receiving cross-chain messages. + +== CrossChainEnabled specializations + +The following specializations of {CrossChainEnabled} provide implementations of the {CrossChainEnabled} abstraction for specific bridges. This can be used to complexe cross-chain aware components such as {AccessControlCrossChain}. + +{{CrossChainEnabledAMB}} + +{{CrossChainEnabledArbitrumL1}} + +{{CrossChainEnabledArbitrumL2}} + +{{CrossChainEnabledOptimism}} + +{{CrossChainEnabledPolygonChild}} + +== Libraries for cross-chain + +In addition to the {CrossChainEnable} abstraction, cross-chain awareness is also available through libraries. These libraries can be used to build complex designs such as contracts with the ability to interact with multiple bridges. + +{{LibAMB}} + +{{LibArbitrumL1}} + +{{LibArbitrumL2}} + +{{LibOptimism}} diff --git a/contracts/crosschain/amb/CrossChainEnabledAMB.sol b/contracts/crosschain/amb/CrossChainEnabledAMB.sol new file mode 100644 index 00000000000..445f04780bd --- /dev/null +++ b/contracts/crosschain/amb/CrossChainEnabledAMB.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.4; + +import "../CrossChainEnabled.sol"; +import "./LibAMB.sol"; + +/** + * @dev [AMB](https://docs.tokenbridge.net/amb-bridge/about-amb-bridge) + * specialization or the {CrossChainEnabled} abstraction. + * + * As of february 2020, AMB bridges are available between the following chains: + * - [ETH <> xDai](https://docs.tokenbridge.net/eth-xdai-amb-bridge/about-the-eth-xdai-amb) + * - [ETH <> qDai](https://docs.tokenbridge.net/eth-qdai-bridge/about-the-eth-qdai-amb) + * - [ETH <> ETC](https://docs.tokenbridge.net/eth-etc-amb-bridge/about-the-eth-etc-amb) + * - [ETH <> BSC](https://docs.tokenbridge.net/eth-bsc-amb/about-the-eth-bsc-amb) + * - [ETH <> POA](https://docs.tokenbridge.net/eth-poa-amb-bridge/about-the-eth-poa-amb) + * - [BSC <> xDai](https://docs.tokenbridge.net/bsc-xdai-amb/about-the-bsc-xdai-amb) + * - [POA <> xDai](https://docs.tokenbridge.net/poa-xdai-amb/about-the-poa-xdai-amb) + * - [Rinkeby <> xDai](https://docs.tokenbridge.net/rinkeby-xdai-amb-bridge/about-the-rinkeby-xdai-amb) + * - [Kovan <> Sokol](https://docs.tokenbridge.net/kovan-sokol-amb-bridge/about-the-kovan-sokol-amb) + * + * _Available since v4.6._ + */ +contract CrossChainEnabledAMB is CrossChainEnabled { + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + address private immutable _bridge; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor(address bridge) { + _bridge = bridge; + } + + /** + * @dev see {CrossChainEnabled-_isCrossChain} + */ + function _isCrossChain() internal view virtual override returns (bool) { + return LibAMB.isCrossChain(_bridge); + } + + /** + * @dev see {CrossChainEnabled-_crossChainSender} + */ + function _crossChainSender() internal view virtual override onlyCrossChain returns (address) { + return LibAMB.crossChainSender(_bridge); + } +} diff --git a/contracts/crosschain/amb/LibAMB.sol b/contracts/crosschain/amb/LibAMB.sol new file mode 100644 index 00000000000..74ee922bff4 --- /dev/null +++ b/contracts/crosschain/amb/LibAMB.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.4; + +import {IAMB as AMB_Bridge} from "../../vendor/amb/IAMB.sol"; +import "../errors.sol"; + +/** + * @dev Primitives for cross-chain aware contracts using the + * [AMB](https://docs.tokenbridge.net/amb-bridge/about-amb-bridge) + * family of bridges. + */ +library LibAMB { + /** + * @dev Returns whether the current function call is the result of a + * cross-chain message relayed by `bridge`. + */ + function isCrossChain(address bridge) internal view returns (bool) { + return msg.sender == bridge; + } + + /** + * @dev Returns the address of the sender that triggered the current + * cross-chain message through `bridge`. + * + * NOTE: {isCrossChain} should be checked before trying to recover the + * sender, as it will revert with `NotCrossChainCall` if the current + * function call is not the result of a cross-chain message. + */ + function crossChainSender(address bridge) internal view returns (address) { + if (!isCrossChain(bridge)) revert NotCrossChainCall(); + return AMB_Bridge(bridge).messageSender(); + } +} diff --git a/contracts/crosschain/arbitrum/CrossChainEnabledArbitrumL1.sol b/contracts/crosschain/arbitrum/CrossChainEnabledArbitrumL1.sol new file mode 100644 index 00000000000..69c5abd56e1 --- /dev/null +++ b/contracts/crosschain/arbitrum/CrossChainEnabledArbitrumL1.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.4; + +import "../CrossChainEnabled.sol"; +import "./LibArbitrumL1.sol"; + +/** + * @dev [Arbitrum](https://arbitrum.io/) specialization or the + * {CrossChainEnabled} abstraction the L1 side (mainnet). + * + * This version should only be deployed on L1 to process cross-chain messages + * originating from L2. For the other side, use {CrossChainEnabledArbitrumL2}. + * + * The bridge contract is provided and maintained by the arbitrum team. You can + * find the address of this contract on the rinkeby testnet in + * [Arbitrum's developer documentation](https://developer.offchainlabs.com/docs/useful_addresses). + * + * _Available since v4.6._ + */ +abstract contract CrossChainEnabledArbitrumL1 is CrossChainEnabled { + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + address private immutable _bridge; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor(address bridge) { + _bridge = bridge; + } + + /** + * @dev see {CrossChainEnabled-_isCrossChain} + */ + function _isCrossChain() internal view virtual override returns (bool) { + return LibArbitrumL1.isCrossChain(_bridge); + } + + /** + * @dev see {CrossChainEnabled-_crossChainSender} + */ + function _crossChainSender() internal view virtual override onlyCrossChain returns (address) { + return LibArbitrumL1.crossChainSender(_bridge); + } +} diff --git a/contracts/crosschain/arbitrum/CrossChainEnabledArbitrumL2.sol b/contracts/crosschain/arbitrum/CrossChainEnabledArbitrumL2.sol new file mode 100644 index 00000000000..061862efd15 --- /dev/null +++ b/contracts/crosschain/arbitrum/CrossChainEnabledArbitrumL2.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.4; + +import "../CrossChainEnabled.sol"; +import "./LibArbitrumL2.sol"; + +/** + * @dev [Arbitrum](https://arbitrum.io/) specialization or the + * {CrossChainEnabled} abstraction the L2 side (arbitrum). + * + * This version should only be deployed on L2 to process cross-chain messages + * originating from L1. For the other side, use {CrossChainEnabledArbitrumL1}. + * + * Arbitrum L2 includes the `ArbSys` contract at a fixed address. Therefore, + * this specialization of {CrossChainEnabled} does not include a constructor. + * + * _Available since v4.6._ + */ +abstract contract CrossChainEnabledArbitrumL2 is CrossChainEnabled { + /** + * @dev see {CrossChainEnabled-_isCrossChain} + */ + function _isCrossChain() internal view virtual override returns (bool) { + return LibArbitrumL2.isCrossChain(LibArbitrumL2.ARBSYS); + } + + /** + * @dev see {CrossChainEnabled-_crossChainSender} + */ + function _crossChainSender() internal view virtual override onlyCrossChain returns (address) { + return LibArbitrumL2.crossChainSender(LibArbitrumL2.ARBSYS); + } +} diff --git a/contracts/crosschain/arbitrum/LibArbitrumL1.sol b/contracts/crosschain/arbitrum/LibArbitrumL1.sol new file mode 100644 index 00000000000..55c9a50d44e --- /dev/null +++ b/contracts/crosschain/arbitrum/LibArbitrumL1.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.4; + +import {IBridge as ArbitrumL1_Bridge} from "../../vendor/arbitrum/IBridge.sol"; +import {IInbox as ArbitrumL1_Inbox} from "../../vendor/arbitrum/IInbox.sol"; +import {IOutbox as ArbitrumL1_Outbox} from "../../vendor/arbitrum/IOutbox.sol"; +import "../errors.sol"; + +/** + * @dev Primitives for cross-chain aware contracts for + * [Arbitrum](https://arbitrum.io/). + * + * This version should only be used on L1 to process cross-chain messages + * originating from L2. For the other side, use {LibArbitrumL2}. + */ +library LibArbitrumL1 { + /** + * @dev Returns whether the current function call is the result of a + * cross-chain message relayed by the `bridge`. + */ + function isCrossChain(address bridge) internal view returns (bool) { + return msg.sender == bridge; + } + + /** + * @dev Returns the address of the sender that triggered the current + * cross-chain message through the `bridge`. + * + * NOTE: {isCrossChain} should be checked before trying to recover the + * sender, as it will revert with `NotCrossChainCall` if the current + * function call is not the result of a cross-chain message. + */ + function crossChainSender(address bridge) internal view returns (address) { + if (!isCrossChain(bridge)) revert NotCrossChainCall(); + + address sender = ArbitrumL1_Outbox(ArbitrumL1_Bridge(bridge).activeOutbox()).l2ToL1Sender(); + require(sender != address(0), "LibArbitrumL1: system messages without sender"); + + return sender; + } +} diff --git a/contracts/crosschain/arbitrum/LibArbitrumL2.sol b/contracts/crosschain/arbitrum/LibArbitrumL2.sol new file mode 100644 index 00000000000..c8a6f541a16 --- /dev/null +++ b/contracts/crosschain/arbitrum/LibArbitrumL2.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.4; + +import {IArbSys as ArbitrumL2_Bridge} from "../../vendor/arbitrum/IArbSys.sol"; +import "../errors.sol"; + +/** + * @dev Primitives for cross-chain aware contracts for + * [Arbitrum](https://arbitrum.io/). + * + * This version should only be used on L2 to process cross-chain messages + * originating from L1. For the other side, use {LibArbitrumL1}. + */ +library LibArbitrumL2 { + /** + * @dev Returns whether the current function call is the result of a + * cross-chain message relayed by `arbsys`. + */ + address public constant ARBSYS = 0x0000000000000000000000000000000000000064; + + function isCrossChain(address arbsys) internal view returns (bool) { + return ArbitrumL2_Bridge(arbsys).isTopLevelCall(); + } + + /** + * @dev Returns the address of the sender that triggered the current + * cross-chain message through `arbsys`. + * + * NOTE: {isCrossChain} should be checked before trying to recover the + * sender, as it will revert with `NotCrossChainCall` if the current + * function call is not the result of a cross-chain message. + */ + function crossChainSender(address arbsys) internal view returns (address) { + if (!isCrossChain(arbsys)) revert NotCrossChainCall(); + + return + ArbitrumL2_Bridge(arbsys).wasMyCallersAddressAliased() + ? ArbitrumL2_Bridge(arbsys).myCallersAddressWithoutAliasing() + : msg.sender; + } +} diff --git a/contracts/crosschain/errors.sol b/contracts/crosschain/errors.sol new file mode 100644 index 00000000000..fcc94778483 --- /dev/null +++ b/contracts/crosschain/errors.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.4; + +error NotCrossChainCall(); +error InvalidCrossChainSender(address actual, address expected); diff --git a/contracts/crosschain/optimism/CrossChainEnabledOptimism.sol b/contracts/crosschain/optimism/CrossChainEnabledOptimism.sol new file mode 100644 index 00000000000..fe3564cbf36 --- /dev/null +++ b/contracts/crosschain/optimism/CrossChainEnabledOptimism.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.4; + +import "../CrossChainEnabled.sol"; +import "./LibOptimism.sol"; + +/** + * @dev [Optimism](https://www.optimism.io/) specialization or the + * {CrossChainEnabled} abstraction. + * + * The messenger (`CrossDomainMessenger`) contract is provided and maintained by + * the optimism team. You can find the address of this contract on mainnet and + * kovan in the [deployments section of Optimism monorepo](https://github.com/ethereum-optimism/optimism/tree/develop/packages/contracts/deployments). + * + * _Available since v4.6._ + */ +abstract contract CrossChainEnabledOptimism is CrossChainEnabled { + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + address private immutable _messenger; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor(address messenger) { + _messenger = messenger; + } + + /** + * @dev see {CrossChainEnabled-_isCrossChain} + */ + function _isCrossChain() internal view virtual override returns (bool) { + return LibOptimism.isCrossChain(_messenger); + } + + /** + * @dev see {CrossChainEnabled-_crossChainSender} + */ + function _crossChainSender() internal view virtual override onlyCrossChain returns (address) { + return LibOptimism.crossChainSender(_messenger); + } +} diff --git a/contracts/crosschain/optimism/LibOptimism.sol b/contracts/crosschain/optimism/LibOptimism.sol new file mode 100644 index 00000000000..3dfc5daf4ca --- /dev/null +++ b/contracts/crosschain/optimism/LibOptimism.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.4; + +import {ICrossDomainMessenger as Optimism_Bridge} from "../../vendor/optimism/ICrossDomainMessenger.sol"; +import "../errors.sol"; + +/** + * @dev Primitives for cross-chain aware contracts for [Optimism](https://www.optimism.io/). + * See the [documentation](https://community.optimism.io/docs/developers/bridge/messaging/#accessing-msg-sender) + * for the functionality used here. + */ +library LibOptimism { + /** + * @dev Returns whether the current function call is the result of a + * cross-chain message relayed by `messenger`. + */ + function isCrossChain(address messenger) internal view returns (bool) { + return msg.sender == messenger; + } + + /** + * @dev Returns the address of the sender that triggered the current + * cross-chain message through `messenger`. + * + * NOTE: {isCrossChain} should be checked before trying to recover the + * sender, as it will revert with `NotCrossChainCall` if the current + * function call is not the result of a cross-chain message. + */ + function crossChainSender(address messenger) internal view returns (address) { + if (!isCrossChain(messenger)) revert NotCrossChainCall(); + + return Optimism_Bridge(messenger).xDomainMessageSender(); + } +} diff --git a/contracts/crosschain/polygon/CrossChainEnabledPolygonChild.sol b/contracts/crosschain/polygon/CrossChainEnabledPolygonChild.sol new file mode 100644 index 00000000000..e7555a3c896 --- /dev/null +++ b/contracts/crosschain/polygon/CrossChainEnabledPolygonChild.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.4; + +import "../CrossChainEnabled.sol"; +import "../../security/ReentrancyGuard.sol"; +import "../../utils/Address.sol"; +import "../../vendor/polygon/IFxMessageProcessor.sol"; + +address constant DEFAULT_SENDER = 0x000000000000000000000000000000000000dEaD; + +/** + * @dev [Polygon](https://polygon.technology/) specialization or the + * {CrossChainEnabled} abstraction the child side (polygon/mumbai). + * + * This version should only be deployed on child chain to process cross-chain + * messages originating from the parent chain. + * + * The fxChild contract is provided and maintained by the polygon team. You can + * find the address of this contract polygon and mumbai in + * [Polygon's Fx-Portal documentation](https://docs.polygon.technology/docs/develop/l1-l2-communication/fx-portal/#contract-addresses). + * + * _Available since v4.6._ + */ +abstract contract CrossChainEnabledPolygonChild is IFxMessageProcessor, CrossChainEnabled, ReentrancyGuard { + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + address private immutable _fxChild; + address private _sender = DEFAULT_SENDER; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor(address fxChild) { + _fxChild = fxChild; + } + + /** + * @dev see {CrossChainEnabled-_isCrossChain} + */ + function _isCrossChain() internal view virtual override returns (bool) { + return msg.sender == _fxChild; + } + + /** + * @dev see {CrossChainEnabled-_crossChainSender} + */ + function _crossChainSender() internal view virtual override onlyCrossChain returns (address) { + return _sender; + } + + /** + * @dev External entry point to receive and relay messages originating + * from the fxChild. + * + * Non-reentrancy is crucial to avoid a cross-chain call being able + * to impersonate anyone by just looping through this with user-defined + * arguments. + * + * Note: if _fxChild calls any other function that does a delegate-call, + * then security could be compromised. + */ + function processMessageFromRoot( + uint256, /* stateId */ + address rootMessageSender, + bytes calldata data + ) external override nonReentrant { + require(msg.sender == _fxChild, "unauthorized cross-chain relay"); + + _sender = rootMessageSender; + Address.functionDelegateCall(address(this), data, "crosschain execution failled"); + _sender = DEFAULT_SENDER; + } +} diff --git a/contracts/governance/extensions/GovernorTimelockCompound.sol b/contracts/governance/extensions/GovernorTimelockCompound.sol index d14489aa098..b6421b89541 100644 --- a/contracts/governance/extensions/GovernorTimelockCompound.sol +++ b/contracts/governance/extensions/GovernorTimelockCompound.sol @@ -6,60 +6,7 @@ pragma solidity ^0.8.0; import "./IGovernorTimelock.sol"; import "../Governor.sol"; import "../../utils/math/SafeCast.sol"; - -/** - * https://github.com/compound-finance/compound-protocol/blob/master/contracts/Timelock.sol[Compound's timelock] interface - */ -interface ICompoundTimelock { - receive() external payable; - - // solhint-disable-next-line func-name-mixedcase - function GRACE_PERIOD() external view returns (uint256); - - // solhint-disable-next-line func-name-mixedcase - function MINIMUM_DELAY() external view returns (uint256); - - // solhint-disable-next-line func-name-mixedcase - function MAXIMUM_DELAY() external view returns (uint256); - - function admin() external view returns (address); - - function pendingAdmin() external view returns (address); - - function delay() external view returns (uint256); - - function queuedTransactions(bytes32) external view returns (bool); - - function setDelay(uint256) external; - - function acceptAdmin() external; - - function setPendingAdmin(address) external; - - function queueTransaction( - address target, - uint256 value, - string memory signature, - bytes memory data, - uint256 eta - ) external returns (bytes32); - - function cancelTransaction( - address target, - uint256 value, - string memory signature, - bytes memory data, - uint256 eta - ) external; - - function executeTransaction( - address target, - uint256 value, - string memory signature, - bytes memory data, - uint256 eta - ) external payable returns (bytes memory); -} +import "../../vendor/compound/ICompoundTimelock.sol"; /** * @dev Extension of {Governor} that binds the execution process to a Compound Timelock. This adds a delay, enforced by diff --git a/contracts/mocks/AccessControlCrossChainMock.sol b/contracts/mocks/AccessControlCrossChainMock.sol new file mode 100644 index 00000000000..90adb740cd2 --- /dev/null +++ b/contracts/mocks/AccessControlCrossChainMock.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.4; + +import "../access/AccessControlCrossChain.sol"; +import "../crosschain/arbitrum/CrossChainEnabledArbitrumL2.sol"; + +contract AccessControlCrossChainMock is AccessControlCrossChain, CrossChainEnabledArbitrumL2 { + constructor() { + _setupRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + function setRoleAdmin(bytes32 roleId, bytes32 adminRoleId) public { + _setRoleAdmin(roleId, adminRoleId); + } + + function senderProtected(bytes32 roleId) public onlyRole(roleId) {} + + function crossChainRoleAlias(bytes32 role) public pure virtual returns (bytes32) { + return _crossChainRoleAlias(role); + } +} diff --git a/contracts/mocks/crosschain/bridges.sol b/contracts/mocks/crosschain/bridges.sol new file mode 100644 index 00000000000..5b9d20358a8 --- /dev/null +++ b/contracts/mocks/crosschain/bridges.sol @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../../utils/Address.sol"; +import "../../vendor/polygon/IFxMessageProcessor.sol"; + +abstract contract BaseRelayMock { + // needed to parse custom errors + error NotCrossChainCall(); + error InvalidCrossChainSender(address sender, address expected); + + address internal _currentSender; + + function relayAs( + address target, + bytes calldata data, + address sender + ) external virtual { + address previousSender = _currentSender; + + _currentSender = sender; + + (bool success, bytes memory returndata) = target.call(data); + Address.verifyCallResult(success, returndata, "low-level call reverted"); + + _currentSender = previousSender; + } +} + +/** + * AMB + */ +contract BridgeAMBMock is BaseRelayMock { + function messageSender() public view returns (address) { + return _currentSender; + } +} + +/** + * Arbitrum + */ +contract BridgeArbitrumL1Mock is BaseRelayMock { + address public immutable inbox = address(new BridgeArbitrumL1Inbox()); + address public immutable outbox = address(new BridgeArbitrumL1Outbox()); + + function activeOutbox() public view returns (address) { + return outbox; + } + + function currentSender() public view returns (address) { + return _currentSender; + } +} + +contract BridgeArbitrumL1Inbox { + address public immutable bridge = msg.sender; +} + +contract BridgeArbitrumL1Outbox { + address public immutable bridge = msg.sender; + + function l2ToL1Sender() public view returns (address) { + return BridgeArbitrumL1Mock(bridge).currentSender(); + } +} + +contract BridgeArbitrumL2Mock is BaseRelayMock { + function isTopLevelCall() public view returns (bool) { + return _currentSender != address(0); + } + + function wasMyCallersAddressAliased() public pure returns (bool) { + return true; + } + + function myCallersAddressWithoutAliasing() public view returns (address) { + return _currentSender; + } +} + +/** + * Optimism + */ +contract BridgeOptimismMock is BaseRelayMock { + function xDomainMessageSender() public view returns (address) { + return _currentSender; + } +} + +/** + * Polygon + */ +contract BridgePolygonChildMock is BaseRelayMock { + function relayAs( + address target, + bytes calldata data, + address sender + ) external override { + IFxMessageProcessor(target).processMessageFromRoot(0, sender, data); + } +} diff --git a/contracts/mocks/crosschain/receivers.sol b/contracts/mocks/crosschain/receivers.sol new file mode 100644 index 00000000000..57b9b5d7e9f --- /dev/null +++ b/contracts/mocks/crosschain/receivers.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.4; + +import "../../access/Ownable.sol"; +import "../../crosschain/amb/CrossChainEnabledAMB.sol"; +import "../../crosschain/arbitrum/CrossChainEnabledArbitrumL1.sol"; +import "../../crosschain/arbitrum/CrossChainEnabledArbitrumL2.sol"; +import "../../crosschain/optimism/CrossChainEnabledOptimism.sol"; +import "../../crosschain/polygon/CrossChainEnabledPolygonChild.sol"; + +abstract contract Receiver is Ownable, CrossChainEnabled { + function crossChainRestricted() external onlyCrossChain {} + + function crossChainOwnerRestricted() external onlyCrossChainSender(owner()) {} +} + +/** + * AMB + */ +contract CrossChainEnabledAMBMock is Receiver, CrossChainEnabledAMB { + constructor(address bridge) CrossChainEnabledAMB(bridge) {} +} + +/** + * Arbitrum + */ +contract CrossChainEnabledArbitrumL1Mock is Receiver, CrossChainEnabledArbitrumL1 { + constructor(address bridge) CrossChainEnabledArbitrumL1(bridge) {} +} + +contract CrossChainEnabledArbitrumL2Mock is Receiver, CrossChainEnabledArbitrumL2 {} + +/** + * Optimism + */ +contract CrossChainEnabledOptimismMock is Receiver, CrossChainEnabledOptimism { + constructor(address bridge) CrossChainEnabledOptimism(bridge) {} +} + +/** + * Polygon + */ +contract CrossChainEnabledPolygonChildMock is Receiver, CrossChainEnabledPolygonChild { + constructor(address bridge) CrossChainEnabledPolygonChild(bridge) {} +} diff --git a/contracts/vendor/amb/IAMB.sol b/contracts/vendor/amb/IAMB.sol new file mode 100644 index 00000000000..529913f7bdf --- /dev/null +++ b/contracts/vendor/amb/IAMB.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IAMB { + event UserRequestForAffirmation(bytes32 indexed messageId, bytes encodedData); + event UserRequestForSignature(bytes32 indexed messageId, bytes encodedData); + event AffirmationCompleted( + address indexed sender, + address indexed executor, + bytes32 indexed messageId, + bool status + ); + event RelayedMessage(address indexed sender, address indexed executor, bytes32 indexed messageId, bool status); + + function messageSender() external view returns (address); + + function maxGasPerTx() external view returns (uint256); + + function transactionHash() external view returns (bytes32); + + function messageId() external view returns (bytes32); + + function messageSourceChainId() external view returns (bytes32); + + function messageCallStatus(bytes32 _messageId) external view returns (bool); + + function failedMessageDataHash(bytes32 _messageId) external view returns (bytes32); + + function failedMessageReceiver(bytes32 _messageId) external view returns (address); + + function failedMessageSender(bytes32 _messageId) external view returns (address); + + function requireToPassMessage( + address _contract, + bytes calldata _data, + uint256 _gas + ) external returns (bytes32); + + function requireToConfirmMessage( + address _contract, + bytes calldata _data, + uint256 _gas + ) external returns (bytes32); + + function sourceChainId() external view returns (uint256); + + function destinationChainId() external view returns (uint256); +} diff --git a/contracts/vendor/arbitrum/IArbSys.sol b/contracts/vendor/arbitrum/IArbSys.sol new file mode 100644 index 00000000000..3658e4660e0 --- /dev/null +++ b/contracts/vendor/arbitrum/IArbSys.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.4.21 <0.9.0; + +/** + * @title Precompiled contract that exists in every Arbitrum chain at address(100), 0x0000000000000000000000000000000000000064. Exposes a variety of system-level functionality. + */ +interface IArbSys { + /** + * @notice Get internal version number identifying an ArbOS build + * @return version number as int + */ + function arbOSVersion() external pure returns (uint256); + + function arbChainID() external view returns (uint256); + + /** + * @notice Get Arbitrum block number (distinct from L1 block number; Arbitrum genesis block has block number 0) + * @return block number as int + */ + function arbBlockNumber() external view returns (uint256); + + /** + * @notice Send given amount of Eth to dest from sender. + * This is a convenience function, which is equivalent to calling sendTxToL1 with empty calldataForL1. + * @param destination recipient address on L1 + * @return unique identifier for this L2-to-L1 transaction. + */ + function withdrawEth(address destination) external payable returns (uint256); + + /** + * @notice Send a transaction to L1 + * @param destination recipient address on L1 + * @param calldataForL1 (optional) calldata for L1 contract call + * @return a unique identifier for this L2-to-L1 transaction. + */ + function sendTxToL1(address destination, bytes calldata calldataForL1) external payable returns (uint256); + + /** + * @notice get the number of transactions issued by the given external account or the account sequence number of the given contract + * @param account target account + * @return the number of transactions issued by the given external account or the account sequence number of the given contract + */ + function getTransactionCount(address account) external view returns (uint256); + + /** + * @notice get the value of target L2 storage slot + * This function is only callable from address 0 to prevent contracts from being able to call it + * @param account target account + * @param index target index of storage slot + * @return stotage value for the given account at the given index + */ + function getStorageAt(address account, uint256 index) external view returns (uint256); + + /** + * @notice check if current call is coming from l1 + * @return true if the caller of this was called directly from L1 + */ + function isTopLevelCall() external view returns (bool); + + /** + * @notice check if the caller (of this caller of this) is an aliased L1 contract address + * @return true iff the caller's address is an alias for an L1 contract address + */ + function wasMyCallersAddressAliased() external view returns (bool); + + /** + * @notice return the address of the caller (of this caller of this), without applying L1 contract address aliasing + * @return address of the caller's caller, without applying L1 contract address aliasing + */ + function myCallersAddressWithoutAliasing() external view returns (address); + + /** + * @notice map L1 sender contract address to its L2 alias + * @param sender sender address + * @param dest destination address + * @return aliased sender address + */ + function mapL1SenderContractAddressToL2Alias(address sender, address dest) external pure returns (address); + + /** + * @notice get the caller's amount of available storage gas + * @return amount of storage gas available to the caller + */ + function getStorageGasAvailable() external view returns (uint256); + + event L2ToL1Transaction( + address caller, + address indexed destination, + uint256 indexed uniqueId, + uint256 indexed batchNumber, + uint256 indexInBatch, + uint256 arbBlockNum, + uint256 ethBlockNum, + uint256 timestamp, + uint256 callvalue, + bytes data + ); +} diff --git a/contracts/vendor/arbitrum/IBridge.sol b/contracts/vendor/arbitrum/IBridge.sol new file mode 100644 index 00000000000..631fce68fdf --- /dev/null +++ b/contracts/vendor/arbitrum/IBridge.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright 2021, Offchain Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +pragma solidity ^0.8.0; + +interface IBridge { + event MessageDelivered( + uint256 indexed messageIndex, + bytes32 indexed beforeInboxAcc, + address inbox, + uint8 kind, + address sender, + bytes32 messageDataHash + ); + + event BridgeCallTriggered(address indexed outbox, address indexed destAddr, uint256 amount, bytes data); + + event InboxToggle(address indexed inbox, bool enabled); + + event OutboxToggle(address indexed outbox, bool enabled); + + function deliverMessageToInbox( + uint8 kind, + address sender, + bytes32 messageDataHash + ) external payable returns (uint256); + + function executeCall( + address destAddr, + uint256 amount, + bytes calldata data + ) external returns (bool success, bytes memory returnData); + + // These are only callable by the admin + function setInbox(address inbox, bool enabled) external; + + function setOutbox(address inbox, bool enabled) external; + + // View functions + + function activeOutbox() external view returns (address); + + function allowedInboxes(address inbox) external view returns (bool); + + function allowedOutboxes(address outbox) external view returns (bool); + + function inboxAccs(uint256 index) external view returns (bytes32); + + function messageCount() external view returns (uint256); +} diff --git a/contracts/vendor/arbitrum/IInbox.sol b/contracts/vendor/arbitrum/IInbox.sol new file mode 100644 index 00000000000..462e24738e5 --- /dev/null +++ b/contracts/vendor/arbitrum/IInbox.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright 2021, Offchain Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +pragma solidity ^0.8.0; + +import "./IMessageProvider.sol"; + +interface IInbox is IMessageProvider { + function sendL2Message(bytes calldata messageData) external returns (uint256); + + function sendUnsignedTransaction( + uint256 maxGas, + uint256 gasPriceBid, + uint256 nonce, + address destAddr, + uint256 amount, + bytes calldata data + ) external returns (uint256); + + function sendContractTransaction( + uint256 maxGas, + uint256 gasPriceBid, + address destAddr, + uint256 amount, + bytes calldata data + ) external returns (uint256); + + function sendL1FundedUnsignedTransaction( + uint256 maxGas, + uint256 gasPriceBid, + uint256 nonce, + address destAddr, + bytes calldata data + ) external payable returns (uint256); + + function sendL1FundedContractTransaction( + uint256 maxGas, + uint256 gasPriceBid, + address destAddr, + bytes calldata data + ) external payable returns (uint256); + + function createRetryableTicket( + address destAddr, + uint256 arbTxCallValue, + uint256 maxSubmissionCost, + address submissionRefundAddress, + address valueRefundAddress, + uint256 maxGas, + uint256 gasPriceBid, + bytes calldata data + ) external payable returns (uint256); + + function createRetryableTicketNoRefundAliasRewrite( + address destAddr, + uint256 arbTxCallValue, + uint256 maxSubmissionCost, + address submissionRefundAddress, + address valueRefundAddress, + uint256 maxGas, + uint256 gasPriceBid, + bytes calldata data + ) external payable returns (uint256); + + function depositEth(uint256 maxSubmissionCost) external payable returns (uint256); + + function bridge() external view returns (address); + + function pauseCreateRetryables() external; + + function unpauseCreateRetryables() external; + + function startRewriteAddress() external; + + function stopRewriteAddress() external; +} diff --git a/contracts/vendor/arbitrum/IMessageProvider.sol b/contracts/vendor/arbitrum/IMessageProvider.sol new file mode 100644 index 00000000000..76208e21586 --- /dev/null +++ b/contracts/vendor/arbitrum/IMessageProvider.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright 2021, Offchain Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +pragma solidity ^0.8.0; + +interface IMessageProvider { + event InboxMessageDelivered(uint256 indexed messageNum, bytes data); + + event InboxMessageDeliveredFromOrigin(uint256 indexed messageNum); +} diff --git a/contracts/vendor/arbitrum/IOutbox.sol b/contracts/vendor/arbitrum/IOutbox.sol new file mode 100644 index 00000000000..9128a2049e5 --- /dev/null +++ b/contracts/vendor/arbitrum/IOutbox.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright 2021, Offchain Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +pragma solidity ^0.8.0; + +interface IOutbox { + event OutboxEntryCreated( + uint256 indexed batchNum, + uint256 outboxEntryIndex, + bytes32 outputRoot, + uint256 numInBatch + ); + event OutBoxTransactionExecuted( + address indexed destAddr, + address indexed l2Sender, + uint256 indexed outboxEntryIndex, + uint256 transactionIndex + ); + + function l2ToL1Sender() external view returns (address); + + function l2ToL1Block() external view returns (uint256); + + function l2ToL1EthBlock() external view returns (uint256); + + function l2ToL1Timestamp() external view returns (uint256); + + function l2ToL1BatchNum() external view returns (uint256); + + function l2ToL1OutputId() external view returns (bytes32); + + function processOutgoingMessages(bytes calldata sendsData, uint256[] calldata sendLengths) external; + + function outboxEntryExists(uint256 batchNum) external view returns (bool); +} diff --git a/contracts/vendor/compound/ICompoundTimelock.sol b/contracts/vendor/compound/ICompoundTimelock.sol new file mode 100644 index 00000000000..1581a8daa56 --- /dev/null +++ b/contracts/vendor/compound/ICompoundTimelock.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +/** + * https://github.com/compound-finance/compound-protocol/blob/master/contracts/Timelock.sol[Compound's timelock] interface + */ +interface ICompoundTimelock { + event NewAdmin(address indexed newAdmin); + event NewPendingAdmin(address indexed newPendingAdmin); + event NewDelay(uint256 indexed newDelay); + event CancelTransaction( + bytes32 indexed txHash, + address indexed target, + uint256 value, + string signature, + bytes data, + uint256 eta + ); + event ExecuteTransaction( + bytes32 indexed txHash, + address indexed target, + uint256 value, + string signature, + bytes data, + uint256 eta + ); + event QueueTransaction( + bytes32 indexed txHash, + address indexed target, + uint256 value, + string signature, + bytes data, + uint256 eta + ); + + receive() external payable; + + // solhint-disable-next-line func-name-mixedcase + function GRACE_PERIOD() external view returns (uint256); + + // solhint-disable-next-line func-name-mixedcase + function MINIMUM_DELAY() external view returns (uint256); + + // solhint-disable-next-line func-name-mixedcase + function MAXIMUM_DELAY() external view returns (uint256); + + function admin() external view returns (address); + + function pendingAdmin() external view returns (address); + + function delay() external view returns (uint256); + + function queuedTransactions(bytes32) external view returns (bool); + + function setDelay(uint256) external; + + function acceptAdmin() external; + + function setPendingAdmin(address) external; + + function queueTransaction( + address target, + uint256 value, + string memory signature, + bytes memory data, + uint256 eta + ) external returns (bytes32); + + function cancelTransaction( + address target, + uint256 value, + string memory signature, + bytes memory data, + uint256 eta + ) external; + + function executeTransaction( + address target, + uint256 value, + string memory signature, + bytes memory data, + uint256 eta + ) external payable returns (bytes memory); +} diff --git a/contracts/vendor/compound/LICENSE b/contracts/vendor/compound/LICENSE new file mode 100644 index 00000000000..7da232470a0 --- /dev/null +++ b/contracts/vendor/compound/LICENSE @@ -0,0 +1,11 @@ +Copyright 2020 Compound Labs, Inc. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/contracts/vendor/optimism/ICrossDomainMessenger.sol b/contracts/vendor/optimism/ICrossDomainMessenger.sol new file mode 100644 index 00000000000..be09e857d62 --- /dev/null +++ b/contracts/vendor/optimism/ICrossDomainMessenger.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity >0.5.0 <0.9.0; + +/** + * @title ICrossDomainMessenger + */ +interface ICrossDomainMessenger { + /********** + * Events * + **********/ + + event SentMessage(address indexed target, address sender, bytes message, uint256 messageNonce, uint256 gasLimit); + event RelayedMessage(bytes32 indexed msgHash); + event FailedRelayedMessage(bytes32 indexed msgHash); + + /************* + * Variables * + *************/ + + function xDomainMessageSender() external view returns (address); + + /******************** + * Public Functions * + ********************/ + + /** + * Sends a cross domain message to the target messenger. + * @param _target Target contract address. + * @param _message Message to send to the target. + * @param _gasLimit Gas limit for the provided message. + */ + function sendMessage( + address _target, + bytes calldata _message, + uint32 _gasLimit + ) external; +} diff --git a/contracts/vendor/optimism/LICENSE b/contracts/vendor/optimism/LICENSE new file mode 100644 index 00000000000..6a7da5218bb --- /dev/null +++ b/contracts/vendor/optimism/LICENSE @@ -0,0 +1,22 @@ +(The MIT License) + +Copyright 2020-2021 Optimism + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/contracts/vendor/polygon/IFxMessageProcessor.sol b/contracts/vendor/polygon/IFxMessageProcessor.sol new file mode 100644 index 00000000000..cf346bc6b43 --- /dev/null +++ b/contracts/vendor/polygon/IFxMessageProcessor.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IFxMessageProcessor { + function processMessageFromRoot( + uint256 stateId, + address rootMessageSender, + bytes calldata data + ) external; +} diff --git a/package.json b/package.json index 2d9d0d5150e..e2c0b973f8b 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "test": "hardhat test", "test:inheritance": "node scripts/inheritanceOrdering artifacts/build-info/*", "gas-report": "env ENABLE_GAS_REPORT=true npm run test", - "slither": "npm run clean && slither . --detect reentrancy-eth,reentrancy-no-eth,reentrancy-unlimited-gas" + "slither": "npm run clean && slither . --detect reentrancy-eth,reentrancy-no-eth,reentrancy-unlimited-gas --filter-paths contracts/mocks" }, "repository": { "type": "git", diff --git a/test/access/AccessControlCrossChain.test.js b/test/access/AccessControlCrossChain.test.js new file mode 100644 index 00000000000..cb4f3626dcb --- /dev/null +++ b/test/access/AccessControlCrossChain.test.js @@ -0,0 +1,59 @@ +const { expectRevert } = require('@openzeppelin/test-helpers'); +const { BridgeHelper } = require('../helpers/crosschain'); + +const { + shouldBehaveLikeAccessControl, +} = require('./AccessControl.behavior.js'); + +const crossChainRoleAlias = (role) => web3.utils.leftPad( + web3.utils.toHex(web3.utils.toBN(role).xor(web3.utils.toBN(web3.utils.soliditySha3('CROSSCHAIN_ALIAS')))), + 64, +); + +const AccessControlCrossChainMock = artifacts.require('AccessControlCrossChainMock'); + +const ROLE = web3.utils.soliditySha3('ROLE'); + +contract('AccessControl', function (accounts) { + before(async function () { + this.bridge = await BridgeHelper.deploy(); + }); + + beforeEach(async function () { + this.accessControl = await AccessControlCrossChainMock.new({ from: accounts[0] }); + }); + + shouldBehaveLikeAccessControl('AccessControl', ...accounts); + + describe('CrossChain enabled', function () { + beforeEach(async function () { + await this.accessControl.grantRole(ROLE, accounts[0], { from: accounts[0] }); + await this.accessControl.grantRole(crossChainRoleAlias(ROLE), accounts[1], { from: accounts[0] }); + }); + + it('check alliassing', async function () { + expect(await this.accessControl.crossChainRoleAlias(ROLE)).to.be.bignumber.equal(crossChainRoleAlias(ROLE)); + }); + + it('Crosschain calls not authorized to non-aliased addresses', async function () { + await expectRevert( + this.bridge.call( + accounts[0], + this.accessControl, + 'senderProtected', + [ ROLE ], + ), + `AccessControl: account ${accounts[0].toLowerCase()} is missing role ${crossChainRoleAlias(ROLE)}`, + ); + }); + + it('Crosschain calls not authorized to non-aliased addresses', async function () { + await this.bridge.call( + accounts[1], + this.accessControl, + 'senderProtected', + [ ROLE ], + ); + }); + }); +}); diff --git a/test/crosschain/CrossChainEnabled.test.js b/test/crosschain/CrossChainEnabled.test.js new file mode 100644 index 00000000000..7e64d29803c --- /dev/null +++ b/test/crosschain/CrossChainEnabled.test.js @@ -0,0 +1,83 @@ +const { BridgeHelper } = require('../helpers/crosschain'); +const { expectRevertCustomError } = require('../helpers/customError'); + +function randomAddress () { + return web3.utils.toChecksumAddress(web3.utils.randomHex(20)); +} + +const CrossChainEnabledAMBMock = artifacts.require('CrossChainEnabledAMBMock'); +const CrossChainEnabledArbitrumL1Mock = artifacts.require('CrossChainEnabledArbitrumL1Mock'); +const CrossChainEnabledArbitrumL2Mock = artifacts.require('CrossChainEnabledArbitrumL2Mock'); +const CrossChainEnabledOptimismMock = artifacts.require('CrossChainEnabledOptimismMock'); +const CrossChainEnabledPolygonChildMock = artifacts.require('CrossChainEnabledPolygonChildMock'); + +function shouldBehaveLikeReceiver (sender = randomAddress()) { + it('should reject same-chain calls', async function () { + await expectRevertCustomError( + this.receiver.crossChainRestricted(), + 'NotCrossChainCall()', + ); + }); + + it('should restrict to cross-chain call from a invalid sender', async function () { + await expectRevertCustomError( + this.bridge.call(sender, this.receiver, 'crossChainOwnerRestricted()'), + `InvalidCrossChainSender("${sender}", "${await this.receiver.owner()}")`, + ); + }); + + it('should grant access to cross-chain call from the owner', async function () { + await this.bridge.call( + await this.receiver.owner(), + this.receiver, + 'crossChainOwnerRestricted()', + ); + }); +} + +contract('CrossChainEnabled', function () { + describe('AMB', function () { + beforeEach(async function () { + this.bridge = await BridgeHelper.deploy('AMB'); + this.receiver = await CrossChainEnabledAMBMock.new(this.bridge.address); + }); + + shouldBehaveLikeReceiver(); + }); + + describe('Arbitrum-L1', function () { + beforeEach(async function () { + this.bridge = await BridgeHelper.deploy('Arbitrum-L1'); + this.receiver = await CrossChainEnabledArbitrumL1Mock.new(this.bridge.address); + }); + + shouldBehaveLikeReceiver(); + }); + + describe('Arbitrum-L2', function () { + beforeEach(async function () { + this.bridge = await BridgeHelper.deploy('Arbitrum-L2'); + this.receiver = await CrossChainEnabledArbitrumL2Mock.new(this.bridge.address); + }); + + shouldBehaveLikeReceiver(); + }); + + describe('Optimism', function () { + beforeEach(async function () { + this.bridge = await BridgeHelper.deploy('Optimism'); + this.receiver = await CrossChainEnabledOptimismMock.new(this.bridge.address); + }); + + shouldBehaveLikeReceiver(); + }); + + describe('Polygon-Child', function () { + beforeEach(async function () { + this.bridge = await BridgeHelper.deploy('Polygon-Child'); + this.receiver = await CrossChainEnabledPolygonChildMock.new(this.bridge.address); + }); + + shouldBehaveLikeReceiver(); + }); +}); diff --git a/test/helpers/crosschain.js b/test/helpers/crosschain.js new file mode 100644 index 00000000000..d4d25d1a15c --- /dev/null +++ b/test/helpers/crosschain.js @@ -0,0 +1,63 @@ +const { promisify } = require('util'); + +const BridgeAMBMock = artifacts.require('BridgeAMBMock'); +const BridgeArbitrumL1Mock = artifacts.require('BridgeArbitrumL1Mock'); +const BridgeArbitrumL2Mock = artifacts.require('BridgeArbitrumL2Mock'); +const BridgeOptimismMock = artifacts.require('BridgeOptimismMock'); +const BridgePolygonChildMock = artifacts.require('BridgePolygonChildMock'); + +class BridgeHelper { + static async deploy (type) { + return new BridgeHelper(await deployBridge(type)); + } + + constructor (bridge) { + this.bridge = bridge; + this.address = bridge.address; + } + + call (from, target, selector = undefined, args = []) { + return this.bridge.relayAs( + target.address || target, + selector + ? target.contract.methods[selector](...args).encodeABI() + : '0x', + from, + ); + } +} + +async function deployBridge (type = 'Arbitrum-L2') { + switch (type) { + case 'AMB': + return BridgeAMBMock.new(); + + case 'Arbitrum-L1': + return BridgeArbitrumL1Mock.new(); + + case 'Arbitrum-L2': { + const instance = await BridgeArbitrumL2Mock.new(); + const code = await web3.eth.getCode(instance.address); + await promisify(web3.currentProvider.send.bind(web3.currentProvider))({ + jsonrpc: '2.0', + method: 'hardhat_setCode', + params: [ '0x0000000000000000000000000000000000000064', code ], + id: new Date().getTime(), + }); + return BridgeArbitrumL2Mock.at('0x0000000000000000000000000000000000000064'); + } + + case 'Optimism': + return BridgeOptimismMock.new(); + + case 'Polygon-Child': + return BridgePolygonChildMock.new(); + + default: + throw new Error(`CrossChain: ${type} is not supported`); + } +} + +module.exports = { + BridgeHelper, +}; diff --git a/test/helpers/customError.js b/test/helpers/customError.js new file mode 100644 index 00000000000..8fea43426cd --- /dev/null +++ b/test/helpers/customError.js @@ -0,0 +1,24 @@ +const { config } = require('hardhat'); + +const optimizationsEnabled = config.solidity.compilers.some(c => c.settings.optimizer.enabled); + +/** Revert handler that supports custom errors. */ +async function expectRevertCustomError (promise, reason) { + try { + await promise; + expect.fail('Expected promise to throw but it didn\'t'); + } catch (revert) { + if (reason) { + if (optimizationsEnabled) { + // Optimizations currently mess with Hardhat's decoding of custom errors + expect(revert.message).to.include.oneOf([reason, 'unrecognized return data or custom error']); + } else { + expect(revert.message).to.include(reason); + } + } + } +}; + +module.exports = { + expectRevertCustomError, +}; diff --git a/test/utils/structs/DoubleEndedQueue.test.js b/test/utils/structs/DoubleEndedQueue.test.js index d1de09c33ea..545c82a72dd 100644 --- a/test/utils/structs/DoubleEndedQueue.test.js +++ b/test/utils/structs/DoubleEndedQueue.test.js @@ -1,5 +1,5 @@ const { expectEvent } = require('@openzeppelin/test-helpers'); -const { expect } = require('chai'); +const { expectRevertCustomError } = require('../../helpers/customError'); const Bytes32DequeMock = artifacts.require('Bytes32DequeMock'); @@ -10,18 +10,6 @@ async function getContent (deque) { return values; } -/** Revert handler that supports custom errors. */ -async function expectRevert (promise, reason) { - try { - await promise; - expect.fail('Expected promise to throw but it didn\'t'); - } catch (error) { - if (reason) { - expect(error.message).to.include(reason); - } - } -} - contract('DoubleEndedQueue', function (accounts) { const bytesA = '0xdeadbeef'.padEnd(66, '0'); const bytesB = '0x0123456789'.padEnd(66, '0'); @@ -39,10 +27,10 @@ contract('DoubleEndedQueue', function (accounts) { }); it('reverts on accesses', async function () { - await expectRevert(this.deque.popBack(), 'Empty()'); - await expectRevert(this.deque.popFront(), 'Empty()'); - await expectRevert(this.deque.back(), 'Empty()'); - await expectRevert(this.deque.front(), 'Empty()'); + await expectRevertCustomError(this.deque.popBack(), 'Empty()'); + await expectRevertCustomError(this.deque.popFront(), 'Empty()'); + await expectRevertCustomError(this.deque.back(), 'Empty()'); + await expectRevertCustomError(this.deque.front(), 'Empty()'); }); }); @@ -63,7 +51,7 @@ contract('DoubleEndedQueue', function (accounts) { }); it('out of bounds access', async function () { - await expectRevert(this.deque.at(this.content.length), 'OutOfBounds()'); + await expectRevertCustomError(this.deque.at(this.content.length), 'OutOfBounds()'); }); describe('push', function () {