deploy with create2
9056150 parent: 69c46a2 modified
script/Love.s.sol +50 -3 | @@ -4,18 +4,65 @@ pragma solidity ^0.8.30; | ||
| 4 | 4 | import {Love} from "../src/Love.sol"; |
| 5 | 5 | import {Script, console} from "forge-std/Script.sol"; |
| 6 | 6 | |
| 7 | +/// @notice Deploys `Love` with CREATE2 through the canonical deterministic | |
| 8 | +/// deployer, so the token gets the same address on every EVM network. | |
| 9 | +/// The address depends only on the salt and the creation code — keep | |
| 10 | +/// `solc`, the optimizer settings and `bytecode_hash = "none"` as they | |
| 11 | +/// are in `foundry.toml`, or the address changes. | |
| 7 | 12 | contract LoveScript is Script { |
| 13 | + bytes32 public constant DEFAULT_SALT = keccak256("LOVE"); | |
| 14 | + | |
| 8 | 15 | Love public love; |
| 9 | 16 | |
| 10 | - function run() public { | |
| 17 | + function run() public returns (Love) { | |
| 18 | + bytes32 s = salt(); | |
| 19 | + address predicted = predict(s); | |
| 20 | + | |
| 21 | + console.log("deployer ", CREATE2_FACTORY); | |
| 22 | + console.log("salt ", vm.toString(s)); | |
| 23 | + console.log("initCodeHash ", vm.toString(initCodeHash())); | |
| 24 | + console.log("predicted ", predicted); | |
| 25 | + | |
| 26 | + if (predicted.code.length > 0) { | |
| 27 | + console.log("already deployed, nothing to do"); | |
| 28 | + love = Love(predicted); | |
| 29 | + return love; | |
| 30 | + } | |
| 31 | + | |
| 11 | 32 | vm.startBroadcast(); |
| 33 | + love = new Love{salt: s}(); | |
| 34 | + vm.stopBroadcast(); | |
| 35 | + | |
| 36 | + require(address(love) == predicted, "LoveScript: address mismatch"); | |
| 12 | 37 | |
| 13 | - love = new Love(); | |
| 14 | 38 | console.log("Love deployed at", address(love)); |
| 15 | 39 | console.log("name ", love.name()); |
| 16 | 40 | console.log("symbol ", love.symbol()); |
| 17 | 41 | console.log("totalSupply ", love.totalSupply()); |
| 18 | 42 | |
| 19 | - vm.stopBroadcast(); | |
| 43 | + return love; | |
| 44 | + } | |
| 45 | + | |
| 46 | + /// @notice Print the address `run()` would deploy to, without broadcasting. | |
| 47 | + function predict() public view returns (address predicted) { | |
| 48 | + bytes32 s = salt(); | |
| 49 | + predicted = predict(s); | |
| 50 | + | |
| 51 | + console.log("salt ", vm.toString(s)); | |
| 52 | + console.log("initCodeHash ", vm.toString(initCodeHash())); | |
| 53 | + console.log("predicted ", predicted); | |
| 54 | + } | |
| 55 | + | |
| 56 | + function predict(bytes32 s) public pure returns (address) { | |
| 57 | + return vm.computeCreate2Address(s, initCodeHash(), CREATE2_FACTORY); | |
| 58 | + } | |
| 59 | + | |
| 60 | + function initCodeHash() public pure returns (bytes32) { | |
| 61 | + return keccak256(type(Love).creationCode); | |
| 62 | + } | |
| 63 | + | |
| 64 | + /// @dev `SALT` overrides the default, e.g. to mine a vanity address. | |
| 65 | + function salt() public view returns (bytes32) { | |
| 66 | + return vm.envOr("SALT", DEFAULT_SALT); | |
| 20 | 67 | } |
| 21 | 68 | } |
| @@ -4,18 +4,65 @@ pragma solidity ^0.8.30; | |||
| 4 | import {Love} from "../src/Love.sol"; | 4 | import {Love} from "../src/Love.sol"; |
| 5 | import {Script, console} from "forge-std/Script.sol"; | 5 | import {Script, console} from "forge-std/Script.sol"; |
| 6 | 6 | ||
| 7 | +/// @notice Deploys `Love` with CREATE2 through the canonical deterministic | ||
| 8 | +/// deployer, so the token gets the same address on every EVM network. | ||
| 9 | +/// The address depends only on the salt and the creation code — keep | ||
| 10 | +/// `solc`, the optimizer settings and `bytecode_hash = "none"` as they | ||
| 11 | +/// are in `foundry.toml`, or the address changes. | ||
| 7 | contract LoveScript is Script { | 12 | contract LoveScript is Script { |
| 13 | + bytes32 public constant DEFAULT_SALT = keccak256("LOVE"); | ||
| 14 | + | ||
| 8 | Love public love; | 15 | Love public love; |
| 9 | 16 | ||
| 10 | - function run() public { | 17 | + function run() public returns (Love) { |
| 18 | + bytes32 s = salt(); | ||
| 19 | + address predicted = predict(s); | ||
| 20 | + | ||
| 21 | + console.log("deployer ", CREATE2_FACTORY); | ||
| 22 | + console.log("salt ", vm.toString(s)); | ||
| 23 | + console.log("initCodeHash ", vm.toString(initCodeHash())); | ||
| 24 | + console.log("predicted ", predicted); | ||
| 25 | + | ||
| 26 | + if (predicted.code.length > 0) { | ||
| 27 | + console.log("already deployed, nothing to do"); | ||
| 28 | + love = Love(predicted); | ||
| 29 | + return love; | ||
| 30 | + } | ||
| 31 | + | ||
| 11 | vm.startBroadcast(); | 32 | vm.startBroadcast(); |
| 33 | + love = new Love{salt: s}(); | ||
| 34 | + vm.stopBroadcast(); | ||
| 35 | + | ||
| 36 | + require(address(love) == predicted, "LoveScript: address mismatch"); | ||
| 12 | 37 | ||
| 13 | - love = new Love(); | ||
| 14 | console.log("Love deployed at", address(love)); | 38 | console.log("Love deployed at", address(love)); |
| 15 | console.log("name ", love.name()); | 39 | console.log("name ", love.name()); |
| 16 | console.log("symbol ", love.symbol()); | 40 | console.log("symbol ", love.symbol()); |
| 17 | console.log("totalSupply ", love.totalSupply()); | 41 | console.log("totalSupply ", love.totalSupply()); |
| 18 | 42 | ||
| 19 | - vm.stopBroadcast(); | 43 | + return love; |
| 44 | + } | ||
| 45 | + | ||
| 46 | + /// @notice Print the address `run()` would deploy to, without broadcasting. | ||
| 47 | + function predict() public view returns (address predicted) { | ||
| 48 | + bytes32 s = salt(); | ||
| 49 | + predicted = predict(s); | ||
| 50 | + | ||
| 51 | + console.log("salt ", vm.toString(s)); | ||
| 52 | + console.log("initCodeHash ", vm.toString(initCodeHash())); | ||
| 53 | + console.log("predicted ", predicted); | ||
| 54 | + } | ||
| 55 | + | ||
| 56 | + function predict(bytes32 s) public pure returns (address) { | ||
| 57 | + return vm.computeCreate2Address(s, initCodeHash(), CREATE2_FACTORY); | ||
| 58 | + } | ||
| 59 | + | ||
| 60 | + function initCodeHash() public pure returns (bytes32) { | ||
| 61 | + return keccak256(type(Love).creationCode); | ||
| 62 | + } | ||
| 63 | + | ||
| 64 | + /// @dev `SALT` overrides the default, e.g. to mine a vanity address. | ||
| 65 | + function salt() public view returns (bytes32) { | ||
| 66 | + return vm.envOr("SALT", DEFAULT_SALT); | ||
| 20 | } | 67 | } |
| 21 | } | 68 | } |
added
test/LoveCreate2.t.sol +76 -0 | new file mode 100644 | ||
| @@ -0,0 +1,76 @@ | ||
| 1 | +// SPDX-License-Identifier: MIT | |
| 2 | +pragma solidity ^0.8.30; | |
| 3 | + | |
| 4 | +import {LoveScript} from "../script/Love.s.sol"; | |
| 5 | +import {Love} from "../src/Love.sol"; | |
| 6 | +import {Test} from "forge-std/Test.sol"; | |
| 7 | + | |
| 8 | +/// @notice The token is meant to live at the same address on every EVM network, | |
| 9 | +/// so these tests pin the CREATE2 deployment path and the inputs the | |
| 10 | +/// address derives from. | |
| 11 | +contract LoveCreate2Test is Test { | |
| 12 | + bytes32 constant SALT = keccak256("LOVE"); | |
| 13 | + | |
| 14 | + LoveScript script; | |
| 15 | + | |
| 16 | + function setUp() public { | |
| 17 | + script = new LoveScript(); | |
| 18 | + | |
| 19 | + // The canonical deterministic deployer, as it exists on live networks. | |
| 20 | + assertGt(CREATE2_FACTORY.code.length, 0, "create2 deployer missing"); | |
| 21 | + } | |
| 22 | + | |
| 23 | + function test_DeploysAtPredictedAddress() public { | |
| 24 | + address predicted = vm.computeCreate2Address(SALT, keccak256(type(Love).creationCode), CREATE2_FACTORY); | |
| 25 | + | |
| 26 | + Love deployed = Love(_deploy(SALT)); | |
| 27 | + | |
| 28 | + assertEq(address(deployed), predicted); | |
| 29 | + assertEq(deployed.symbol(), "LOVE"); | |
| 30 | + } | |
| 31 | + | |
| 32 | + /// @dev What the deploy script prints must be what the chain gives back. | |
| 33 | + function test_ScriptPredictionMatchesDeployment() public { | |
| 34 | + assertEq(script.predict(SALT), _deploy(SALT)); | |
| 35 | + } | |
| 36 | + | |
| 37 | + function test_ScriptUsesKeccakOfLoveAsDefaultSalt() public view { | |
| 38 | + assertEq(script.DEFAULT_SALT(), SALT); | |
| 39 | + } | |
| 40 | + | |
| 41 | + /// @dev Same salt, same code, same address — that is the whole point. | |
| 42 | + function test_PredictionIsChainAgnostic() public { | |
| 43 | + address onThisChain = script.predict(SALT); | |
| 44 | + | |
| 45 | + vm.chainId(137); | |
| 46 | + assertEq(script.predict(SALT), onThisChain); | |
| 47 | + | |
| 48 | + vm.chainId(42_161); | |
| 49 | + assertEq(script.predict(SALT), onThisChain); | |
| 50 | + } | |
| 51 | + | |
| 52 | + function test_DifferentSaltsGiveDifferentAddresses() public view { | |
| 53 | + assertTrue(script.predict(SALT) != script.predict(keccak256("LOVE2"))); | |
| 54 | + } | |
| 55 | + | |
| 56 | + /// @dev Redeploying with the same salt must fail, not silently return the | |
| 57 | + /// existing token. | |
| 58 | + function test_RevertWhen_RedeployingWithSameSalt() public { | |
| 59 | + _deploy(SALT); | |
| 60 | + | |
| 61 | + (bool ok,) = CREATE2_FACTORY.call(abi.encodePacked(SALT, type(Love).creationCode)); | |
| 62 | + assertFalse(ok); | |
| 63 | + } | |
| 64 | + | |
| 65 | + function testFuzz_PredictionMatchesDeployment(bytes32 salt) public { | |
| 66 | + assertEq(script.predict(salt), _deploy(salt)); | |
| 67 | + } | |
| 68 | + | |
| 69 | + function _deploy(bytes32 salt) internal returns (address deployed) { | |
| 70 | + (bool ok, bytes memory ret) = CREATE2_FACTORY.call(abi.encodePacked(salt, type(Love).creationCode)); | |
| 71 | + require(ok, "create2 deployment failed"); | |
| 72 | + // casting to 'bytes20' is safe because the deployer returns the 20-byte address, raw | |
| 73 | + // forge-lint: disable-next-line(unsafe-typecast) | |
| 74 | + deployed = address(bytes20(ret)); | |
| 75 | + } | |
| 76 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,76 @@ | |||
| 1 | +// SPDX-License-Identifier: MIT | ||
| 2 | +pragma solidity ^0.8.30; | ||
| 3 | + | ||
| 4 | +import {LoveScript} from "../script/Love.s.sol"; | ||
| 5 | +import {Love} from "../src/Love.sol"; | ||
| 6 | +import {Test} from "forge-std/Test.sol"; | ||
| 7 | + | ||
| 8 | +/// @notice The token is meant to live at the same address on every EVM network, | ||
| 9 | +/// so these tests pin the CREATE2 deployment path and the inputs the | ||
| 10 | +/// address derives from. | ||
| 11 | +contract LoveCreate2Test is Test { | ||
| 12 | + bytes32 constant SALT = keccak256("LOVE"); | ||
| 13 | + | ||
| 14 | + LoveScript script; | ||
| 15 | + | ||
| 16 | + function setUp() public { | ||
| 17 | + script = new LoveScript(); | ||
| 18 | + | ||
| 19 | + // The canonical deterministic deployer, as it exists on live networks. | ||
| 20 | + assertGt(CREATE2_FACTORY.code.length, 0, "create2 deployer missing"); | ||
| 21 | + } | ||
| 22 | + | ||
| 23 | + function test_DeploysAtPredictedAddress() public { | ||
| 24 | + address predicted = vm.computeCreate2Address(SALT, keccak256(type(Love).creationCode), CREATE2_FACTORY); | ||
| 25 | + | ||
| 26 | + Love deployed = Love(_deploy(SALT)); | ||
| 27 | + | ||
| 28 | + assertEq(address(deployed), predicted); | ||
| 29 | + assertEq(deployed.symbol(), "LOVE"); | ||
| 30 | + } | ||
| 31 | + | ||
| 32 | + /// @dev What the deploy script prints must be what the chain gives back. | ||
| 33 | + function test_ScriptPredictionMatchesDeployment() public { | ||
| 34 | + assertEq(script.predict(SALT), _deploy(SALT)); | ||
| 35 | + } | ||
| 36 | + | ||
| 37 | + function test_ScriptUsesKeccakOfLoveAsDefaultSalt() public view { | ||
| 38 | + assertEq(script.DEFAULT_SALT(), SALT); | ||
| 39 | + } | ||
| 40 | + | ||
| 41 | + /// @dev Same salt, same code, same address — that is the whole point. | ||
| 42 | + function test_PredictionIsChainAgnostic() public { | ||
| 43 | + address onThisChain = script.predict(SALT); | ||
| 44 | + | ||
| 45 | + vm.chainId(137); | ||
| 46 | + assertEq(script.predict(SALT), onThisChain); | ||
| 47 | + | ||
| 48 | + vm.chainId(42_161); | ||
| 49 | + assertEq(script.predict(SALT), onThisChain); | ||
| 50 | + } | ||
| 51 | + | ||
| 52 | + function test_DifferentSaltsGiveDifferentAddresses() public view { | ||
| 53 | + assertTrue(script.predict(SALT) != script.predict(keccak256("LOVE2"))); | ||
| 54 | + } | ||
| 55 | + | ||
| 56 | + /// @dev Redeploying with the same salt must fail, not silently return the | ||
| 57 | + /// existing token. | ||
| 58 | + function test_RevertWhen_RedeployingWithSameSalt() public { | ||
| 59 | + _deploy(SALT); | ||
| 60 | + | ||
| 61 | + (bool ok,) = CREATE2_FACTORY.call(abi.encodePacked(SALT, type(Love).creationCode)); | ||
| 62 | + assertFalse(ok); | ||
| 63 | + } | ||
| 64 | + | ||
| 65 | + function testFuzz_PredictionMatchesDeployment(bytes32 salt) public { | ||
| 66 | + assertEq(script.predict(salt), _deploy(salt)); | ||
| 67 | + } | ||
| 68 | + | ||
| 69 | + function _deploy(bytes32 salt) internal returns (address deployed) { | ||
| 70 | + (bool ok, bytes memory ret) = CREATE2_FACTORY.call(abi.encodePacked(salt, type(Love).creationCode)); | ||
| 71 | + require(ok, "create2 deployment failed"); | ||
| 72 | + // casting to 'bytes20' is safe because the deployer returns the 20-byte address, raw | ||
| 73 | + // forge-lint: disable-next-line(unsafe-typecast) | ||
| 74 | + deployed = address(bytes20(ret)); | ||
| 75 | + } | ||
| 76 | +} | ||