julien/lovepublic Fork 0
9056150
Commits
Clone
git clone https://git.rickub.com/julien/love.git
git clone ssh://git@rickub.com/julien/love.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

deploy with create2

julienbrg committed 2026-09-17T21:07:44+02:00 Browse files
9056150 parent: 69c46a2
modified script/Love.s.sol +50 -3
@@ -4,18 +4,65 @@ pragma solidity ^0.8.30;
44 import {Love} from "../src/Love.sol";
55 import {Script, console} from "forge-std/Script.sol";
66
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.
712 contract LoveScript is Script {
13+ bytes32 public constant DEFAULT_SALT = keccak256("LOVE");
14+
815 Love public love;
916
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+
1132 vm.startBroadcast();
33+ love = new Love{salt: s}();
34+ vm.stopBroadcast();
35+
36+ require(address(love) == predicted, "LoveScript: address mismatch");
1237
13- love = new Love();
1438 console.log("Love deployed at", address(love));
1539 console.log("name ", love.name());
1640 console.log("symbol ", love.symbol());
1741 console.log("totalSupply ", love.totalSupply());
1842
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);
2067 }
2168 }
@@ -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+}