diff --git a/src/deployers/CollectionPlusDeployer.sol b/src/deployers/CollectionPlusDeployer.sol new file mode 100644 index 0000000..e001a93 --- /dev/null +++ b/src/deployers/CollectionPlusDeployer.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +import { IManager } from "../manager/IManager.sol"; +import { IBaseToken } from "../token/interfaces/IBaseToken.sol"; +import { IPropertyIPFSMetadataRenderer } from "../metadata/interfaces/IPropertyIPFSMetadataRenderer.sol"; +import { ERC721RedeemMinter } from "../minters/ERC721RedeemMinter.sol"; +import { TokenTypesV2 } from "../token/default/types/TokenTypesV2.sol"; +import { Ownable } from "../lib/utils/Ownable.sol"; + +/// @title CollectionPlusDeployer +/// @notice A deployer that allows a user to deploy a Collection Plus style DAO in one transaction +/// @author @neokry +contract CollectionPlusDeployer { + /// /// + /// IMMUTABLES /// + /// /// + + /// @notice The contract upgrade manager + IManager public immutable manager; + + /// @notice The minter to deploy the DAO with + ERC721RedeemMinter public immutable redeemMinter; + + /// /// + /// STRUCTS /// + /// /// + + /// @notice Adds properties and/or items to be pseudo-randomly chosen from during token minting + /// @param names The names of the properties to add + /// @param items The items to add to each property + /// @param ipfsGroup The IPFS base URI and extension + struct MetadataParams { + string[] names; + IPropertyIPFSMetadataRenderer.ItemParam[] items; + IPropertyIPFSMetadataRenderer.IPFSGroup ipfsGroup; + } + + /// /// + /// CONSTRUCTOR /// + /// /// + + constructor(IManager _manager, ERC721RedeemMinter _redeemMinter) { + manager = _manager; + redeemMinter = _redeemMinter; + } + + /// /// + /// DEPLOYMENT /// + /// /// + + /// @notice Deploys a DAO with mirror and token redeeming enabled + /// @dev The address of this deployer must be set as founder 0 + /// @param _founderParams The DAO founders + /// @param _tokenParams The ERC-721 token settings + /// @param _auctionParams The auction settings + /// @param _govParams The governance settings + /// @param _metadataParams The metadata settings + /// @param _minterParams The minter settings + function deploy( + IManager.FounderParams[] calldata _founderParams, + IManager.MirrorTokenParams calldata _tokenParams, + IManager.AuctionParams calldata _auctionParams, + IManager.GovParams calldata _govParams, + MetadataParams calldata _metadataParams, + ERC721RedeemMinter.RedeemSettings calldata _minterParams + ) external returns (address) { + // Deploy the DAO with token mirroring enabled + (address token, address metadata, address auction, address treasury, ) = manager.deployWithMirror( + _founderParams, + _tokenParams, + _auctionParams, + _govParams + ); + + // Setup minter settings to use the redeem minter + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + minters[0] = TokenTypesV2.MinterParams({ minter: address(redeemMinter), allowed: true }); + + // Add new minter + IBaseToken(token).updateMinters(minters); + + // Initilize minter with given params + redeemMinter.setMintSettings(token, _minterParams); + + // Initilize metadata renderer with given params + IPropertyIPFSMetadataRenderer(metadata).addProperties(_metadataParams.names, _metadataParams.items, _metadataParams.ipfsGroup); + + // Transfer ownership of token contract + Ownable(token).transferOwnership(treasury); + + // Transfer ownership of auction contract + Ownable(auction).transferOwnership(treasury); + + return token; + } +} diff --git a/src/deployers/MigrationDeployer.sol b/src/deployers/MigrationDeployer.sol new file mode 100644 index 0000000..e974218 --- /dev/null +++ b/src/deployers/MigrationDeployer.sol @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +import { IManager } from "../manager/IManager.sol"; +import { IBaseToken } from "../token/interfaces/IBaseToken.sol"; +import { IPropertyIPFSMetadataRenderer } from "../metadata/interfaces/IPropertyIPFSMetadataRenderer.sol"; +import { MerkleReserveMinter } from "../minters/MerkleReserveMinter.sol"; +import { TokenTypesV2 } from "../token/default/types/TokenTypesV2.sol"; +import { Ownable } from "../lib/utils/Ownable.sol"; +import { ICrossDomainMessenger } from "./interfaces/ICrossDomainMessenger.sol"; + +/// @title MigrationDeployer +/// @notice A deployer that allows a DAO to migrate from L1 to L2 +/// @author @neokry +contract MigrationDeployer { + /// /// + /// EVENTS /// + /// /// + + /// @notice Deployer has been set + event DeployerSet(address deployer); + + /// /// + /// ERRORS /// + /// /// + + /// @dev Caller is not cross domain messenger + error NOT_CROSS_DOMAIN_MESSENGER(); + + /// @dev DAO is already deployed + error DAO_ALREADY_DEPLOYED(); + + /// @dev No DAO has been deployed + error NO_DAO_DEPLOYED(); + + /// @dev Transfer failed + error TRANSFER_FAILED(); + + /// /// + /// IMMUTABLES /// + /// /// + + /// @notice The contract upgrade manager + IManager public immutable manager; + + /// @notice The minter to deploy the DAO with + MerkleReserveMinter public immutable merkleMinter; + + /// @notice The cross domain messenger for the chain + ICrossDomainMessenger public immutable crossDomainMessenger; + + /// /// + /// STORAGE /// + /// /// + + /// @notice Mapping of L1 deployer => L2 deployed token + mapping(address => address) public crossDomainDeployerToToken; + + /// /// + /// MODIFIERS /// + /// /// + + /// @notice Modifier to revert if sender is not cross domain messenger + modifier onlyCrossDomainMessenger() { + if (msg.sender != address(crossDomainMessenger)) revert NOT_CROSS_DOMAIN_MESSENGER(); + _; + } + + /// /// + /// CONSTRUCTOR /// + /// /// + + constructor( + IManager _manager, + MerkleReserveMinter _merkleMinter, + ICrossDomainMessenger _crossDomainMessenger + ) { + manager = _manager; + merkleMinter = _merkleMinter; + crossDomainMessenger = _crossDomainMessenger; + } + + /// /// + /// DEPLOYMENT /// + /// /// + + /// @notice Deploys a DAO via cross domain message + /// @dev The address of this deployer must be set as founder 0 + /// @param _founderParams The DAO founders + /// @param _tokenParams The ERC-721 token settings + /// @param _auctionParams The auction settings + /// @param _govParams The governance settings + /// @param _minterParams The minter settings + function deploy( + IManager.FounderParams[] calldata _founderParams, + IManager.TokenParams calldata _tokenParams, + IManager.AuctionParams calldata _auctionParams, + IManager.GovParams calldata _govParams, + MerkleReserveMinter.MerkleMinterSettings calldata _minterParams + ) external onlyCrossDomainMessenger returns (address token) { + if (_getTokenFromSender() != address(0)) { + revert DAO_ALREADY_DEPLOYED(); + } + + // Deploy the DAO + (address _token, , , , ) = manager.deploy(_founderParams, _tokenParams, _auctionParams, _govParams); + + // Setup minter settings to use the redeem minter + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + minters[0] = TokenTypesV2.MinterParams({ minter: address(merkleMinter), allowed: true }); + + // Add new minter + IBaseToken(_token).updateMinters(minters); + + // Initilize minter with given params + merkleMinter.setMintSettings(_token, _minterParams); + + // Set the deployer + _setTokenDeployer(_token); + + return (_token); + } + + ///@notice Adds metadata properties to the migrated DAO + /// @param _names The names of the properties to add + /// @param _items The items to add to each property + /// @param _ipfsGroup The IPFS base URI and extension + function addMetadataProperties( + string[] calldata _names, + IPropertyIPFSMetadataRenderer.ItemParam[] calldata _items, + IPropertyIPFSMetadataRenderer.IPFSGroup calldata _ipfsGroup + ) external onlyCrossDomainMessenger { + (, address metadata, , , ) = _getDAOAddressesFromSender(); + IPropertyIPFSMetadataRenderer(metadata).addProperties(_names, _items, _ipfsGroup); + } + + ///@notice Called once all metadata properties are added to set ownership of migrated DAO contracts to treasury + function finalize() external onlyCrossDomainMessenger { + (address token, , address auction, address treasury, ) = _getDAOAddressesFromSender(); + + // Transfer ownership of token contract + Ownable(token).transferOwnership(treasury); + + // Transfer ownership of auction contract + Ownable(auction).transferOwnership(treasury); + } + + ///@notice Resets the stored deployment if L1 DAO wants to redeploy + function resetDeployment() external onlyCrossDomainMessenger { + _resetTokenDeployer(); + } + + /// /// + /// DEPOSIT /// + /// /// + + ///@notice Helper method to deposit ether from L1 DAO treasury to L2 DAO treasury + function depositToTreasury() external payable onlyCrossDomainMessenger { + (, , , address treasury, ) = _getDAOAddressesFromSender(); + + (bool success, ) = treasury.call{ value: msg.value }(""); + + // Revert if transfer fails + if (!success) { + revert TRANSFER_FAILED(); + } + } + + /// /// + /// PRIVATE /// + /// /// + + function _xMsgSender() private view returns (address) { + return crossDomainMessenger.xDomainMessageSender(); + } + + function _setTokenDeployer(address token) private { + crossDomainDeployerToToken[_xMsgSender()] = token; + } + + function _resetTokenDeployer() private { + delete crossDomainDeployerToToken[_xMsgSender()]; + } + + function _getTokenFromSender() private view returns (address) { + return crossDomainDeployerToToken[_xMsgSender()]; + } + + function _getDAOAddressesFromSender() + private + returns ( + address token, + address metadata, + address auction, + address treasury, + address governor + ) + { + address _token = _getTokenFromSender(); + + if (_token == address(0)) revert NO_DAO_DEPLOYED(); + + (address _metadata, address _auction, address _treasury, address _governor) = manager.getAddresses(_token); + return (_token, _metadata, _auction, _treasury, _governor); + } +} diff --git a/src/deployers/interfaces/ICrossDomainMessenger.sol b/src/deployers/interfaces/ICrossDomainMessenger.sol new file mode 100644 index 0000000..f25eb1b --- /dev/null +++ b/src/deployers/interfaces/ICrossDomainMessenger.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +/// @title ICrossDomainMessenger +interface ICrossDomainMessenger { + /// @notice Retrieves the address of the contract or wallet that initiated the currently + /// executing message on the other chain. Will throw an error if there is no message + /// currently being executed. Allows the recipient of a call to see who triggered it. + /// @return Address of the sender of the currently executing message on the other chain. + function xDomainMessageSender() external view returns (address); +} diff --git a/test/CollectionPlusDeployer.t.sol b/test/CollectionPlusDeployer.t.sol new file mode 100644 index 0000000..b208627 --- /dev/null +++ b/test/CollectionPlusDeployer.t.sol @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +import { NounsBuilderTest } from "./utils/NounsBuilderTest.sol"; +import { MetadataRendererTypesV1 } from "../../src/metadata/types/MetadataRendererTypesV1.sol"; +import { CollectionPlusDeployer } from "../../src/deployers/CollectionPlusDeployer.sol"; +import { ERC721RedeemMinter } from "../../src/minters/ERC721RedeemMinter.sol"; + +import { IToken, Token } from "../../src/token/default/Token.sol"; +import { MetadataRenderer } from "../../src/metadata/MetadataRenderer.sol"; +import { IAuction, Auction } from "../../src/auction/Auction.sol"; +import { IGovernor, Governor } from "../../src/governance/governor/Governor.sol"; +import { ITreasury, Treasury } from "../../src/governance/treasury/Treasury.sol"; + +contract CollectionPlusDeployerTest is NounsBuilderTest { + ERC721RedeemMinter minter; + CollectionPlusDeployer deployer; + ERC721RedeemMinter.RedeemSettings minterParams; + + function setUp() public virtual override { + super.setUp(); + + minter = new ERC721RedeemMinter(manager, zoraDAO); + deployer = new CollectionPlusDeployer(manager, minter); + } + + function deploy() internal { + setAltMockFounderParams(); + + setMockMirrorTokenParams(0, address(0)); + + setMockAuctionParams(); + + setMockGovParams(); + + getMetadataParams(); + + CollectionPlusDeployer.MetadataParams memory metadataParams = getMetadataParams(); + + address _token = deployer.deploy(foundersArr, mirrorTokenParams, auctionParams, govParams, metadataParams, minterParams); + (address _metadata, address _auction, address _treasury, address _governor) = manager.getAddresses(_token); + + token = Token(_token); + metadataRenderer = MetadataRenderer(_metadata); + auction = Auction(_auction); + treasury = Treasury(payable(_treasury)); + governor = Governor(_governor); + + vm.label(address(token), "TOKEN"); + vm.label(address(metadataRenderer), "METADATA_RENDERER"); + vm.label(address(auction), "AUCTION"); + vm.label(address(treasury), "TREASURY"); + vm.label(address(governor), "GOVERNOR"); + } + + function setAltMockFounderParams() internal virtual { + address[] memory wallets = new address[](3); + uint256[] memory percents = new uint256[](3); + uint256[] memory vestingEnds = new uint256[](3); + + wallets[0] = address(deployer); + wallets[1] = founder; + wallets[2] = founder2; + + percents[0] = 0; + percents[1] = 10; + percents[2] = 5; + + percents[0] = 0; + vestingEnds[1] = 4 weeks; + vestingEnds[2] = 4 weeks; + + setFounderParams(wallets, percents, vestingEnds); + } + + function getMetadataParams() internal pure returns (CollectionPlusDeployer.MetadataParams memory metadataParams) { + metadataParams.names = new string[](1); + metadataParams.names[0] = "testing"; + metadataParams.items = new MetadataRendererTypesV1.ItemParam[](2); + metadataParams.items[0] = MetadataRendererTypesV1.ItemParam({ propertyId: 0, name: "failure1", isNewProperty: true }); + metadataParams.items[1] = MetadataRendererTypesV1.ItemParam({ propertyId: 0, name: "failure2", isNewProperty: true }); + + metadataParams.ipfsGroup = MetadataRendererTypesV1.IPFSGroup({ baseUri: "BASE_URI", extension: "EXTENSION" }); + } + + function setMinterParams() internal { + minterParams = ERC721RedeemMinter.RedeemSettings({ + mintStart: 0, + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0 ether, + redeemToken: address(0) + }); + } + + function test_Deploy() external { + deploy(); + } + + function test_MinterIsSet() external { + deploy(); + + assertTrue(token.isMinter(address(minter))); + + (uint64 mintStart, uint64 mintEnd, uint64 pricePerToken, address redeemToken) = minter.redeemSettings(address(token)); + + assertEq(minterParams.mintStart, mintStart); + assertEq(minterParams.mintEnd, mintEnd); + assertEq(minterParams.pricePerToken, pricePerToken); + assertEq(minterParams.redeemToken, redeemToken); + } + + function test_MetadataIsSet() external { + deploy(); + + assertGt(metadataRenderer.propertiesCount(), 0); + assertGt(metadataRenderer.itemsCount(0), 0); + assertGt(metadataRenderer.ipfsDataCount(), 0); + } + + function test_FounderAreSet() external { + deploy(); + + IToken.Founder[] memory founders = token.getFounders(); + assertEq(founders.length, 2); + assertEq(founders[0].wallet, founder); + assertEq(founders[1].wallet, founder2); + } + + function test_TreasuryIsOwner() external { + deploy(); + + assertEq(token.owner(), address(treasury)); + assertEq(metadataRenderer.owner(), address(treasury)); + assertEq(auction.owner(), address(treasury)); + } +} diff --git a/test/MigrationDeployer.t.sol b/test/MigrationDeployer.t.sol new file mode 100644 index 0000000..b769f3e --- /dev/null +++ b/test/MigrationDeployer.t.sol @@ -0,0 +1,212 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +import { NounsBuilderTest } from "./utils/NounsBuilderTest.sol"; +import { MetadataRendererTypesV1 } from "../../src/metadata/types/MetadataRendererTypesV1.sol"; +import { MigrationDeployer } from "../../src/deployers/MigrationDeployer.sol"; +import { MerkleReserveMinter } from "../../src/minters/MerkleReserveMinter.sol"; +import { MockCrossDomainMessenger } from "./utils/mocks/MockCrossDomainMessenger.sol"; + +import { IToken, Token } from "../../src/token/default/Token.sol"; +import { MetadataRenderer } from "../../src/metadata/MetadataRenderer.sol"; +import { IAuction, Auction } from "../../src/auction/Auction.sol"; +import { IGovernor, Governor } from "../../src/governance/governor/Governor.sol"; +import { ITreasury, Treasury } from "../../src/governance/treasury/Treasury.sol"; + +contract MigrationDeployerTest is NounsBuilderTest { + MockCrossDomainMessenger xDomainMessenger; + MerkleReserveMinter minter; + MigrationDeployer deployer; + MerkleReserveMinter.MerkleMinterSettings minterParams; + + function setUp() public virtual override { + super.setUp(); + + minter = new MerkleReserveMinter(manager); + xDomainMessenger = new MockCrossDomainMessenger(founder); + deployer = new MigrationDeployer(manager, minter, xDomainMessenger); + } + + function deploy() internal { + setAltMockFounderParams(); + + setMockTokenParams(); + + setMockAuctionParams(); + + setMockGovParams(); + + vm.startPrank(address(xDomainMessenger)); + + address _token = deployer.deploy(foundersArr, tokenParams, auctionParams, govParams, minterParams); + + addMetadataProperties(); + + deployer.finalize(); + + vm.stopPrank(); + + (address _metadata, address _auction, address _treasury, address _governor) = manager.getAddresses(_token); + + token = Token(_token); + metadataRenderer = MetadataRenderer(_metadata); + auction = Auction(_auction); + treasury = Treasury(payable(_treasury)); + governor = Governor(_governor); + + vm.label(address(token), "TOKEN"); + vm.label(address(metadataRenderer), "METADATA_RENDERER"); + vm.label(address(auction), "AUCTION"); + vm.label(address(treasury), "TREASURY"); + vm.label(address(governor), "GOVERNOR"); + } + + function setAltMockFounderParams() internal virtual { + address[] memory wallets = new address[](3); + uint256[] memory percents = new uint256[](3); + uint256[] memory vestingEnds = new uint256[](3); + + wallets[0] = address(deployer); + wallets[1] = founder; + wallets[2] = founder2; + + percents[0] = 0; + percents[1] = 10; + percents[2] = 5; + + percents[0] = 0; + vestingEnds[1] = 4 weeks; + vestingEnds[2] = 4 weeks; + + setFounderParams(wallets, percents, vestingEnds); + } + + function addMetadataProperties() internal { + string[] memory names = new string[](1); + names[0] = "testing"; + MetadataRendererTypesV1.ItemParam[] memory items = new MetadataRendererTypesV1.ItemParam[](2); + items[0] = MetadataRendererTypesV1.ItemParam({ propertyId: 0, name: "failure1", isNewProperty: true }); + items[1] = MetadataRendererTypesV1.ItemParam({ propertyId: 0, name: "failure2", isNewProperty: true }); + + MetadataRendererTypesV1.IPFSGroup memory ipfsGroup = MetadataRendererTypesV1.IPFSGroup({ baseUri: "BASE_URI", extension: "EXTENSION" }); + + deployer.addMetadataProperties(names, items, ipfsGroup); + } + + function setMinterParams() internal { + minterParams = MerkleReserveMinter.MerkleMinterSettings({ + mintStart: 200, + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0.1 ether, + merkleRoot: hex"00", + snapshotBlock: 100 + }); + } + + function test_Deploy() external { + deploy(); + } + + function test_MinterIsSet() external { + deploy(); + + assertTrue(token.isMinter(address(minter))); + + (uint64 mintStart, uint64 mintEnd, uint64 pricePerToken, bytes32 merkleRoot, uint256 snapshotBlock) = minter.allowedMerkles(address(token)); + + assertEq(minterParams.mintStart, mintStart); + assertEq(minterParams.mintEnd, mintEnd); + assertEq(minterParams.pricePerToken, pricePerToken); + assertEq(minterParams.merkleRoot, merkleRoot); + assertEq(minterParams.snapshotBlock, snapshotBlock); + } + + function test_MetadataIsSet() external { + deploy(); + + assertGt(metadataRenderer.propertiesCount(), 0); + assertGt(metadataRenderer.itemsCount(0), 0); + assertGt(metadataRenderer.ipfsDataCount(), 0); + } + + function test_FounderAreSet() external { + deploy(); + + IToken.Founder[] memory founders = token.getFounders(); + assertEq(founders.length, 2); + assertEq(founders[0].wallet, founder); + assertEq(founders[1].wallet, founder2); + } + + function test_TreasuryIsOwner() external { + deploy(); + + assertEq(token.owner(), address(treasury)); + assertEq(metadataRenderer.owner(), address(treasury)); + assertEq(auction.owner(), address(treasury)); + } + + function test_ResetDeployment() external { + deploy(); + + assertEq(deployer.crossDomainDeployerToToken(xDomainMessenger.xDomainMessageSender()), address(token)); + + vm.prank(address(xDomainMessenger)); + deployer.resetDeployment(); + + assertEq(deployer.crossDomainDeployerToToken(xDomainMessenger.xDomainMessageSender()), address(0)); + } + + function test_DepositToTreasury() external { + deploy(); + + vm.deal(address(xDomainMessenger), 0.1 ether); + + vm.prank(address(xDomainMessenger)); + deployer.depositToTreasury{ value: 0.1 ether }(); + + assertEq(address(treasury).balance, 0.1 ether); + } + + function testRevert_OnlyCrossDomainMessenger() external { + setAltMockFounderParams(); + + setMockTokenParams(); + + setMockAuctionParams(); + + setMockGovParams(); + + vm.expectRevert(abi.encodeWithSignature("NOT_CROSS_DOMAIN_MESSENGER()")); + deployer.deploy(foundersArr, tokenParams, auctionParams, govParams, minterParams); + + vm.expectRevert(abi.encodeWithSignature("NOT_CROSS_DOMAIN_MESSENGER()")); + addMetadataProperties(); + + vm.expectRevert(abi.encodeWithSignature("NOT_CROSS_DOMAIN_MESSENGER()")); + deployer.finalize(); + + vm.expectRevert(abi.encodeWithSignature("NOT_CROSS_DOMAIN_MESSENGER()")); + deployer.resetDeployment(); + } + + function testRevert_NoDAODeployed() external { + vm.startPrank(address(xDomainMessenger)); + + vm.expectRevert(abi.encodeWithSignature("NO_DAO_DEPLOYED()")); + addMetadataProperties(); + + vm.expectRevert(abi.encodeWithSignature("NO_DAO_DEPLOYED()")); + deployer.finalize(); + + vm.stopPrank(); + } + + function testRevert_DAOAlreadyDeployed() external { + deploy(); + + vm.prank(address(xDomainMessenger)); + vm.expectRevert(abi.encodeWithSignature("DAO_ALREADY_DEPLOYED()")); + deployer.deploy(foundersArr, tokenParams, auctionParams, govParams, minterParams); + } +} diff --git a/test/utils/mocks/MockCrossDomainMessenger.sol b/test/utils/mocks/MockCrossDomainMessenger.sol new file mode 100644 index 0000000..757745a --- /dev/null +++ b/test/utils/mocks/MockCrossDomainMessenger.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +import { ICrossDomainMessenger } from "../../../src/deployers/interfaces/ICrossDomainMessenger.sol"; + +contract MockCrossDomainMessenger is ICrossDomainMessenger { + address sender; + + constructor(address _sender) { + sender = _sender; + } + + function xDomainMessageSender() external view override returns (address) { + return sender; + } +}