Skip to content

Commit

Permalink
Merge pull request #6 from Hats-Protocol/multichain-deployment
Browse files Browse the repository at this point in the history
Multichain-deployment
  • Loading branch information
spengrah authored Sep 15, 2024
2 parents e91a34b + 71acd07 commit 64b3c1f
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 14 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ When users purchase a key, they are also automatically minted the hat. To remain

The module must serve as both a Hats eligibility module and a hatter contract. To mint the target hat when a user purchases a new key, it must be an amin of hte target hat — i.e. wear one of the target hat's admin hats — which makes it a hatter contract. To control eligibility for the target hat, it must also be set as the eligibility module for the target hat.

## Implementation Deployment

In order to deploy a new implementation — eg to a new network — you must not only call the constructor but also the `setUnlock()` initializer function. This function sets the address of the Unlock contract that instances created from the new implementation will use. This is separate from the constructor to enable deployment to use the same initCode and therefore achieve the same address across multiple chains, even though the Unlock address differs by chain.

The full flow is included in the `script/Deploy.s.sol` script.

## Development

This repo uses Foundry for development and testing. To get started:
Expand Down
7 changes: 5 additions & 2 deletions script/Deploy.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ contract Deploy is Script {

// default values
bool internal _verbose = true;
string internal _version = "0.1.1"; // increment this with each new deployment
string internal _version = "0.1.2"; // increment this with each new deployment
address internal _feeSplitRecipient = 0x58C8854a8E51BdCE9F00726B966905FE2719B4D9;
uint256 internal _feeSplitPercentage = 500; // 5%

Expand Down Expand Up @@ -69,7 +69,10 @@ contract Deploy is Script {
* 2. The provided salt, `SALT`
*/
implementation =
new PublicLockV14Eligibility{ salt: SALT }(_version, getUnlockAddress(), _feeSplitRecipient, _feeSplitPercentage);
new PublicLockV14Eligibility{ salt: SALT }(_version, _feeSplitRecipient, _feeSplitPercentage, deployer());

// set the unlock address on the implementation
implementation.setUnlock(getUnlockAddress());

vm.stopBroadcast();

Expand Down
2 changes: 1 addition & 1 deletion script/Deployments.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"unlockDeploymentBlock": 13994123
},
"100": {
"hatsModuleFactoryDeploymentBlock": 34772144,
"hatsModuleFactoryDeploymentBlock": 36005519,
"unlock": "0x1bc53f4303c711cc693F6Ec3477B83703DcB317f",
"unlockDeploymentBlock": 19338710
},
Expand Down
42 changes: 39 additions & 3 deletions src/PublicLockV14Eligibility.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ contract PublicLockV14Eligibility is HatsEligibilityModule, ILockKeyPurchaseHook
/// @dev Thrown when a non-referrer calls a function only authorized to the referrer
error NotReferrer();

/// @dev Thrown when the implementation contract has already been initialized
error ImplementationAlreadyInitialized();

/// @dev Thrown when a non-initializer calls a function that can only be called by the initializer
error NotInitializer();

/*//////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/
Expand Down Expand Up @@ -93,6 +99,13 @@ contract PublicLockV14Eligibility is HatsEligibilityModule, ILockKeyPurchaseHook
/// @notice The Unlock Protocol lock contract that is created along with this module and coupled to the hat
IPublicLock public lock;

/// @notice The address authorized to initialize the implementation contract by setting the Unlock Protocol factory
/// contract address
address internal _initializer;

/// @notice Whether the implementation contract has been initialized
bool internal _implementationInitialized;

/*//////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////*/
Expand All @@ -101,17 +114,40 @@ contract PublicLockV14Eligibility is HatsEligibilityModule, ILockKeyPurchaseHook
/// @param _version The version of the implementation contract
/// @param _referrer The referrer address, which will receive a portion of the fees
/// @param __referrerFeePercentage The percentage of fees to go to the referrer, in basis points (10000 = 100%)
/// @param __initializer The address authorized to initialize the implementation contract by setting the Unlock
/// Protocol factory contract address
/// @dev This is only used to deploy the implementation contract, and should not be used to deploy clones
constructor(string memory _version, IUnlock _unlock, address _referrer, uint256 __referrerFeePercentage)
constructor(string memory _version, address _referrer, uint256 __referrerFeePercentage, address __initializer)
HatsModule(_version)
{
unlock_ = _unlock;
REFERRER = _referrer;
implementationReferrerFeePercentage = __referrerFeePercentage;
_initializer = __initializer;
}

/*//////////////////////////////////////////////////////////////
IMPLEMENTATION INITIALIZER
//////////////////////////////////////////////////////////////*/

/// @notice Sets the Unlock Protocol factory contract. This function must be called before instances can be created.
/// @dev This function can only be called once by the initializer
/// @param _unlock The Unlock Protocol factory contract
function setUnlock(IUnlock _unlock) external {
// caller must be the initializer
if (msg.sender != _initializer) revert NotInitializer();

// the implementationcontract must not be initialized
if (_implementationInitialized) revert ImplementationAlreadyInitialized();

// prevent re-initialization
_implementationInitialized = true;

// set the unlock contract
unlock_ = _unlock;
}

/*//////////////////////////////////////////////////////////////
INITIALIZER
INSTANCE INITIALIZER
//////////////////////////////////////////////////////////////*/

/// @inheritdoc HatsModule
Expand Down
73 changes: 65 additions & 8 deletions test/PublicLockEligibility.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ contract PublicLockV14EligibilityTest is Deploy, Test {

uint256 public fork;
uint256 public BLOCK_NUMBER = 19_467_227; // deployment block for HatsModuleFactory v0.7.0
string public NETWORK = "mainnet";

IHats public HATS = IHats(0x3bc1A0Ad72417f2d411118085256fC53CBdDd137); // v1.hatsprotocol.eth
HatsModuleFactory public factory;
PublicLockV14Eligibility public instance;
Expand Down Expand Up @@ -55,8 +55,11 @@ contract PublicLockV14EligibilityTest is Deploy, Test {
string public MODULE_VERSION;

function setUp() public virtual {
// create and activate a fork, at BLOCK_NUMBER
fork = _createForkForNetwork(NETWORK);
// create and activate a fork, unless we're already on a fork
// 31337 is the chain id for the default local network
if (block.chainid == 31_337) {
fork = _createForkForNetwork("mainnet");
}

// deploy implementation via the script
prepare(false, MODULE_VERSION, referrer, referrerFeePercentage);
Expand Down Expand Up @@ -97,10 +100,6 @@ contract PublicLockV14EligibilityTest is Deploy, Test {
function _createForkForNetwork(string memory _network) internal returns (uint256) {
return vm.createSelectFork(vm.rpcUrl(_network), _getForkBlockForNetwork(_network));
}

function test_mainnet() public {
_createForkForNetwork("mainnet");
}
}

contract WithInstanceTest is PublicLockV14EligibilityTest {
Expand All @@ -111,7 +110,6 @@ contract WithInstanceTest is PublicLockV14EligibilityTest {
saltNonce = 1;

// set lock init data

expirationDuration_ = 1 days; // 1 day
tokenAddress_ = address(0); // ETH
keyPrice_ = 1 ether; // 1 ETH
Expand Down Expand Up @@ -243,6 +241,51 @@ contract Deployment is WithInstanceTest {
// lock version
assertEq(lock.publicLockVersion(), lockVersion);
}

function test_revert_setUnlock() public {
address unlockTry = makeAddr("unlockTry");

vm.prank(deployer());
vm.expectRevert(PublicLockV14Eligibility.ImplementationAlreadyInitialized.selector);
implementation.setUnlock(IUnlock(unlockTry));
}
}

contract SetUnlock is PublicLockV14EligibilityTest {
PublicLockV14Eligibility public newImplementation;
bytes32 public newSalt = keccak256(abi.encode(1));
address unlockTry = makeAddr("unlockTry");

function setUp() public override {
super.setUp();

// deploy a new implementation contract
newImplementation =
new PublicLockV14Eligibility{ salt: newSalt }(_version, _feeSplitRecipient, _feeSplitPercentage, deployer());
}

function test_happy() public {
// set the new implementation contract's unlock address
vm.prank(deployer());
newImplementation.setUnlock(IUnlock(unlockTry));
}

function test_revert_notInitializer() public {
// try to set the unlock address from an arbitrary address, expect a revert
vm.expectRevert(PublicLockV14Eligibility.NotInitializer.selector);
newImplementation.setUnlock(IUnlock(unlockTry));
}

function test_revert_alreadyInitialized() public {
// // set the new implementation contract's unlock address
vm.prank(deployer());
newImplementation.setUnlock(IUnlock(unlockTry));

// try to set the unlock address from an authorized address, expect a revert now that its already initialized
vm.prank(deployer());
vm.expectRevert(PublicLockV14Eligibility.ImplementationAlreadyInitialized.selector);
newImplementation.setUnlock(IUnlock(unlockTry));
}
}

contract DeploymentArbitrum is Deployment {
Expand All @@ -255,6 +298,8 @@ contract DeploymentArbitrum is Deployment {

function test_arbitrum() public {
test_createLock();
console2.log("chainid", block.chainid);
console2.log("implementation", address(implementation));
}
}

Expand All @@ -268,6 +313,8 @@ contract DeploymentBase is Deployment {

function test_base() public {
test_createLock();
console2.log("chainid", block.chainid);
console2.log("implementation", address(implementation));
}
}

Expand All @@ -281,6 +328,8 @@ contract DeploymentCelo is Deployment {

function test_celo() public {
test_createLock();
console2.log("chainid", block.chainid);
console2.log("implementation", address(implementation));
}
}

Expand All @@ -294,6 +343,8 @@ contract DeploymentGnosis is Deployment {

function test_gnosis() public {
test_createLock();
console2.log("chainid", block.chainid);
console2.log("implementation", address(implementation));
}
}

Expand All @@ -307,6 +358,8 @@ contract DeploymentOptimism is Deployment {

function test_optimism() public {
test_createLock();
console2.log("chainid", block.chainid);
console2.log("implementation", address(implementation));
}
}

Expand All @@ -320,6 +373,8 @@ contract DeploymentPolygon is Deployment {

function test_polygon() public {
test_createLock();
console2.log("chainid", block.chainid);
console2.log("implementation", address(implementation));
}
}

Expand All @@ -333,6 +388,8 @@ contract DeploymentSepolia is Deployment {

function test_sepolia() public {
test_createLock();
console2.log("chainid", block.chainid);
console2.log("implementation", address(implementation));
}
}

Expand Down

0 comments on commit 64b3c1f

Please sign in to comment.