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

replace mint with weth-pegged deposit and withdraw

julienbrg committed 2026-09-17T21:58:00+02:00 Browse files
92dc0c7 parent: 7c6b16b
modified script/Love.s.sol +32 -12
@@ -2,25 +2,32 @@
22 pragma solidity ^0.8.30;
33
44 import {Love} from "../src/Love.sol";
5+import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
56 import {Script, console} from "forge-std/Script.sol";
67
78 /// @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
9+/// deployer. The address depends on the salt and the creation code, and
10+/// the creation code embeds the wETH address — so the token only gets
11+/// the same address on chains where wETH sits at the same address. Keep
1012 /// `solc`, the optimizer settings and `bytecode_hash = "none"` as they
11-/// are in `foundry.toml`, or the address changes.
13+/// are in `foundry.toml`, or the address changes too.
1214 contract LoveScript is Script {
1315 bytes32 public constant DEFAULT_SALT = keccak256("LOVE");
1416
17+ /// @dev wETH on Base, Optimism and every other OP-Stack chain.
18+ address public constant DEFAULT_WETH = 0x4200000000000000000000000000000000000006;
19+
1520 Love public love;
1621
1722 function run() public returns (Love) {
1823 bytes32 s = salt();
19- address predicted = predict(s);
24+ address w = wethAddress();
25+ address predicted = predict(s, w);
2026
2127 console.log("deployer ", CREATE2_FACTORY);
2228 console.log("salt ", vm.toString(s));
23- console.log("initCodeHash ", vm.toString(initCodeHash()));
29+ console.log("weth ", w);
30+ console.log("initCodeHash ", vm.toString(initCodeHash(w)));
2431 console.log("predicted ", predicted);
2532
2633 if (predicted.code.length > 0) {
@@ -30,7 +37,7 @@ contract LoveScript is Script {
3037 }
3138
3239 vm.startBroadcast();
33- love = new Love{salt: s}();
40+ love = new Love{salt: s}(IERC20(w));
3441 vm.stopBroadcast();
3542
3643 require(address(love) == predicted, "LoveScript: address mismatch");
@@ -38,6 +45,8 @@ contract LoveScript is Script {
3845 console.log("Love deployed at", address(love));
3946 console.log("name ", love.name());
4047 console.log("symbol ", love.symbol());
48+ console.log("weth ", address(love.weth()));
49+ console.log("rate ", love.RATE());
4150 console.log("totalSupply ", love.totalSupply());
4251
4352 return love;
@@ -46,23 +55,34 @@ contract LoveScript is Script {
4655 /// @notice Print the address `run()` would deploy to, without broadcasting.
4756 function predict() public view returns (address predicted) {
4857 bytes32 s = salt();
49- predicted = predict(s);
58+ address w = wethAddress();
59+ predicted = predict(s, w);
5060
5161 console.log("salt ", vm.toString(s));
52- console.log("initCodeHash ", vm.toString(initCodeHash()));
62+ console.log("weth ", w);
63+ console.log("initCodeHash ", vm.toString(initCodeHash(w)));
5364 console.log("predicted ", predicted);
5465 }
5566
56- function predict(bytes32 s) public pure returns (address) {
57- return vm.computeCreate2Address(s, initCodeHash(), CREATE2_FACTORY);
67+ function predict(bytes32 s, address w) public pure returns (address) {
68+ return vm.computeCreate2Address(s, initCodeHash(w), CREATE2_FACTORY);
69+ }
70+
71+ function initCode(address w) public pure returns (bytes memory) {
72+ return abi.encodePacked(type(Love).creationCode, abi.encode(w));
5873 }
5974
60- function initCodeHash() public pure returns (bytes32) {
61- return keccak256(type(Love).creationCode);
75+ function initCodeHash(address w) public pure returns (bytes32) {
76+ return keccak256(initCode(w));
6277 }
6378
6479 /// @dev `SALT` overrides the default, e.g. to mine a vanity address.
6580 function salt() public view returns (bytes32) {
6681 return vm.envOr("SALT", DEFAULT_SALT);
6782 }
83+
84+ /// @dev `WETH` overrides the default, which only holds on OP-Stack chains.
85+ function wethAddress() public view returns (address) {
86+ return vm.envOr("WETH", DEFAULT_WETH);
87+ }
6888 }
@@ -2,25 +2,32 @@
2 pragma solidity ^0.8.30;2 pragma solidity ^0.8.30;
3 3
4 import {Love} from "../src/Love.sol";4 import {Love} from "../src/Love.sol";
5+import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5 import {Script, console} from "forge-std/Script.sol";6 import {Script, console} from "forge-std/Script.sol";
6 7
7 /// @notice Deploys `Love` with CREATE2 through the canonical deterministic8 /// @notice Deploys `Love` with CREATE2 through the canonical deterministic
8-/// deployer, so the token gets the same address on every EVM network.9+/// deployer. The address depends on the salt and the creation code, and
9-/// The address depends only on the salt and the creation code — keep10+/// the creation code embeds the wETH address — so the token only gets
11+/// the same address on chains where wETH sits at the same address. Keep
10 /// `solc`, the optimizer settings and `bytecode_hash = "none"` as they12 /// `solc`, the optimizer settings and `bytecode_hash = "none"` as they
11-/// are in `foundry.toml`, or the address changes.13+/// are in `foundry.toml`, or the address changes too.
12 contract LoveScript is Script {14 contract LoveScript is Script {
13 bytes32 public constant DEFAULT_SALT = keccak256("LOVE");15 bytes32 public constant DEFAULT_SALT = keccak256("LOVE");
14 16
17+ /// @dev wETH on Base, Optimism and every other OP-Stack chain.
18+ address public constant DEFAULT_WETH = 0x4200000000000000000000000000000000000006;
19+
15 Love public love;20 Love public love;
16 21
17 function run() public returns (Love) {22 function run() public returns (Love) {
18 bytes32 s = salt();23 bytes32 s = salt();
19- address predicted = predict(s);24+ address w = wethAddress();
25+ address predicted = predict(s, w);
20 26
21 console.log("deployer ", CREATE2_FACTORY);27 console.log("deployer ", CREATE2_FACTORY);
22 console.log("salt ", vm.toString(s));28 console.log("salt ", vm.toString(s));
23- console.log("initCodeHash ", vm.toString(initCodeHash()));29+ console.log("weth ", w);
30+ console.log("initCodeHash ", vm.toString(initCodeHash(w)));
24 console.log("predicted ", predicted);31 console.log("predicted ", predicted);
25 32
26 if (predicted.code.length > 0) {33 if (predicted.code.length > 0) {
@@ -30,7 +37,7 @@ contract LoveScript is Script {
30 }37 }
31 38
32 vm.startBroadcast();39 vm.startBroadcast();
33- love = new Love{salt: s}();40+ love = new Love{salt: s}(IERC20(w));
34 vm.stopBroadcast();41 vm.stopBroadcast();
35 42
36 require(address(love) == predicted, "LoveScript: address mismatch");43 require(address(love) == predicted, "LoveScript: address mismatch");
@@ -38,6 +45,8 @@ contract LoveScript is Script {
38 console.log("Love deployed at", address(love));45 console.log("Love deployed at", address(love));
39 console.log("name ", love.name());46 console.log("name ", love.name());
40 console.log("symbol ", love.symbol());47 console.log("symbol ", love.symbol());
48+ console.log("weth ", address(love.weth()));
49+ console.log("rate ", love.RATE());
41 console.log("totalSupply ", love.totalSupply());50 console.log("totalSupply ", love.totalSupply());
42 51
43 return love;52 return love;
@@ -46,23 +55,34 @@ contract LoveScript is Script {
46 /// @notice Print the address `run()` would deploy to, without broadcasting.55 /// @notice Print the address `run()` would deploy to, without broadcasting.
47 function predict() public view returns (address predicted) {56 function predict() public view returns (address predicted) {
48 bytes32 s = salt();57 bytes32 s = salt();
49- predicted = predict(s);58+ address w = wethAddress();
59+ predicted = predict(s, w);
50 60
51 console.log("salt ", vm.toString(s));61 console.log("salt ", vm.toString(s));
52- console.log("initCodeHash ", vm.toString(initCodeHash()));62+ console.log("weth ", w);
63+ console.log("initCodeHash ", vm.toString(initCodeHash(w)));
53 console.log("predicted ", predicted);64 console.log("predicted ", predicted);
54 }65 }
55 66
56- function predict(bytes32 s) public pure returns (address) {67+ function predict(bytes32 s, address w) public pure returns (address) {
57- return vm.computeCreate2Address(s, initCodeHash(), CREATE2_FACTORY);68+ return vm.computeCreate2Address(s, initCodeHash(w), CREATE2_FACTORY);
69+ }
70+
71+ function initCode(address w) public pure returns (bytes memory) {
72+ return abi.encodePacked(type(Love).creationCode, abi.encode(w));
58 }73 }
59 74
60- function initCodeHash() public pure returns (bytes32) {75+ function initCodeHash(address w) public pure returns (bytes32) {
61- return keccak256(type(Love).creationCode);76+ return keccak256(initCode(w));
62 }77 }
63 78
64 /// @dev `SALT` overrides the default, e.g. to mine a vanity address.79 /// @dev `SALT` overrides the default, e.g. to mine a vanity address.
65 function salt() public view returns (bytes32) {80 function salt() public view returns (bytes32) {
66 return vm.envOr("SALT", DEFAULT_SALT);81 return vm.envOr("SALT", DEFAULT_SALT);
67 }82 }
83+
84+ /// @dev `WETH` overrides the default, which only holds on OP-Stack chains.
85+ function wethAddress() public view returns (address) {
86+ return vm.envOr("WETH", DEFAULT_WETH);
87+ }
68 }88 }
modified script/multichain-check.sh +7 -4
@@ -1,11 +1,13 @@
11 #!/usr/bin/env bash
22 #
33 # Deploys Love on two local anvil nodes with different chain ids and checks
4-# that CREATE2 gives the token the same address on both.
4+# that CREATE2 gives the token the same address on both. The address embeds the
5+# wETH address, so this only holds for chains sharing one wETH deployment —
6+# both nodes here are given the same WETH.
57 #
68 # Usage: ./script/multichain-check.sh
79 #
8-# Env overrides: PORT_A, PORT_B, CHAIN_ID_A, CHAIN_ID_B, SALT
10+# Env overrides: PORT_A, PORT_B, CHAIN_ID_A, CHAIN_ID_B, SALT, WETH
911
1012 set -euo pipefail
1113
@@ -77,10 +79,11 @@ report() {
7779 exit 1
7880 fi
7981
80- printf '%s %s %s bytes %s / %s\n' \
82+ printf '%s %s %s bytes %s / %s weth %s\n' \
8183 "$label" "$addr" "$(((${#code} - 2) / 2))" \
8284 "$(cast call "$addr" 'name()(string)' --rpc-url "$rpc")" \
83- "$(cast call "$addr" 'symbol()(string)' --rpc-url "$rpc")"
85+ "$(cast call "$addr" 'symbol()(string)' --rpc-url "$rpc")" \
86+ "$(cast call "$addr" 'weth()(address)' --rpc-url "$rpc")"
8487 }
8588
8689 cd "$ROOT"
@@ -1,11 +1,13 @@
1 #!/usr/bin/env bash1 #!/usr/bin/env bash
2 #2 #
3 # Deploys Love on two local anvil nodes with different chain ids and checks3 # Deploys Love on two local anvil nodes with different chain ids and checks
4-# that CREATE2 gives the token the same address on both.4+# that CREATE2 gives the token the same address on both. The address embeds the
5+# wETH address, so this only holds for chains sharing one wETH deployment —
6+# both nodes here are given the same WETH.
5 #7 #
6 # Usage: ./script/multichain-check.sh8 # Usage: ./script/multichain-check.sh
7 #9 #
8-# Env overrides: PORT_A, PORT_B, CHAIN_ID_A, CHAIN_ID_B, SALT10+# Env overrides: PORT_A, PORT_B, CHAIN_ID_A, CHAIN_ID_B, SALT, WETH
9 11
10 set -euo pipefail12 set -euo pipefail
11 13
@@ -77,10 +79,11 @@ report() {
77 exit 179 exit 1
78 fi80 fi
79 81
80- printf '%s %s %s bytes %s / %s\n' \82+ printf '%s %s %s bytes %s / %s weth %s\n' \
81 "$label" "$addr" "$(((${#code} - 2) / 2))" \83 "$label" "$addr" "$(((${#code} - 2) / 2))" \
82 "$(cast call "$addr" 'name()(string)' --rpc-url "$rpc")" \84 "$(cast call "$addr" 'name()(string)' --rpc-url "$rpc")" \
83- "$(cast call "$addr" 'symbol()(string)' --rpc-url "$rpc")"85+ "$(cast call "$addr" 'symbol()(string)' --rpc-url "$rpc")" \
86+ "$(cast call "$addr" 'weth()(address)' --rpc-url "$rpc")"
84 }87 }
85 88
86 cd "$ROOT"89 cd "$ROOT"
modified src/Love.sol +42 -7
@@ -2,16 +2,51 @@
22 pragma solidity ^0.8.30;
33
44 import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
5+import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
6+import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
57
68 /// @title Love
7-/// @notice A basic ERC-20 with an unrestricted mint: anyone can mint any amount
8-/// to any address. Supply is therefore unbounded and meaningless — this
9-/// is a faucet/demo token, not a token with economic value.
9+/// @notice An ERC-20 pegged to wETH at a fixed rate of 100000 LOVE per wETH.
10+/// Every LOVE in circulation is backed by wETH held by this contract:
11+/// supply only moves through `deposit` and `withdraw`, and both are
12+/// permissionless.
1013 contract Love is ERC20 {
11- constructor() ERC20("Love", "LOVE") {}
14+ using SafeERC20 for IERC20;
1215
13- /// @notice Mint `amount` tokens to `to`. Callable by anyone, without limit.
14- function mint(address to, uint256 amount) external {
15- _mint(to, amount);
16+ uint256 public constant RATE = 100_000;
17+
18+ IERC20 public immutable weth;
19+
20+ error AmountNotDivisibleByRate(uint256 loveAmount, uint256 rate);
21+
22+ event Deposit(address indexed account, uint256 wethAmount, uint256 loveAmount);
23+ event Withdraw(address indexed account, uint256 loveAmount, uint256 wethAmount);
24+
25+ constructor(IERC20 weth_) ERC20("Love", "LOVE") {
26+ weth = weth_;
27+ }
28+
29+ /// @notice Lock `wethAmount` wETH and mint `wethAmount * RATE` LOVE to the caller.
30+ function deposit(uint256 wethAmount) external {
31+ uint256 loveAmount = wethAmount * RATE;
32+
33+ weth.safeTransferFrom(msg.sender, address(this), wethAmount);
34+ _mint(msg.sender, loveAmount);
35+
36+ emit Deposit(msg.sender, wethAmount, loveAmount);
37+ }
38+
39+ /// @notice Burn `loveAmount` LOVE and release `loveAmount / RATE` wETH to the caller.
40+ /// @dev Reverts unless `loveAmount` is a multiple of `RATE`, so the peg never
41+ /// rounds against the caller or the remaining holders.
42+ function withdraw(uint256 loveAmount) external {
43+ if (loveAmount % RATE != 0) revert AmountNotDivisibleByRate(loveAmount, RATE);
44+
45+ uint256 wethAmount = loveAmount / RATE;
46+
47+ _burn(msg.sender, loveAmount);
48+ weth.safeTransfer(msg.sender, wethAmount);
49+
50+ emit Withdraw(msg.sender, loveAmount, wethAmount);
1651 }
1752 }
@@ -2,16 +2,51 @@
2 pragma solidity ^0.8.30;2 pragma solidity ^0.8.30;
3 3
4 import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";4 import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
5+import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
6+import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
5 7
6 /// @title Love8 /// @title Love
7-/// @notice A basic ERC-20 with an unrestricted mint: anyone can mint any amount9+/// @notice An ERC-20 pegged to wETH at a fixed rate of 100000 LOVE per wETH.
8-/// to any address. Supply is therefore unbounded and meaningless — this10+/// Every LOVE in circulation is backed by wETH held by this contract:
9-/// is a faucet/demo token, not a token with economic value.11+/// supply only moves through `deposit` and `withdraw`, and both are
12+/// permissionless.
10 contract Love is ERC20 {13 contract Love is ERC20 {
11- constructor() ERC20("Love", "LOVE") {}14+ using SafeERC20 for IERC20;
12 15
13- /// @notice Mint `amount` tokens to `to`. Callable by anyone, without limit.16+ uint256 public constant RATE = 100_000;
14- function mint(address to, uint256 amount) external {17+
15- _mint(to, amount);18+ IERC20 public immutable weth;
19+
20+ error AmountNotDivisibleByRate(uint256 loveAmount, uint256 rate);
21+
22+ event Deposit(address indexed account, uint256 wethAmount, uint256 loveAmount);
23+ event Withdraw(address indexed account, uint256 loveAmount, uint256 wethAmount);
24+
25+ constructor(IERC20 weth_) ERC20("Love", "LOVE") {
26+ weth = weth_;
27+ }
28+
29+ /// @notice Lock `wethAmount` wETH and mint `wethAmount * RATE` LOVE to the caller.
30+ function deposit(uint256 wethAmount) external {
31+ uint256 loveAmount = wethAmount * RATE;
32+
33+ weth.safeTransferFrom(msg.sender, address(this), wethAmount);
34+ _mint(msg.sender, loveAmount);
35+
36+ emit Deposit(msg.sender, wethAmount, loveAmount);
37+ }
38+
39+ /// @notice Burn `loveAmount` LOVE and release `loveAmount / RATE` wETH to the caller.
40+ /// @dev Reverts unless `loveAmount` is a multiple of `RATE`, so the peg never
41+ /// rounds against the caller or the remaining holders.
42+ function withdraw(uint256 loveAmount) external {
43+ if (loveAmount % RATE != 0) revert AmountNotDivisibleByRate(loveAmount, RATE);
44+
45+ uint256 wethAmount = loveAmount / RATE;
46+
47+ _burn(msg.sender, loveAmount);
48+ weth.safeTransfer(msg.sender, wethAmount);
49+
50+ emit Withdraw(msg.sender, loveAmount, wethAmount);
16 }51 }
17 }52 }
added test/mocks/MockWETH.sol +23 -0
new file mode 100644
@@ -0,0 +1,23 @@
1+// SPDX-License-Identifier: MIT
2+pragma solidity ^0.8.30;
3+
4+import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
5+
6+/// @notice Minimal stand-in for WETH9: wraps ETH 1:1, same 18 decimals, same
7+/// `deposit`/`withdraw` entrypoints. Test-only.
8+contract MockWETH is ERC20 {
9+ constructor() ERC20("Wrapped Ether", "WETH") {}
10+
11+ receive() external payable {
12+ deposit();
13+ }
14+
15+ function deposit() public payable {
16+ _mint(msg.sender, msg.value);
17+ }
18+
19+ function withdraw(uint256 amount) external {
20+ _burn(msg.sender, amount);
21+ payable(msg.sender).transfer(amount);
22+ }
23+}
new file mode 100644
@@ -0,0 +1,23 @@
1+// SPDX-License-Identifier: MIT
2+pragma solidity ^0.8.30;
3+
4+import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
5+
6+/// @notice Minimal stand-in for WETH9: wraps ETH 1:1, same 18 decimals, same
7+/// `deposit`/`withdraw` entrypoints. Test-only.
8+contract MockWETH is ERC20 {
9+ constructor() ERC20("Wrapped Ether", "WETH") {}
10+
11+ receive() external payable {
12+ deposit();
13+ }
14+
15+ function deposit() public payable {
16+ _mint(msg.sender, msg.value);
17+ }
18+
19+ function withdraw(uint256 amount) external {
20+ _burn(msg.sender, amount);
21+ payable(msg.sender).transfer(amount);
22+ }
23+}