forked from OpenZeppelin/openzeppelin-contracts
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a cross-chain guide for the documentation (OpenZeppelin#3325)
- Loading branch information
Showing
3 changed files
with
221 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,4 +16,6 @@ | |
* xref:governance.adoc[Governance] | ||
* xref:crosschain.adoc[Crosschain] | ||
* xref:utilities.adoc[Utilities] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,210 @@ | ||
= Adding cross-chain support to contracts | ||
|
||
If your contract is targetting to be used in the context of multichain operations, you may need specific tools to identify and process these cross-chain operations. | ||
|
||
OpenZeppelin provides the xref:api:crosschain.adoc#CrossChainEnabled[`CrossChainEnabled`] abstract contract, that includes dedicated internal functions. | ||
|
||
In this guide, we will go through an example use case: _how to build an upgradeable & mintable ERC20 token controlled by a governor present on a foreign chain_. | ||
|
||
== Starting point, our ERC20 contract | ||
|
||
Let's start with a small ERC20 contract, that we bootstrapped using the https://wizard.openzeppelin.com/[OpenZeppelin Contracts Wizard], and extended with an owner that has the ability to mint. Note that for demonstration purposes we have not used the built-in `Ownable` contract. | ||
|
||
[source,solidity] | ||
---- | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.4; | ||
import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; | ||
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; | ||
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; | ||
contract MyToken is Initializable, ERC20Upgradeable, UUPSUpgradeable { | ||
address public owner; | ||
modifier onlyOwner() { | ||
require(owner == _msgSender(), "Not authorized"); | ||
_; | ||
} | ||
/// @custom:oz-upgrades-unsafe-allow constructor | ||
constructor() initializer {} | ||
function initialize(address initialOwner) initializer public { | ||
__ERC20_init("MyToken", "MTK"); | ||
__UUPSUpgradeable_init(); | ||
owner = initialOwner; | ||
} | ||
function mint(address to, uint256 amount) public onlyOwner { | ||
_mint(to, amount); | ||
} | ||
function _authorizeUpgrade(address newImplementation) internal override onlyOwner { | ||
} | ||
} | ||
---- | ||
|
||
This token is mintable and upgradeable by the owner of the contract. | ||
|
||
== Preparing our contract for cross-chain operations. | ||
|
||
Let's now imagine that this contract is going to live on one chain, but we want the minting and the upgrading to be performed by a xref:governance.adoc[`governor`] contract on another chain. | ||
|
||
For example, we could have our token on xDai, with our governor on mainnet, or we could have our token on mainnet, with our governor on optimism | ||
|
||
In order to do that, we will start by adding xref:api:crosschain.adoc#CrossChainEnabled[`CrossChainEnabled`] to our contract. You will notice that the contract is now abstract. This is because `CrossChainEnabled` is an abstract contract: it is not tied to any particular chain and it deals with cross-chain interactions in an abstract way. This is what enables us to easily reuse the code for different chains. We will specialize it later by inheriting from a chain-specific implementation of the abstraction. | ||
|
||
```diff | ||
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; | ||
+import "@openzeppelin/contracts-upgradeable/crosschain/CrossChainEnabled.sol"; | ||
|
||
-contract MyToken is Initializable, ERC20Upgradeable, UUPSUpgradeable { | ||
+abstract contract MyTokenCrossChain is Initializable, ERC20Upgradeable, UUPSUpgradeable, CrossChainEnabled { | ||
``` | ||
|
||
Once that is done, we can use the `onlyCrossChainSender` modifier, provided by `CrossChainEnabled` in order to protect the minting and upgrading operations. | ||
|
||
```diff | ||
- function mint(address to, uint256 amount) public onlyOwner { | ||
+ function mint(address to, uint256 amount) public onlyCrossChainSender(owner) { | ||
|
||
- function _authorizeUpgrade(address newImplementation) internal override onlyOwner { | ||
+ function _authorizeUpgrade(address newImplementation) internal override onlyCrossChainSender(owner) { | ||
``` | ||
|
||
This change will effectively restrict the mint and upgrade operations to the `owner` on the remote chain. | ||
|
||
== Specializing for a specific chain | ||
|
||
Once the abstract cross-chain version of our token is ready we can easily specialize it for the chain we want, or more precisely for the bridge system that we want to rely on. | ||
|
||
This is done using one of the many `CrossChainEnabled` implementations. | ||
|
||
For example, if our token on xDai, and our governor on mainnet, we can use the https://docs.tokenbridge.net/amb-bridge/about-amb-bridge[AMB] bridge available on xDai at https://blockscout.com/xdai/mainnet/address/0x75Df5AF045d91108662D8080fD1FEFAd6aA0bb59[0x75Df5AF045d91108662D8080fD1FEFAd6aA0bb59] | ||
|
||
[source,solidity] | ||
---- | ||
[...] | ||
import "@openzeppelin/contracts-upgradeable/crosschain/amb/CrossChainEnabledAMB.sol"; | ||
contract MyTokenXDAI is | ||
MyTokenCrossChain, | ||
CrossChainEnabledAMB(0x75Df5AF045d91108662D8080fD1FEFAd6aA0bb59) | ||
{} | ||
---- | ||
|
||
If the token is on Ethereum mainnet, and our governor on Optimism, we use the Optimism https://community.optimism.io/docs/protocol/protocol-2.0/#l1crossdomainmessenger[CrossDomainMessenger] available on mainnet at https://etherscan.io/address/0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1[0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1]. | ||
|
||
[source,solidity] | ||
---- | ||
[...] | ||
import "@openzeppelin/contracts-upgradeable/crosschain/optimismCrossChainEnabledOptimism.sol"; | ||
contract MyTokenOptimism is | ||
MyTokenCrossChain, | ||
CrossChainEnabledOptimism(0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1) | ||
{} | ||
---- | ||
|
||
== Mixing cross domain addresses is dangerous | ||
|
||
When designing a contract with cross-chain support, it is essential to understand possible fallbacks and the security assumption that are being made. | ||
|
||
In this guide, we are particularly focusing on restricting access to a specific caller. This is usually done (as shown above) using `msg.sender` or `_msgSender()`. However, when going cross-chain, it is not just that simple. Even without considering possible bridge issues, it is important to keep in mind that the same address can correspond to very different entities when considering a multi-chain space. EOA wallets can only execute operations if the wallet's private-key signs the transaction. To our knowledge this is the case in all EVM chains, so a cross-chain message coming from such a wallet is arguably equivalent to a non-cross-chain message by the same wallet. The situation is however very different for smart contracts. | ||
|
||
Due to the way smart contract addresses are computed, and the fact that smart contracts on different chains live independent lives, you could have two very different contracts live at the same address on different chains. You could imagine two multisig wallets with different signers use the same address on different chains. You could also see a very basic smart wallet live on one chain at the same address as a full-fledge governor on another chain. Therefore, you should be careful that whenever you give permissions to a specific address, you control with chain this address can act from. | ||
|
||
== Going further with access control | ||
|
||
In previous example, we have both a `onlyOwner()` modifier and the `onlyCrossChainSender(owner)` mechanism. We didn't use the xref:access-control.adoc#ownership-and-ownable[`Ownable`] pattern because the ownership transfer mechanism in includes is not designed to work with the owner being a cross-chain entity. Unlike xref:access-control.adoc#ownership-and-ownable[`Ownable`], xref:access-control.adoc#role-based-access-control[`AccessControl`] is more effective at capturing the nuances and can effectivelly be used to build cross-chain-aware contracts. | ||
|
||
Using xref:api:access.adoc#AccessControlCrossChain[`AccessControlCrossChain`] includes both the xref:api:access.adoc#AccessControl[`AccessControl`] core and the xref:api:crosschain.adoc#CrossChainEnabled[`CrossChainEnabled`] abstraction. It also includes some binding to make role management compatible with cross-chain operations. | ||
|
||
In the case of the `mint` function, the caller must have the `MINTER_ROLE` when the call originates from the same chain. If the caller is on a remote chain, then the caller should not have the `MINTER_ROLE`, but the "aliased" version (`MINTER_ROLE ^ CROSSCHAIN_ALIAS`). This mitigates the danger described in the previous section by strictly separating local accounts from remote accounts from a different chain. See the xref:api:access.adoc#AccessControlCrossChain[`AccessControlCrossChain`] documentation for more details. | ||
|
||
|
||
```diff | ||
import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; | ||
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; | ||
+import "@openzeppelin/contracts-upgradeable/access/AccessControlCrossChainUpgradeable.sol"; | ||
|
||
-abstract contract MyTokenCrossChain is Initializable, ERC20Upgradeable, UUPSUpgradeable, CrossChainEnabled { | ||
+abstract contract MyTokenCrossChain is Initializable, ERC20Upgradeable, UUPSUpgradeable, AccessControlCrossChainUpgradeable { | ||
|
||
- address public owner; | ||
- modifier onlyOwner() { | ||
- require(owner == _msgSender(), "Not authorized"); | ||
- _; | ||
- } | ||
|
||
+ bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); | ||
+ bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE"); | ||
|
||
function initialize(address initialOwner) initializer public { | ||
__ERC20_init("MyToken", "MTK"); | ||
__UUPSUpgradeable_init(); | ||
+ __AccessControl_init(); | ||
+ _grantRole(_crossChainRoleAlias(DEFAULT_ADMIN_ROLE), initialOwner); // initialOwner is on a remote chain | ||
- owner = initialOwner; | ||
} | ||
|
||
- function mint(address to, uint256 amount) public onlyCrossChainSender(owner) { | ||
+ function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) { | ||
|
||
- function _authorizeUpgrade(address newImplementation) internal override onlyCrossChainSender(owner) { | ||
+ function _authorizeUpgrade(address newImplementation) internal override onlyRole(UPGRADER_ROLE) { | ||
``` | ||
|
||
This results in the following, final, code: | ||
|
||
[source,solidity] | ||
---- | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.4; | ||
import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; | ||
import "@openzeppelin/contracts-upgradeable/access/AccessControlCrossChainUpgradeable.sol"; | ||
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; | ||
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; | ||
abstract contract MyTokenCrossChain is Initializable, ERC20Upgradeable, AccessControlCrossChainUpgradeable, UUPSUpgradeable { | ||
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); | ||
bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE"); | ||
/// @custom:oz-upgrades-unsafe-allow constructor | ||
constructor() initializer {} | ||
function initialize(address initialOwner) initializer public { | ||
__ERC20_init("MyToken", "MTK"); | ||
__AccessControl_init(); | ||
__UUPSUpgradeable_init(); | ||
_grantRole(_crossChainRoleAlias(DEFAULT_ADMIN_ROLE), initialOwner); // initialOwner is on a remote chain | ||
} | ||
function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) { | ||
_mint(to, amount); | ||
} | ||
function _authorizeUpgrade(address newImplementation) internal onlyRole(UPGRADER_ROLE) override { | ||
} | ||
} | ||
import "@openzeppelin/contracts-upgradeable/crosschain/amb/CrossChainEnabledAMB.sol"; | ||
contract MyTokenXDAI is | ||
MyTokenCrossChain, | ||
CrossChainEnabledAMB(0x75Df5AF045d91108662D8080fD1FEFAd6aA0bb59) | ||
{} | ||
import "@openzeppelin/contracts-upgradeable/crosschain/optimismCrossChainEnabledOptimism.sol"; | ||
contract MyTokenOptimism is | ||
MyTokenCrossChain, | ||
CrossChainEnabledOptimism(0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1) | ||
{} | ||
---- |