Skip to content

Commit

Permalink
feat: add UniswapV4DeployerCompetition (#117)
Browse files Browse the repository at this point in the history
* feat: add UniswapV4DeployerCompetition

this commit adds a contract that creates a competition to generate the
vanity address for Uniswap V4. It does so using CREATE2 salts,
pre-calculating the address at which the contract will be deployed and
applying a score to the address based on its vanity. leading 0's are
weighted most heavily, followed by other 0's and 4's. The winner
receives an NFT, a bounty, and deployer privileges

* add comments

* change scoring to discuss

* interface plus tests

* inheritdoc tags

* change scoring algorithm

* feat: remove prizes

this commit removes the ETH payout, NFT mint, and exclusive deploy
rights

* feat: add natspec

* fix: snaps

* feat: improve test

* feat: cleanup scoring code

* fix: typo

* update constructor parameters for pool manager

* correct snapshots

* Include address in salt

* remove console logs

* correct test name

* More constructor parameters

* fix: remove override keywords

* fix: remove unused code

* fix: use default create2 function signature

* feat: remove unused import

* fix: remove unused v4Owner variable

* fix: add rules to natspec

* fix(deployComp): remove bestaddress storage

This commit removes the storage variable for bestAddres, calculating it
on-the-fly instead. It also just takes the initial `salt = 0` value as
a default score-to-beat rather than a sentinel for any salt is allowed
as a simplification

* fix: minor nits

- comparison strictness match comments
- unchecked vanity math

* snapshot

---------

Co-authored-by: dianakocsis <diana.kocsis@uniswap.org>
Co-authored-by: hensha256 <henshawalice@gmail.com>
  • Loading branch information
3 people authored Nov 8, 2024
1 parent 2768c3d commit 0a97fda
Show file tree
Hide file tree
Showing 8 changed files with 488 additions and 4 deletions.
2 changes: 1 addition & 1 deletion .forge-snapshots/positionDescriptor bytecode size.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
31847
31728
2 changes: 1 addition & 1 deletion lib/v4-core
85 changes: 85 additions & 0 deletions src/UniswapV4DeployerCompetition.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
25 changes: 25 additions & 0 deletions src/interfaces/IUniswapV4DeployerCompetition.sol
Original file line number Diff line number Diff line change
@@ -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;
}
97 changes: 97 additions & 0 deletions src/libraries/VanityAddressLib.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
178 changes: 178 additions & 0 deletions test/UniswapV4DeployerCompetition.t.sol
Original file line number Diff line number Diff line change
@@ -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))));
}
}
Loading

0 comments on commit 0a97fda

Please sign in to comment.