diff --git a/.forge-snapshots/positionDescriptor bytecode size.snap b/.forge-snapshots/positionDescriptor bytecode size.snap index 1ee6cee6..4babbfb1 100644 --- a/.forge-snapshots/positionDescriptor bytecode size.snap +++ b/.forge-snapshots/positionDescriptor bytecode size.snap @@ -1 +1 @@ -31847 \ No newline at end of file +31728 \ No newline at end of file diff --git a/lib/v4-core b/lib/v4-core index 0a849b18..362c9cab 160000 --- a/lib/v4-core +++ b/lib/v4-core @@ -1 +1 @@ -Subproject commit 0a849b1810210bc523da73399d6ed8e4480fd3f5 +Subproject commit 362c9cab7c03ca1122795aed2e8e118de9ed186e diff --git a/src/UniswapV4DeployerCompetition.sol b/src/UniswapV4DeployerCompetition.sol new file mode 100644 index 00000000..c9af05a6 --- /dev/null +++ b/src/UniswapV4DeployerCompetition.sol @@ -0,0 +1,85 @@ +// SPADIX-License-Identifier: UNLICENSED +pragma solidity 0.8.26; + +import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; +import {VanityAddressLib} from "./libraries/VanityAddressLib.sol"; +import {IUniswapV4DeployerCompetition} from "./interfaces/IUniswapV4DeployerCompetition.sol"; + +/// @title UniswapV4DeployerCompetition +/// @notice A contract to crowdsource a salt for the best Uniswap V4 address +contract UniswapV4DeployerCompetition is IUniswapV4DeployerCompetition { + using VanityAddressLib for address; + + /// @dev The salt for the best address found so far + bytes32 public bestAddressSalt; + /// @dev The submitter of the best address found so far + address public bestAddressSubmitter; + + /// @dev The deadline for the competition + uint256 public immutable competitionDeadline; + /// @dev The init code hash of the V4 contract + bytes32 public immutable initCodeHash; + + /// @dev The deployer who can initiate the deployment of the v4 PoolManager, until the exclusive deploy deadline. + /// @dev After this deadline anyone can deploy. + address public immutable deployer; + /// @dev The deadline for exclusive deployment by deployer after deadline + uint256 public immutable exclusiveDeployDeadline; + + constructor( + bytes32 _initCodeHash, + uint256 _competitionDeadline, + address _exclusiveDeployer, + uint256 _exclusiveDeployLength + ) { + initCodeHash = _initCodeHash; + competitionDeadline = _competitionDeadline; + exclusiveDeployDeadline = _competitionDeadline + _exclusiveDeployLength; + deployer = _exclusiveDeployer; + } + + /// @inheritdoc IUniswapV4DeployerCompetition + function updateBestAddress(bytes32 salt) external { + if (block.timestamp > competitionDeadline) { + revert CompetitionOver(block.timestamp, competitionDeadline); + } + + address saltSubAddress = address(bytes20(salt)); + if (saltSubAddress != msg.sender && saltSubAddress != address(0)) revert InvalidSender(salt, msg.sender); + + address newAddress = Create2.computeAddress(salt, initCodeHash); + address _bestAddress = bestAddress(); + if (!newAddress.betterThan(_bestAddress)) { + revert WorseAddress(newAddress, _bestAddress, newAddress.score(), _bestAddress.score()); + } + + bestAddressSalt = salt; + bestAddressSubmitter = msg.sender; + + emit NewAddressFound(newAddress, msg.sender, newAddress.score()); + } + + /// @inheritdoc IUniswapV4DeployerCompetition + function deploy(bytes memory bytecode) external { + if (keccak256(bytecode) != initCodeHash) { + revert InvalidBytecode(); + } + + if (block.timestamp <= competitionDeadline) { + revert CompetitionNotOver(block.timestamp, competitionDeadline); + } + + if (msg.sender != deployer && block.timestamp <= exclusiveDeployDeadline) { + // anyone can deploy after the deadline + revert NotAllowedToDeploy(msg.sender, deployer); + } + + // the owner of the contract must be encoded in the bytecode + Create2.deploy(0, bestAddressSalt, bytecode); + } + + /// @dev returns the best address found so far + function bestAddress() public view returns (address) { + return Create2.computeAddress(bestAddressSalt, initCodeHash); + } +} diff --git a/src/interfaces/IUniswapV4DeployerCompetition.sol b/src/interfaces/IUniswapV4DeployerCompetition.sol new file mode 100644 index 00000000..82959230 --- /dev/null +++ b/src/interfaces/IUniswapV4DeployerCompetition.sol @@ -0,0 +1,25 @@ +// SPADIX-License-Identifier: UNLICENSED +pragma solidity 0.8.26; + +/// @title UniswapV4DeployerCompetition +/// @notice A competition to deploy the UniswapV4 contract with the best address +interface IUniswapV4DeployerCompetition { + event NewAddressFound(address indexed bestAddress, address indexed submitter, uint256 score); + + error InvalidBytecode(); + error CompetitionNotOver(uint256 currentTime, uint256 deadline); + error CompetitionOver(uint256 currentTime, uint256 deadline); + error NotAllowedToDeploy(address sender, address deployer); + error WorseAddress(address newAddress, address bestAddress, uint256 newScore, uint256 bestScore); + error InvalidSender(bytes32 salt, address sender); + + /// @notice Updates the best address if the new address has a better vanity score + /// @param salt The salt to use to compute the new address with CREATE2 + /// @dev The first 20 bytes of the salt must be either address(0) or msg.sender + function updateBestAddress(bytes32 salt) external; + + /// @notice deploys the Uniswap v4 PoolManager contract + /// @param bytecode The bytecode of the Uniswap v4 PoolManager contract + /// @dev The bytecode must match the initCodeHash + function deploy(bytes memory bytecode) external; +} diff --git a/src/libraries/VanityAddressLib.sol b/src/libraries/VanityAddressLib.sol new file mode 100644 index 00000000..ba35b969 --- /dev/null +++ b/src/libraries/VanityAddressLib.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +/// @title VanityAddressLib +/// @notice A library to score addresses based on their vanity +library VanityAddressLib { + /// @notice Compares two addresses and returns true if the first address has a better vanity score + /// @param first The first address to compare + /// @param second The second address to compare + /// @return better True if the first address has a better vanity score + function betterThan(address first, address second) internal pure returns (bool better) { + return score(first) > score(second); + } + + /// @notice Scores an address based on its vanity + /// @dev Scoring rules: + /// Requirement: The first nonzero nibble must be 4 + /// 10 points for every leading 0 nibble + /// 40 points if the first 4 is followed by 3 more 4s + /// 20 points if the first nibble after the 4 4s is NOT a 4 + /// 20 points if the last 4 nibbles are 4s + /// 1 point for every 4 + /// @param addr The address to score + /// @return calculatedScore The vanity score of the address + function score(address addr) internal pure returns (uint256 calculatedScore) { + // convert the address to bytes for easier parsing + bytes20 addrBytes = bytes20(addr); + + unchecked { + // 10 points per leading zero nibble + uint256 leadingZeroCount = getLeadingNibbleCount(addrBytes, 0, 0); + calculatedScore += (leadingZeroCount * 10); + + // special handling for 4s immediately after leading 0s + uint256 leadingFourCount = getLeadingNibbleCount(addrBytes, leadingZeroCount, 4); + // If the first nonzero nibble is not 4, return 0 + if (leadingFourCount == 0) { + return 0; + } else if (leadingFourCount == 4) { + // 60 points if exactly 4 4s + calculatedScore += 60; + } else if (leadingFourCount > 4) { + // 40 points if more than 4 4s + calculatedScore += 40; + } + + // handling for remaining nibbles + for (uint256 i = 0; i < addrBytes.length * 2; i++) { + uint8 currentNibble = getNibble(addrBytes, i); + + // 1 extra point for any 4 nibbles + if (currentNibble == 4) { + calculatedScore += 1; + } + } + + // If the last 4 nibbles are 4s, add 20 points + if (addrBytes[18] == 0x44 && addrBytes[19] == 0x44) { + calculatedScore += 20; + } + } + } + + /// @notice Returns the number of leading nibbles in an address that match a given value + /// @param addrBytes The address to count the leading zero nibbles in + function getLeadingNibbleCount(bytes20 addrBytes, uint256 startIndex, uint8 comparison) + internal + pure + returns (uint256 count) + { + if (startIndex >= addrBytes.length * 2) { + return count; + } + + for (uint256 i = startIndex; i < addrBytes.length * 2; i++) { + uint8 currentNibble = getNibble(addrBytes, i); + if (currentNibble != comparison) { + return count; + } + count += 1; + } + } + + /// @notice Returns the nibble at a given index in an address + /// @param input The address to get the nibble from + /// @param nibbleIndex The index of the nibble to get + function getNibble(bytes20 input, uint256 nibbleIndex) internal pure returns (uint8 currentNibble) { + uint8 currByte = uint8(input[nibbleIndex / 2]); + if (nibbleIndex % 2 == 0) { + // Get the higher nibble of the byte + currentNibble = currByte >> 4; + } else { + // Get the lower nibble of the byte + currentNibble = currByte & 0x0F; + } + } +} diff --git a/test/UniswapV4DeployerCompetition.t.sol b/test/UniswapV4DeployerCompetition.t.sol new file mode 100644 index 00000000..265d3b6f --- /dev/null +++ b/test/UniswapV4DeployerCompetition.t.sol @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import {Owned} from "solmate/src/auth/Owned.sol"; +import {Test} from "forge-std/Test.sol"; +import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; +import {UniswapV4DeployerCompetition} from "../src/UniswapV4DeployerCompetition.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {VanityAddressLib} from "../src/libraries/VanityAddressLib.sol"; +import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; +import {IUniswapV4DeployerCompetition} from "../src/interfaces/IUniswapV4DeployerCompetition.sol"; + +contract UniswapV4DeployerCompetitionTest is Test { + using VanityAddressLib for address; + + UniswapV4DeployerCompetition competition; + bytes32 initCodeHash; + address deployer; + address v4Owner; + address winner; + address defaultAddress; + uint256 competitionDeadline; + uint256 exclusiveDeployLength = 1 days; + + bytes32 mask20bytes = bytes32(uint256(type(uint96).max)); + + function setUp() public { + competitionDeadline = block.timestamp + 7 days; + v4Owner = makeAddr("V4Owner"); + winner = makeAddr("Winner"); + deployer = makeAddr("Deployer"); + vm.prank(deployer); + initCodeHash = keccak256(abi.encodePacked(type(PoolManager).creationCode, uint256(uint160(v4Owner)))); + competition = + new UniswapV4DeployerCompetition(initCodeHash, competitionDeadline, deployer, exclusiveDeployLength); + defaultAddress = Create2.computeAddress(bytes32(0), initCodeHash, address(competition)); + } + + function test_defaultSalt_deploy_succeeds() public { + assertEq(competition.bestAddressSubmitter(), address(0)); + assertEq(competition.bestAddressSalt(), bytes32(0)); + assertEq(competition.bestAddress(), defaultAddress); + + assertEq(defaultAddress.code.length, 0); + vm.warp(competition.competitionDeadline() + 1); + vm.prank(deployer); + competition.deploy(abi.encodePacked(type(PoolManager).creationCode, uint256(uint160(v4Owner)))); + assertFalse(defaultAddress.code.length == 0); + assertEq(Owned(defaultAddress).owner(), v4Owner); + } + + function test_updateBestAddress_succeeds(bytes32 salt) public { + salt = (salt & mask20bytes) | bytes32(bytes20(winner)); + + assertEq(competition.bestAddressSubmitter(), address(0)); + assertEq(competition.bestAddressSalt(), bytes32(0)); + assertEq(competition.bestAddress(), defaultAddress); + + address newAddress = Create2.computeAddress(salt, initCodeHash, address(competition)); + vm.assume(newAddress.betterThan(defaultAddress)); + + vm.prank(winner); + vm.expectEmit(true, true, true, false, address(competition)); + emit IUniswapV4DeployerCompetition.NewAddressFound(newAddress, winner, VanityAddressLib.score(newAddress)); + competition.updateBestAddress(salt); + assertFalse(competition.bestAddress() == address(0), "best address not set"); + assertEq(competition.bestAddress(), newAddress, "wrong address set"); + assertEq(competition.bestAddressSubmitter(), winner, "wrong submitter set"); + assertEq(competition.bestAddressSalt(), salt, "incorrect salt set"); + address v4Core = competition.bestAddress(); + + assertEq(v4Core.code.length, 0); + vm.warp(competition.competitionDeadline() + 1); + vm.prank(deployer); + competition.deploy(abi.encodePacked(type(PoolManager).creationCode, uint256(uint160(v4Owner)))); + assertFalse(v4Core.code.length == 0); + assertEq(Owned(v4Core).owner(), v4Owner); + assertEq(address(competition).balance, 0 ether); + } + + function test_updateBestAddress_reverts_CompetitionOver(bytes32 salt) public { + vm.warp(competition.competitionDeadline() + 1); + vm.expectRevert( + abi.encodeWithSelector( + IUniswapV4DeployerCompetition.CompetitionOver.selector, + block.timestamp, + competition.competitionDeadline() + ) + ); + competition.updateBestAddress(salt); + } + + function test_updateBestAddress_reverts_InvalidSigner(bytes32 salt) public { + vm.assume(bytes20(salt) != bytes20(0)); + vm.assume(bytes20(salt) != bytes20(winner)); + + vm.expectRevert(abi.encodeWithSelector(IUniswapV4DeployerCompetition.InvalidSender.selector, salt, winner)); + vm.prank(winner); + competition.updateBestAddress(salt); + } + + function test_updateBestAddress_reverts_WorseAddress(bytes32 salt) public { + vm.assume(salt != bytes32(0)); + salt = (salt & mask20bytes) | bytes32(bytes20(winner)); + + address newAddr = Create2.computeAddress(salt, initCodeHash, address(competition)); + if (!newAddr.betterThan(defaultAddress)) { + vm.expectRevert( + abi.encodeWithSelector( + IUniswapV4DeployerCompetition.WorseAddress.selector, + newAddr, + competition.bestAddress(), + newAddr.score(), + competition.bestAddress().score() + ) + ); + vm.prank(winner); + competition.updateBestAddress(salt); + } else { + vm.prank(winner); + competition.updateBestAddress(salt); + assertEq(competition.bestAddressSubmitter(), winner); + assertEq(competition.bestAddressSalt(), salt); + assertEq(competition.bestAddress(), newAddr); + } + } + + function test_deploy_succeeds(bytes32 salt) public { + salt = (salt & mask20bytes) | bytes32(bytes20(winner)); + + address newAddress = Create2.computeAddress(salt, initCodeHash, address(competition)); + vm.assume(newAddress.betterThan(defaultAddress)); + + vm.prank(winner); + competition.updateBestAddress(salt); + address v4Core = competition.bestAddress(); + + vm.warp(competition.competitionDeadline() + 1); + vm.prank(deployer); + competition.deploy(abi.encodePacked(type(PoolManager).creationCode, uint256(uint160(v4Owner)))); + assertFalse(v4Core.code.length == 0); + assertEq(Owned(v4Core).owner(), v4Owner); + assertEq(TickMath.MAX_TICK_SPACING, type(int16).max); + } + + function test_deploy_reverts_CompetitionNotOver(uint256 timestamp) public { + vm.assume(timestamp < competition.competitionDeadline()); + vm.warp(timestamp); + vm.expectRevert( + abi.encodeWithSelector( + IUniswapV4DeployerCompetition.CompetitionNotOver.selector, timestamp, competition.competitionDeadline() + ) + ); + competition.deploy(abi.encodePacked(type(PoolManager).creationCode, uint256(uint160(v4Owner)))); + } + + function test_deploy_reverts_InvalidBytecode() public { + vm.expectRevert(IUniswapV4DeployerCompetition.InvalidBytecode.selector); + vm.prank(deployer); + // set the owner as the winner not the correct owner + competition.deploy(abi.encodePacked(type(PoolManager).creationCode, uint256(uint160(winner)))); + } + + function test_deploy_reverts_NotAllowedToDeploy() public { + vm.warp(competition.competitionDeadline() + 1); + vm.prank(address(1)); + vm.expectRevert( + abi.encodeWithSelector(IUniswapV4DeployerCompetition.NotAllowedToDeploy.selector, address(1), deployer) + ); + competition.deploy(abi.encodePacked(type(PoolManager).creationCode, uint256(uint160(v4Owner)))); + } + + function test_deploy_succeeds_afterExcusiveDeployDeadline() public { + vm.warp(competition.exclusiveDeployDeadline() + 1); + vm.prank(address(1)); + competition.deploy(abi.encodePacked(type(PoolManager).creationCode, uint256(uint160(v4Owner)))); + } +} diff --git a/test/libraries/VanityAddressLib.t.sol b/test/libraries/VanityAddressLib.t.sol new file mode 100644 index 00000000..f9ae5474 --- /dev/null +++ b/test/libraries/VanityAddressLib.t.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {Test} from "forge-std/Test.sol"; +import {VanityAddressLib} from "../../src/libraries/VanityAddressLib.sol"; + +contract VanityAddressLibTest is Test { + function test_fuzz_reasonableScoreNeverReverts(address test) public pure { + uint256 score = VanityAddressLib.score(address(test)); + assertGe(score, 0); + assertLe(score, 444); + } + + function test_scoreAllFours() public pure { + address addr = address(0x4444444444444444444444444444444444444444); + uint256 score = VanityAddressLib.score(addr); + uint256 expected = 100; // 40 + 40 + 20 = 100 + assertEq(score, expected); + } + + function test_scoreLaterFours() public pure { + address addr = address(0x1444444444444444444444444444444444444444); + uint256 score = VanityAddressLib.score(addr); + uint256 expected = 0; // no leading 4 + assertEq(score, expected); + } + + function test_scoreMixed_4() public pure { + address addr = address(0x0044001111111111111111111111111111114114); + // counts first null byte + // counts first leading 4s after that + // does not count future null bytes + // counts 4 nibbles after that + uint256 score = VanityAddressLib.score(addr); + uint256 expected = 24; // 10 * 2 + 2 + 2 = 24 + assertEq(score, expected); + } + + function test_scoreMixed_44() public pure { + address addr = address(0x0044001111111111111111111111111111114444); + // counts first null byte + // counts first leading 4s after that + // does not count future null bytes + // counts 4 nibbles after that + uint256 score = VanityAddressLib.score(addr); + uint256 expected = 46; // 10 * 2 + 6 + 20 = 46 + assertEq(score, expected); + } + + function test_scoreMixed_halfZeroHalf4() public pure { + address addr = address(0x0004111111111111111111111111111111111111); + // counts first null byte + // counts first leading 4s after that + uint256 score = VanityAddressLib.score(addr); + uint256 expected = 31; // 10 * 3 + 1 = 31 + assertEq(score, expected); + } + + function test_scores_succeed() public pure { + assertEq(VanityAddressLib.score(address(0x0000000000000000000000000000000000000082)), 0); // 0 + assertEq(VanityAddressLib.score(address(0x0400000000000000000000000000000000000000)), 11); // 10 * 1 + 1 = 11 + assertEq(VanityAddressLib.score(address(0x0044000000000000000000000000000000004444)), 46); // 10 * 2 + 6 + 20 = 46 + assertEq(VanityAddressLib.score(address(0x4444000000000000000000000000000000004444)), 88); // 40 + 20 + 20 + 8 = 88 + assertEq(VanityAddressLib.score(address(0x0044440000000000000000000000000000000044)), 86); // 10 * 2 + 40 + 20 + 6 = 86 + assertEq(VanityAddressLib.score(address(0x0000444400000000000000000000000000004444)), 128); // 10 * 4 + 40 + 20 + 20 + 8 = 128 + assertEq(VanityAddressLib.score(address(0x0040444444444444444444444444444444444444)), 77); // 10 * 2 + 37 + 20 = 77 + assertEq(VanityAddressLib.score(address(0x0000000000000000000000000000000000000444)), 373); // 10 * 37 + 3 = 373 + assertEq(VanityAddressLib.score(address(0x0000000000000000000000000000000044444444)), 388); // 10 * 32 + 40 + 20 + 8 = 388 + assertEq(VanityAddressLib.score(address(0x0000000000000000000000000000000000454444)), 365); // 10 * 34 + 20 + 5 = 365 + assertEq(VanityAddressLib.score(address(0x0000000000000000000000000000000000000044)), 382); // 10 * 38 + 2 = 382 + assertEq(VanityAddressLib.score(address(0x0000000000000000000000000000000000000004)), 391); // 10 * 39 + 1 = 391 + assertEq(VanityAddressLib.score(address(0x0000000000000000000000000000000000444444)), 406); // 10 * 34 + 40 + 20 + 6 = 406 + assertEq(VanityAddressLib.score(address(0x0000000000000000000000000000000000044444)), 415); // 10 * 35 + 40 + 20 + 5 = 415 + assertEq(VanityAddressLib.score(address(0x0000000000000000000000000000000000444455)), 404); // 10 * 34 + 40 + 20 + 4 = 404 + assertEq(VanityAddressLib.score(address(0x0000000000000000000000000000000000044445)), 414); // 10 * 35 + 40 + 20 + 4 = 414 + assertEq(VanityAddressLib.score(address(0x0000000000000000000000000000000000004444)), 444); // 10 * 36 + 40 + 20 + 20 + 4 = 444 + } + + function test_betterThan() public pure { + address addr1 = address(0x0011111111111111111111111111111111111111); // 0 points + address addr2 = address(0x4000111111111111111111111111111111111111); // 1 points + address addr3 = address(0x0000411111111111111111111111111111111111); // 10 * 4 + 1 = 41 points + address addr4 = address(0x0000441111111111111111111111111111111111); // 10 * 4 + 2 = 42 points + address addr5 = address(0x0000440011111111111111111111111111111111); // 10 * 4 + 2 = 42 points + assertTrue(VanityAddressLib.betterThan(addr2, addr1)); // 1 > 0 + assertTrue(VanityAddressLib.betterThan(addr3, addr2)); // 41 > 1 + assertTrue(VanityAddressLib.betterThan(addr3, addr1)); // 41 > 0 + assertTrue(VanityAddressLib.betterThan(addr4, addr3)); // 42 > 41 + assertTrue(VanityAddressLib.betterThan(addr4, addr2)); // 42 > 1 + assertTrue(VanityAddressLib.betterThan(addr4, addr1)); // 42 > 0 + assertFalse(VanityAddressLib.betterThan(addr5, addr4)); // 42 == 42 + assertEq(VanityAddressLib.score(addr5), VanityAddressLib.score(addr4)); // 42 == 42 + assertTrue(VanityAddressLib.betterThan(addr5, addr3)); // 42 > 41 + assertTrue(VanityAddressLib.betterThan(addr5, addr2)); // 42 > 1 + assertTrue(VanityAddressLib.betterThan(addr5, addr1)); // 42 > 0 + + address addr6 = address(0x0000000000000000000000000000000000004444); + address addr7 = address(0x0000000000000000000000000000000000000082); + assertTrue(VanityAddressLib.betterThan(addr6, addr7)); // 10 * 36 + 40 + 20 + 20 + 4 = 444 > 0 + } +} diff --git a/test/position-managers/PositionManager.modifyLiquidities.t.sol b/test/position-managers/PositionManager.modifyLiquidities.t.sol index 6f422ccf..4e14a733 100644 --- a/test/position-managers/PositionManager.modifyLiquidities.t.sol +++ b/test/position-managers/PositionManager.modifyLiquidities.t.sol @@ -33,8 +33,6 @@ import {ActionConstants} from "../../src/libraries/ActionConstants.sol"; import {Planner, Plan} from "../shared/Planner.sol"; import {DeltaResolver} from "../../src/base/DeltaResolver.sol"; -import "forge-std/console2.sol"; - contract PositionManagerModifyLiquiditiesTest is Test, PosmTestSetup, LiquidityFuzzers { using StateLibrary for IPoolManager; using PoolIdLibrary for PoolKey;