julien/lovepublic Fork 0
dded29a
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.

Merge 1-add-main-logic into main

rickub committed 2026-09-17T19:14:12Z Browse files
dded29a parent: 69c46a2
modified CHANGELOG.md +5 -1
@@ -13,7 +13,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313 - `Love`, an ERC-20 token (`LOVE`, 18 decimals) on OpenZeppelin v5, with an
1414 unrestricted public `mint(address,uint256)` — anyone can mint any amount.
1515 - Metadata, mint, transfer, approve/`transferFrom`, revert and fuzz tests.
16-- Deployment script `script/Love.s.sol`.
16+- Deployment script `script/Love.s.sol`, deploying with CREATE2 through the
17+ canonical deterministic deployer so `Love` gets the same address on every EVM
18+ network; salt defaults to `keccak256("LOVE")` and is overridable via `SALT`.
19+- `predict()` entrypoint printing the deployment address without broadcasting,
20+ and CREATE2 tests pinning address determinism across chain IDs.
1721 - `foundry.toml` with pinned solc 0.8.30, optimizer, fmt rules, fuzz/invariant
1822 defaults, and Base Sepolia RPC/Etherscan endpoints driven by env vars.
1923 - `remappings.txt` for `@openzeppelin/contracts/` and `forge-std/`.
@@ -13,7 +13,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
13 - `Love`, an ERC-20 token (`LOVE`, 18 decimals) on OpenZeppelin v5, with an13 - `Love`, an ERC-20 token (`LOVE`, 18 decimals) on OpenZeppelin v5, with an
14 unrestricted public `mint(address,uint256)` — anyone can mint any amount.14 unrestricted public `mint(address,uint256)` — anyone can mint any amount.
15 - Metadata, mint, transfer, approve/`transferFrom`, revert and fuzz tests.15 - Metadata, mint, transfer, approve/`transferFrom`, revert and fuzz tests.
16-- Deployment script `script/Love.s.sol`.16+- Deployment script `script/Love.s.sol`, deploying with CREATE2 through the
17+ canonical deterministic deployer so `Love` gets the same address on every EVM
18+ network; salt defaults to `keccak256("LOVE")` and is overridable via `SALT`.
19+- `predict()` entrypoint printing the deployment address without broadcasting,
20+ and CREATE2 tests pinning address determinism across chain IDs.
17 - `foundry.toml` with pinned solc 0.8.30, optimizer, fmt rules, fuzz/invariant21 - `foundry.toml` with pinned solc 0.8.30, optimizer, fmt rules, fuzz/invariant
18 defaults, and Base Sepolia RPC/Etherscan endpoints driven by env vars.22 defaults, and Base Sepolia RPC/Etherscan endpoints driven by env vars.
19 - `remappings.txt` for `@openzeppelin/contracts/` and `forge-std/`.23 - `remappings.txt` for `@openzeppelin/contracts/` and `forge-std/`.
modified README.md +20 -29
@@ -1,41 +1,31 @@
1-# love
1+# Love
22
3-A [Foundry](https://getfoundry.sh) project.
3+The official LOVE token.
44
5-`Love` (`LOVE`, 18 decimals) is a basic ERC-20 built on
6-[OpenZeppelin](https://github.com/OpenZeppelin/openzeppelin-contracts), with one
7-deliberate twist: `mint` has **no access control**.
8-
9-```solidity
10-function mint(address to, uint256 amount) external;
11-```
12-
13-Anyone can mint any amount to any address. Supply starts at zero and is
14-unbounded, so the token carries no economic value — it is a faucet / demo token.
15-Do not deploy it to mainnet expecting otherwise.
16-
17-## Setup
5+## Install
186
197 ```shell
208 forge install
219 cp .env.example .env
2210 ```
2311
24-## Usage
12+## Build & test
2513
2614 ```shell
2715 forge build # compile
2816 forge test # run tests
2917 forge test -vvv # with traces
3018 forge fmt # format
31-forge coverage # coverage report
32-forge snapshot # gas snapshot
3319 anvil # local node
3420 ```
3521
3622 ## Deploy
3723
24+Deployment uses CREATE2, so the token has the same address on every EVM network.
25+
3826 ```shell
27+forge script script/Love.s.sol:LoveScript --sig 'predict()' # print the address
28+
3929 forge script script/Love.s.sol:LoveScript \
4030 --rpc-url base_sepolia \
4131 --account "$DEPLOYER_ACCOUNT" \
@@ -43,17 +33,6 @@ forge script script/Love.s.sol:LoveScript \
4333 --verify
4434 ```
4535
46-Drop `--broadcast` for a dry run.
47-
48-## Layout
49-
50-| Path | Contents |
51-| --------- | ------------------- |
52-| `src/` | Contracts |
53-| `test/` | Tests |
54-| `script/` | Deployment scripts |
55-| `lib/` | Dependencies |
56-
5736 ## Mint
5837
5938 ```shell
@@ -61,3 +40,15 @@ cast send <LOVE_ADDRESS> "mint(address,uint256)" <RECIPIENT> 1000000000000000000
6140 --rpc-url base_sepolia \
6241 --account "$DEPLOYER_ACCOUNT"
6342 ```
43+
44+## License
45+
46+GPL-3.0
47+
48+## Contact
49+
50+**Julien Béranger** ([GitHub](https://github.com/julienbrg))
51+
52+- Element: [@julienbrg:matrix.org](https://matrix.to/#/@julienbrg:matrix.org)
53+- Farcaster: [julien-](https://warpcast.com/julien-)
54+- Telegram: [@julienbrg](https://t.me/julienbrg)
@@ -1,41 +1,31 @@
1-# love1+# Love
2 2
3-A [Foundry](https://getfoundry.sh) project.3+The official LOVE token.
4 4
5-`Love` (`LOVE`, 18 decimals) is a basic ERC-20 built on5+## Install
6-[OpenZeppelin](https://github.com/OpenZeppelin/openzeppelin-contracts), with one
7-deliberate twist: `mint` has **no access control**.
8-
9-```solidity
10-function mint(address to, uint256 amount) external;
11-```
12-
13-Anyone can mint any amount to any address. Supply starts at zero and is
14-unbounded, so the token carries no economic value — it is a faucet / demo token.
15-Do not deploy it to mainnet expecting otherwise.
16-
17-## Setup
18 6
19 ```shell7 ```shell
20 forge install8 forge install
21 cp .env.example .env9 cp .env.example .env
22 ```10 ```
23 11
24-## Usage12+## Build & test
25 13
26 ```shell14 ```shell
27 forge build # compile15 forge build # compile
28 forge test # run tests16 forge test # run tests
29 forge test -vvv # with traces17 forge test -vvv # with traces
30 forge fmt # format18 forge fmt # format
31-forge coverage # coverage report
32-forge snapshot # gas snapshot
33 anvil # local node19 anvil # local node
34 ```20 ```
35 21
36 ## Deploy22 ## Deploy
37 23
24+Deployment uses CREATE2, so the token has the same address on every EVM network.
25+
38 ```shell26 ```shell
27+forge script script/Love.s.sol:LoveScript --sig 'predict()' # print the address
28+
39 forge script script/Love.s.sol:LoveScript \29 forge script script/Love.s.sol:LoveScript \
40 --rpc-url base_sepolia \30 --rpc-url base_sepolia \
41 --account "$DEPLOYER_ACCOUNT" \31 --account "$DEPLOYER_ACCOUNT" \
@@ -43,17 +33,6 @@ forge script script/Love.s.sol:LoveScript \
43 --verify33 --verify
44 ```34 ```
45 35
46-Drop `--broadcast` for a dry run.
47-
48-## Layout
49-
50-| Path | Contents |
51-| --------- | ------------------- |
52-| `src/` | Contracts |
53-| `test/` | Tests |
54-| `script/` | Deployment scripts |
55-| `lib/` | Dependencies |
56-
57 ## Mint36 ## Mint
58 37
59 ```shell38 ```shell
@@ -61,3 +40,15 @@ cast send <LOVE_ADDRESS> "mint(address,uint256)" <RECIPIENT> 1000000000000000000
61 --rpc-url base_sepolia \40 --rpc-url base_sepolia \
62 --account "$DEPLOYER_ACCOUNT"41 --account "$DEPLOYER_ACCOUNT"
63 ```42 ```
43+
44+## License
45+
46+GPL-3.0
47+
48+## Contact
49+
50+**Julien Béranger** ([GitHub](https://github.com/julienbrg))
51+
52+- Element: [@julienbrg:matrix.org](https://matrix.to/#/@julienbrg:matrix.org)
53+- Farcaster: [julien-](https://warpcast.com/julien-)
54+- Telegram: [@julienbrg](https://t.me/julienbrg)
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+}