// SPDX-License-Identifier: MIT pragma solidity ^0.8.30; import {LoveScript} from "../script/Love.s.sol"; import {Love} from "../src/Love.sol"; import {Fixtures} from "./Fixtures.sol"; import {Test} from "forge-std/Test.sol"; /// @title LoveCreate2Test /// @notice The deployment address now derives from the salt and the creation /// code alone. wETH used to be a constructor argument and so part of /// that creation code, which gave the token a different address on /// every chain whose wETH sat elsewhere; it is read from the registry /// instead. These tests pin the consequence: same salt, same address, /// on any chain, whatever its wETH is and wherever it lives. contract LoveCreate2Test is Test, Fixtures { bytes32 constant SALT = keccak256("LOVE"); address constant OP_STACK_WETH = 0x4200000000000000000000000000000000000006; address constant MAINNET_WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; LoveScript script; function setUp() public { script = new LoveScript(); // The canonical deterministic deployer, as it exists on live networks. assertGt(CREATE2_FACTORY.code.length, 0, "create2 deployer missing"); } /*////////////////////////////////////////////////////////////// THE REGISTRY //////////////////////////////////////////////////////////////*/ /// @dev `Love` has the registry's address compiled in, so if the registry's /// creation code moves — an edit to the contract, a solc bump, a /// change of optimizer settings — this fails rather than shipping a /// token pointing at an empty address. function test_LoveRegistryConstantMatchesDeterministicAddress() public { setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY); assertEq(new Love().REGISTRY(), script.registryAddress()); } function test_RegistryLandsAtTheAddressLoveExpects() public { assertEq(address(deployRegistry()), script.registryAddress()); } /// @dev The registry takes no constructor arguments, which is what makes /// its address the same everywhere. function test_RegistryAddressIsChainAgnostic() public { address onThisChain = script.registryAddress(); vm.chainId(137); assertEq(script.registryAddress(), onThisChain); vm.chainId(42_161); assertEq(script.registryAddress(), onThisChain); } /*////////////////////////////////////////////////////////////// THE DERIVATION //////////////////////////////////////////////////////////////*/ function test_DeploysAtPredictedAddress() public { setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY); address predicted = vm.computeCreate2Address(SALT, keccak256(script.initCode()), CREATE2_FACTORY); Love deployed = Love(_deploy(SALT)); assertEq(address(deployed), predicted); assertEq(deployed.symbol(), "LOVE"); assertEq(address(deployed.WETH()), OP_STACK_WETH); } /// @dev What the deploy script prints must be what the chain gives back. function test_ScriptPredictionMatchesDeployment() public { setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY); assertEq(script.predict(SALT), _deploy(SALT)); } function test_ScriptUsesKeccakOfLoveAsDefaultSalt() public view { assertEq(script.DEFAULT_SALT(), SALT); } function test_ScriptDefaultsToOpStackWeth() public view { assertEq(script.DEFAULT_WETH(), OP_STACK_WETH); } /// @dev Same salt, same code, same address — and unlike before, no wETH /// argument that could make the answer differ. function test_PredictionIsChainAgnostic() public { address onThisChain = script.predict(SALT); vm.chainId(137); assertEq(script.predict(SALT), onThisChain); vm.chainId(42_161); assertEq(script.predict(SALT), onThisChain); } function test_DifferentSaltsGiveDifferentAddresses() public view { assertTrue(script.predict(SALT) != script.predict(keccak256("LOVE2"))); } /*////////////////////////////////////////////////////////////// THE POINT OF ALL THIS //////////////////////////////////////////////////////////////*/ /// @dev The test that replaces `test_DifferentWethGivesDifferentAddress`. /// Two chains, two different wETH implementations at two different /// addresses, one LOVE address. This is what the registry buys. function test_SameAddressOnChainsWithDifferentWeth() public { // A snapshot stands in for a second chain. Etching the code away is // not enough: a deployed account keeps its nonce, and CREATE2 refuses // to build over that, so the registry could never be redeployed. uint256 freshChain = vm.snapshotState(); setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY); address onOpStack = _deploy(SALT); assertEq(address(Love(onOpStack).WETH()), OP_STACK_WETH); vm.revertToState(freshChain); // Different chain id, a different wETH implementation, at a different // address. vm.chainId(1); setUpChain(MAINNET_WETH, WETH9_CANONICAL); address onMainnet = _deploy(SALT); assertEq(address(Love(onMainnet).WETH()), MAINNET_WETH); assertEq(onOpStack, onMainnet, "LOVE must land at one address on both chains"); } /// @dev Redeploying with the same salt must fail, not silently return the /// existing token. function test_RevertWhen_RedeployingWithSameSalt() public { setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY); _deploy(SALT); (bool ok,) = CREATE2_FACTORY.call(abi.encodePacked(SALT, script.initCode())); assertFalse(ok); } function testFuzz_PredictionMatchesDeployment(bytes32 salt) public { setUpChain(OP_STACK_WETH, WETH9_OP_LEGACY); assertEq(script.predict(salt), _deploy(salt)); } /*////////////////////////////////////////////////////////////// CONSTRUCTOR GUARDS //////////////////////////////////////////////////////////////*/ function test_RevertWhen_RegistryNotDeployed() public { vm.expectRevert(abi.encodeWithSelector(Love.RegistryNotDeployed.selector, script.registryAddress())); new Love(); } function test_RevertWhen_WethNotRegistered() public { deployRegistry(); vm.expectRevert(abi.encodeWithSelector(Love.WethNotRegistered.selector, script.registryAddress())); new Love(); } /// @dev Deploys through the canonical deterministic deployer, as the script does. function _deploy(bytes32 salt) internal returns (address deployed) { (bool ok, bytes memory ret) = CREATE2_FACTORY.call(abi.encodePacked(salt, script.initCode())); require(ok, "create2 deployment failed"); // casting to 'bytes20' is safe because the deployer returns the 20-byte address, raw // forge-lint: disable-next-line(unsafe-typecast) deployed = address(bytes20(ret)); } }