Skip to content
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

Merged
merged 31 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
d02ba21
feat: add UniswapV4DeployerCompetition
marktoda Jun 4, 2024
246abe3
merge main
dianakocsis Sep 30, 2024
cf4c612
add comments
dianakocsis Oct 4, 2024
5db7a3a
change scoring to discuss
dianakocsis Oct 7, 2024
1af2165
interface plus tests
dianakocsis Oct 7, 2024
e8a4854
inheritdoc tags
dianakocsis Oct 7, 2024
0bbf0dc
change scoring algorithm
dianakocsis Oct 11, 2024
5966068
feat: remove prizes
marktoda Oct 17, 2024
1549749
Merge branch 'main' into add-deployer
marktoda Oct 17, 2024
663b019
feat: add natspec
marktoda Oct 17, 2024
c744963
fix: snaps
marktoda Oct 17, 2024
07657f4
feat: improve test
marktoda Oct 17, 2024
d70e668
feat: cleanup scoring code
marktoda Oct 18, 2024
ef02200
Merge branch 'main' into add-deployer
marktoda Oct 18, 2024
a7bfb24
fix: typo
marktoda Oct 18, 2024
1a27be0
Merge branch 'main' into add-deployer
hensha256 Nov 1, 2024
7ee4dea
update constructor parameters for pool manager
hensha256 Nov 1, 2024
81791ba
correct snapshots
hensha256 Nov 1, 2024
58fa41b
Include address in salt
hensha256 Nov 1, 2024
e56c8b7
remove console logs
hensha256 Nov 1, 2024
5e38cb8
correct test name
hensha256 Nov 1, 2024
f7b60a7
More constructor parameters
hensha256 Nov 1, 2024
46557ec
fix: remove override keywords
marktoda Nov 6, 2024
17a81bf
fix: remove unused code
marktoda Nov 6, 2024
bc20985
fix: use default create2 function signature
marktoda Nov 6, 2024
f788998
feat: remove unused import
marktoda Nov 6, 2024
8055ecb
fix: remove unused v4Owner variable
marktoda Nov 6, 2024
df948e3
fix: add rules to natspec
marktoda Nov 6, 2024
fbd1596
fix(deployComp): remove bestaddress storage
marktoda Nov 7, 2024
1a7f117
fix: minor nits
marktoda Nov 7, 2024
66b91ce
snapshot
hensha256 Nov 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WHYYYYYYY 😂

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;
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);
}
}
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));
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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

Copy link
Contributor

Choose a reason for hiding this comment

The 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 👀

Copy link
Contributor

Choose a reason for hiding this comment

The 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))));
}
}
Loading
Loading