diff --git a/.solcover.js b/.solcover.js index 9e7285e1fb4..ca9a114a9cf 100644 --- a/.solcover.js +++ b/.solcover.js @@ -3,7 +3,6 @@ module.exports = { testCommand: 'npm test', compileCommand: 'npm run compile', skipFiles: [ - 'lifecycle/Migrations.sol', - 'mocks' + 'mocks', ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index d8bece08f55..373e93fe5a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### New features * `SafeCast.toUintXX`: new library for integer downcasting, which allows for safe operation on smaller types (e.g. `uint32`) when combined with `SafeMath`. ([#1926](https://github.com/OpenZeppelin/openzeppelin-solidity/pull/1926)) * `ERC721Metadata`: added `baseURI`, which can be used for dramatic gas savings when all token URIs share a prefix (e.g. `http://api.myapp.com/tokens/`). ([#1970](https://github.com/OpenZeppelin/openzeppelin-solidity/pull/1970)) + * `Create2`: simple library to make usage of the `CREATE2` opcode easier. ([#1744](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/1744)) ### Improvements * `ERC777`: `_burn` is now internal, providing more flexibility and making it easier to create tokens that deflate. ([#1908](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/1908)) diff --git a/contracts/mocks/Create2Impl.sol b/contracts/mocks/Create2Impl.sol new file mode 100644 index 00000000000..de3a14f238b --- /dev/null +++ b/contracts/mocks/Create2Impl.sol @@ -0,0 +1,23 @@ +pragma solidity ^0.5.0; + +import "../utils/Create2.sol"; +import "../token/ERC20/ERC20.sol"; + +contract Create2Impl { + function deploy(bytes32 salt, bytes memory code) public { + Create2.deploy(salt, code); + } + + function deployERC20(bytes32 salt) public { + // solhint-disable-next-line indent + Create2.deploy(salt, type(ERC20).creationCode); + } + + function computeAddress(bytes32 salt, bytes memory code) public view returns (address) { + return Create2.computeAddress(salt, code); + } + + function computeAddress(bytes32 salt, bytes memory code, address deployer) public pure returns (address) { + return Create2.computeAddress(salt, code, deployer); + } +} diff --git a/contracts/utils/Create2.sol b/contracts/utils/Create2.sol new file mode 100644 index 00000000000..6cc0c9b29bb --- /dev/null +++ b/contracts/utils/Create2.sol @@ -0,0 +1,47 @@ +pragma solidity ^0.5.0; + +/** + * @dev Helper to make usage of the `CREATE2` EVM opcode easier and safer. + * `CREATE2` can be used to compute in advance the address where a smart + * contract will be deployed, which allows for interesting new mechanisms known + * as 'counterfactual interactions'. + * + * See the https://eips.ethereum.org/EIPS/eip-1014#motivation[EIP] for more + * information. + */ +library Create2 { + /** + * @dev Deploys a contract using `CREATE2`. The address where the contract + * will be deployed can be known in advance via {computeAddress}. Note that + * a contract cannot be deployed twice using the same salt. + */ + function deploy(bytes32 salt, bytes memory bytecode) internal returns (address) { + address addr; + // solhint-disable-next-line no-inline-assembly + assembly { + addr := create2(0, add(bytecode, 0x20), mload(bytecode), salt) + } + require(addr != address(0), "Create2: Failed on deploy"); + return addr; + } + + /** + * @dev Returns the address where a contract will be stored if deployed via {deploy}. Any change in the `bytecode` + * or `salt` will result in a new destination address. + */ + function computeAddress(bytes32 salt, bytes memory bytecode) internal view returns (address) { + return computeAddress(salt, bytecode, address(this)); + } + + /** + * @dev Returns the address where a contract will be stored if deployed via {deploy} from a contract located at + * `deployer`. If `deployer` is this contract's address, returns the same value as {computeAddress}. + */ + function computeAddress(bytes32 salt, bytes memory bytecodeHash, address deployer) internal pure returns (address) { + bytes32 bytecodeHashHash = keccak256(bytecodeHash); + bytes32 _data = keccak256( + abi.encodePacked(bytes1(0xff), deployer, salt, bytecodeHashHash) + ); + return address(bytes20(_data << 96)); + } +} diff --git a/contracts/utils/README.adoc b/contracts/utils/README.adoc index 22908021134..52d2c413a67 100644 --- a/contracts/utils/README.adoc +++ b/contracts/utils/README.adoc @@ -10,4 +10,6 @@ Miscellaneous contracts containing utility functions, often related to working w {{Arrays}} +{{Create2}} + {{ReentrancyGuard}} diff --git a/test/utils/Create2.test.js b/test/utils/Create2.test.js new file mode 100644 index 00000000000..5df38557309 --- /dev/null +++ b/test/utils/Create2.test.js @@ -0,0 +1,71 @@ +const { contract, accounts, web3 } = require('@openzeppelin/test-environment'); +const { BN, expectRevert } = require('@openzeppelin/test-helpers'); + +const { expect } = require('chai'); + +const Create2Impl = contract.fromArtifact('Create2Impl'); +const ERC20Mock = contract.fromArtifact('ERC20Mock'); +const ERC20 = contract.fromArtifact('ERC20'); + +describe('Create2', function () { + const [deployerAccount] = accounts; + + const salt = 'salt message'; + const saltHex = web3.utils.soliditySha3(salt); + const constructorByteCode = `${ERC20Mock.bytecode}${web3.eth.abi + .encodeParameters(['address', 'uint256'], [deployerAccount, 100]).slice(2) + }`; + + beforeEach(async function () { + this.factory = await Create2Impl.new(); + }); + + it('should compute the correct contract address', async function () { + const onChainComputed = await this.factory + .computeAddress(saltHex, constructorByteCode); + const offChainComputed = + computeCreate2Address(saltHex, constructorByteCode, this.factory.address); + expect(onChainComputed).to.equal(offChainComputed); + }); + + it('should compute the correct contract address with deployer', async function () { + const onChainComputed = await this.factory + .computeAddress(saltHex, constructorByteCode, deployerAccount); + const offChainComputed = + computeCreate2Address(saltHex, constructorByteCode, deployerAccount); + expect(onChainComputed).to.equal(offChainComputed); + }); + + it('should deploy a ERC20 from inline assembly code', async function () { + const offChainComputed = + computeCreate2Address(saltHex, ERC20.bytecode, this.factory.address); + await this.factory + .deploy(saltHex, ERC20.bytecode, { from: deployerAccount }); + expect(ERC20.bytecode).to.include((await web3.eth.getCode(offChainComputed)).slice(2)); + }); + + it('should deploy a ERC20Mock with correct balances', async function () { + const offChainComputed = + computeCreate2Address(saltHex, constructorByteCode, this.factory.address); + await this.factory + .deploy(saltHex, constructorByteCode, { from: deployerAccount }); + const erc20 = await ERC20Mock.at(offChainComputed); + expect(await erc20.balanceOf(deployerAccount)).to.be.bignumber.equal(new BN(100)); + }); + + it('should failed deploying a contract in an existent address', async function () { + await this.factory.deploy(saltHex, constructorByteCode, { from: deployerAccount }); + await expectRevert( + this.factory.deploy(saltHex, constructorByteCode, { from: deployerAccount }), 'Create2: Failed on deploy' + ); + }); +}); + +function computeCreate2Address (saltHex, bytecode, deployer) { + return web3.utils.toChecksumAddress(`0x${web3.utils.sha3(`0x${[ + 'ff', + deployer, + saltHex, + web3.utils.soliditySha3(bytecode), + ].map(x => x.replace(/0x/, '')).join('')}`).slice(-40)}`); +}