-
Notifications
You must be signed in to change notification settings - Fork 515
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add UniswapV4DeployerCompetition #117
Changes from all commits
d02ba21
246abe3
cf4c612
5db7a3a
1af2165
e8a4854
0bbf0dc
5966068
1549749
663b019
c744963
07657f4
d70e668
ef02200
a7bfb24
1a27be0
7ee4dea
81791ba
58fa41b
e56c8b7
5e38cb8
f7b60a7
46557ec
17a81bf
bc20985
f788998
8055ecb
df948e3
fbd1596
1a7f117
66b91ce
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
31847 | ||
31728 | ||
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; | ||
marktoda marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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); | ||
marktoda marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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; | ||
marktoda marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
emit NewAddressFound(newAddress, msg.sender, newAddress.score()); | ||
} | ||
|
||
/// @inheritdoc IUniswapV4DeployerCompetition | ||
function deploy(bytes memory bytecode) external { | ||
if (keccak256(bytecode) != initCodeHash) { | ||
marktoda marked this conversation as resolved.
Show resolved
Hide resolved
|
||
revert InvalidBytecode(); | ||
} | ||
|
||
if (block.timestamp <= competitionDeadline) { | ||
revert CompetitionNotOver(block.timestamp, competitionDeadline); | ||
} | ||
|
||
if (msg.sender != deployer && block.timestamp <= exclusiveDeployDeadline) { | ||
// anyone can deploy after the deadline | ||
marktoda marked this conversation as resolved.
Show resolved
Hide resolved
|
||
revert NotAllowedToDeploy(msg.sender, deployer); | ||
} | ||
|
||
// the owner of the contract must be encoded in the bytecode | ||
marktoda marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Create2.deploy(0, bestAddressSalt, bytecode); | ||
hensha256 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
/// @dev returns the best address found so far | ||
function bestAddress() public view returns (address) { | ||
return Create2.computeAddress(bestAddressSalt, initCodeHash); | ||
} | ||
} |
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; | ||
} |
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; | ||
} | ||
} | ||
} |
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)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think maybe prefer having the address in the top bits rather than bottom? Just makes it a prefix and incrementing the lower bits feels more natural There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe i'm wrong but i think because bytes are left-aligned this is already in the top bits. Let me check 👀 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. confirmed this btw - address is left aligned |
||
|
||
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)))); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WHYYYYYYY 😂