-
Notifications
You must be signed in to change notification settings - Fork 489
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add UniswapV4DeployerCompetition (#117)
* 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
1 parent
2768c3d
commit 0a97fda
Showing
8 changed files
with
488 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
31847 | ||
31728 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)))); | ||
} | ||
} |
Oops, something went wrong.