diff --git a/.gas-snapshot b/.gas-snapshot index 848be72..34517c2 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,5 +1,21 @@ +AddEntryTest:test_AddEntry() (gas: 44369) +AddEntryTest:test_RevertWhen_EntryAlreadyExists() (gas: 42598) +AddEntryTest:test_RevertWhen_InvalidAddress() (gas: 25087) +AddEntryTest:test_RevertWhen_SenderIsNotTokenDeployer() (gas: 14804) CollectibleV1Test:test_Deployment() (gas: 38626) CommunityERC20Test:test_Deployment() (gas: 29720) +CommunityTokenDeployerTest:test_Deployment() (gas: 14794) +DeployTest:test_Deploy() (gas: 4820243) +DeployTest:test_Deployment() (gas: 14914) +DeployTest:test_RevertWhen_AlreadyDeployed() (gas: 4817349) +DeployTest:test_RevertWhen_DeploymentSignatureExpired() (gas: 54842) +DeployTest:test_RevertWhen_InvalidCommunityAddress() (gas: 52541) +DeployTest:test_RevertWhen_InvalidDeployerAddress() (gas: 56529) +DeployTest:test_RevertWhen_InvalidDeploymentSignature() (gas: 66863) +DeployTest:test_RevertWhen_InvalidSignerPublicKey() (gas: 54635) +DeployTest:test_RevertWhen_InvalidTokenMetadata() (gas: 70764) +DeploymentTest:test_Deployment() (gas: 17239) +GetEntryTest:test_ReturnZeroAddressIfEntryDoesNotExist() (gas: 11906) MintToTest:test_Deployment() (gas: 29742) MintToTest:test_Deployment() (gas: 38626) MintToTest:test_Deployment() (gas: 85415) @@ -15,6 +31,9 @@ RemoteBurnTest:test_Deployment() (gas: 85437) RemoteBurnTest:test_RemoteBurn() (gas: 455285) RemoteBurnTest:test_RevertWhen_RemoteBurn() (gas: 19499) RemoteBurnTest:test_RevertWhen_SenderIsNotOwner() (gas: 25211) +SetCommunityTokenDeployerAddressTest:test_RevertWhen_InvalidTokenDeployerAddress() (gas: 12963) +SetCommunityTokenDeployerAddressTest:test_RevertWhen_SenderIsNotOwner() (gas: 12504) +SetCommunityTokenDeployerAddressTest:test_SetCommunityTokenDeployerAddress() (gas: 22807) SetMaxSupplyTest:test_Deployment() (gas: 29720) SetMaxSupplyTest:test_Deployment() (gas: 85437) SetMaxSupplyTest:test_RevertWhen_CalledBecauseMaxSupplyIsLocked() (gas: 16521) diff --git a/contracts/CommunityOwnerTokenRegistry.sol b/contracts/CommunityOwnerTokenRegistry.sol new file mode 100644 index 0000000..496c21c --- /dev/null +++ b/contracts/CommunityOwnerTokenRegistry.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.17; + +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { IAddressRegistry } from "./interfaces/IAddressRegistry.sol"; +import { OwnerToken } from "./OwnerToken.sol"; + +/** + * @title CommunityOwnerTokenRegistry contract + * @author 0x-r4bbit + * + * This contract serves as a simple registry to map Status community addresses + * to Status community `OwnerToken` addresses. + * The `CommunityTokenDeployer` contract uses this registry contract to maintain + * a list of community address and their token addresses. + * @notice This contract will be deployed by Status similar to the `CommunityTokenDeployer` + * contract. + * @notice This contract maps community addresses to `OwnerToken` addresses. + * @notice Only one entry per community address can exist in the registry. + * @dev This registry has been extracted into its own contract so that it's possible + * to introduce different version of a `CommunityDeployerContract` without needing to + * migrate existing registry data, as the deployer contract would simply point at this + * registry contract. + * @dev Only `tokenDeployer` can add entries to the registry. + */ +contract CommunityOwnerTokenRegistry is IAddressRegistry, Ownable { + error CommunityOwnerTokenRegistry_InvalidTokenDeployerAddress(); + error CommunityOwnerTokenRegistry_NotAuthorized(); + error CommunityOwnerTokenRegistry_EntryAlreadyExists(); + error CommunityOwnerTokenRegistry_InvalidAddress(); + + event CommunityDeployerAddressChange(address indexed); + event AddEntry(address indexed, address indexed); + + /// @dev The address of the token deployer contract. + address public tokenDeployer; + + mapping(address => address) public communityAddressToTokenAddress; + + modifier onlyTokenDeployer() { + if (msg.sender != tokenDeployer) { + revert CommunityOwnerTokenRegistry_NotAuthorized(); + } + _; + } + + /** + * @notice Sets the address of the community token deployer contract. This is needed to + * ensure only the known token deployer contract can add new entries to the registry. + * @dev Only the owner of this contract can call this function. + * @dev Emits a {CommunityDeployerAddressChange} event. + * + * @param _tokenDeployer The address of the community token deployer contract + */ + function setCommunityTokenDeployerAddress(address _tokenDeployer) external onlyOwner { + if (_tokenDeployer == address(0)) { + revert CommunityOwnerTokenRegistry_InvalidTokenDeployerAddress(); + } + tokenDeployer = _tokenDeployer; + emit CommunityDeployerAddressChange(tokenDeployer); + } + + /** + * @notice Adds an entry to the registry. Only one entry per community address can exist. + * @dev Only the token deployer contract can call this function. + * @dev Reverts when the entry already exists. + * @dev Reverts when either `_communityAddress` or `_tokenAddress` are zero addresses. + * @dev Emits a {AddEntry} event. + */ + function addEntry(address _communityAddress, address _tokenAddress) external onlyTokenDeployer { + if (getEntry(_communityAddress) != address(0)) { + revert CommunityOwnerTokenRegistry_EntryAlreadyExists(); + } + + if (_communityAddress == address(0) || _tokenAddress == address(0)) { + revert CommunityOwnerTokenRegistry_InvalidAddress(); + } + + communityAddressToTokenAddress[_communityAddress] = _tokenAddress; + emit AddEntry(_communityAddress, _tokenAddress); + } + + /** + * @notice Returns the owner token address for a given community address. + * @param _communityAddress The community address to look up an owner token address. + * @return address The owner token address for the community addres, or zero address . + */ + function getEntry(address _communityAddress) public view returns (address) { + return communityAddressToTokenAddress[_communityAddress]; + } +} diff --git a/contracts/CommunityTokenDeployer.sol b/contracts/CommunityTokenDeployer.sol new file mode 100644 index 0000000..257fda6 --- /dev/null +++ b/contracts/CommunityTokenDeployer.sol @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.17; + +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { EIP712 } from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; +import { OwnerToken } from "./OwnerToken.sol"; +import { MasterToken } from "./MasterToken.sol"; +import { IAddressRegistry } from "./interfaces/IAddressRegistry.sol"; + +/** + * @title CommunityTokenDeployer contract + * @author 0x-r4bbit + * + * This contract serves as a deployment process for Status community owners + * to deploy access control token contracts on behalf of their Status community. + * The contract deploys the two token contracts `OwnerToken` and `MasterToken`. + * The contract maintains a registry which keeps track of `OwnerToken` contract + * addresses per community. + * + * Only one deployment per community can be done. + * Status community owners have to provide an EIP712 hash signature that was + * created using their community's private key to successfully execute a deployment. + * + * @notice This contract is used by Status community owners to deploy + * community access control token contracts. + * @notice This contract maintains a registry that tracks contract addresses + * and community addresses + * @dev This contract will be deployed by Status, making Status the owner + * of the contract. + * @dev A contract address for a `CommunityTokenRegistry` contract has to be provided + * to create this contract. + * @dev The `CommunityTokenRegistry` can be changed by the owner of this contract. + */ +contract CommunityTokenDeployer is EIP712("CommunityTokenDeployer", "1"), Ownable { + using ECDSA for bytes32; + + error CommunityTokenDeployer_InvalidDeploymentRegistryAddress(); + error CommunityTokenDeployer_AlreadyDeployed(); + error CommunityTokenDeployer_InvalidSignerKeyOrCommunityAddress(); + error CommunityTokenDeployer_InvalidTokenMetadata(); + error CommunityTokenDeployer_InvalidDeployerAddress(); + error CommunityTokenDeployer_InvalidDeploymentSignature(); + error CommunityTokenDeployer_DeploymentSignatureExpired(); + + /// @dev Needed to avoid "Stack too deep" error. + struct TokenConfig { + string name; + string symbol; + string baseURI; + } + + /// @dev Used to verify signatures. + struct DeploymentSignature { + address signer; + address deployer; + /// a `deadline` must be provided to avoid unlimited lifetimes + uint256 deadline; + uint8 v; + bytes32 r; + bytes32 s; + } + + bytes32 public constant DEPLOYMENT_SIGNATURE_TYPEHASH = + keccak256("Deploy(address signer,address deployer,uint256 deadline)"); + + address public deploymentRegistry; + + event Deploy(address indexed, address indexed); + + /// @param _registry The address of the `CommunityTokenRegistry` contract. + constructor(address _registry) { + if (_registry == address(0)) { + revert CommunityTokenDeployer_InvalidDeploymentRegistryAddress(); + } + deploymentRegistry = _registry; + } + + /** + * @notice Deploys an instance of `OwnerToken` and `MasterToken` on behalf + * of a Status community account, provided `_signature` is valid and was signed + * by that Status community account. + * @dev Anyone can call this function but a valid EIP712 hash signature has to be + * provided for a successful deployment. + * @dev Emits a {Deploy} event. + * + * @param _ownerToken A `TokenConfig` containing ERC721 metadata for `OwnerToken` + * @param _masterToken A `TokenConfig` containing ERC721 metadata for `MasterToken` + * @param _signature A `DeploymentSignature` containing a signer and deployer address, + * and a signature created by a Status community + * @return address The address of the deployed `OwnerToken` contract. + * @return address The address of the deployed `MasterToken` contract. + */ + function deploy( + TokenConfig calldata _ownerToken, + TokenConfig calldata _masterToken, + DeploymentSignature calldata _signature, + bytes memory _signerPublicKey + ) + external + returns (address, address) + { + if ( + bytes(_ownerToken.name).length == 0 || bytes(_ownerToken.symbol).length == 0 + || bytes(_ownerToken.baseURI).length == 0 || bytes(_masterToken.name).length == 0 + || bytes(_masterToken.symbol).length == 0 || bytes(_masterToken.baseURI).length == 0 + ) { + revert CommunityTokenDeployer_InvalidTokenMetadata(); + } + + if (_signature.signer == address(0) || _signerPublicKey.length == 0) { + revert CommunityTokenDeployer_InvalidSignerKeyOrCommunityAddress(); + } + + if (_signature.deadline < block.timestamp) { + revert CommunityTokenDeployer_DeploymentSignatureExpired(); + } + + if (_signature.deployer != msg.sender) { + revert CommunityTokenDeployer_InvalidDeployerAddress(); + } + + if (IAddressRegistry(deploymentRegistry).getEntry(_signature.signer) != address(0)) { + revert CommunityTokenDeployer_AlreadyDeployed(); + } + + if (!_verifySignature(_signature)) { + revert CommunityTokenDeployer_InvalidDeploymentSignature(); + } + + OwnerToken ownerToken = new OwnerToken( + _ownerToken.name, + _ownerToken.symbol, + _ownerToken.baseURI, + msg.sender, + _signerPublicKey + ); + + MasterToken masterToken = new MasterToken( + _masterToken.name, + _masterToken.symbol, + _masterToken.baseURI, + address(ownerToken) + ); + + IAddressRegistry(deploymentRegistry).addEntry(_signature.signer, address(ownerToken)); + emit Deploy(address(ownerToken), address(masterToken)); + + return (address(ownerToken), address(masterToken)); + } + + /** + * @notice Returns an EIP712 domain separator hash + * @return bytes32 An EIP712 domain separator hash + */ + function DOMAIN_SEPARATOR() external view returns (bytes32) { + return _domainSeparatorV4(); + } + + /** + * @notice Verifies provided `DeploymentSignature` which was created by + * the Status community account for which the access control token contracts + * will be deployed. + * @dev This contract does not maintain nonces for the typed data hash, which + * is typically done to prevent signature replay attacks. The `deploy()` function + * allows only one deployment per Status community, so replay attacks are not possible. + * @return bool Whether the provided signature could be recovered. + */ + function _verifySignature(DeploymentSignature calldata signature) internal view returns (bool) { + bytes32 digest = _hashTypedDataV4( + keccak256( + abi.encode(DEPLOYMENT_SIGNATURE_TYPEHASH, signature.signer, signature.deployer, signature.deadline) + ) + ); + return signature.signer == digest.recover(signature.v, signature.r, signature.s); + } +} diff --git a/contracts/OwnerToken.sol b/contracts/OwnerToken.sol index 4075707..debda8b 100644 --- a/contracts/OwnerToken.sol +++ b/contracts/OwnerToken.sol @@ -13,18 +13,14 @@ contract OwnerToken is BaseToken { string memory _name, string memory _symbol, string memory _baseTokenURI, - string memory _masterName, - string memory _masterSymbol, - string memory _masterBaseTokenURI, + address _receiver, bytes memory _signerPublicKey ) BaseToken(_name, _symbol, 1, false, true, _baseTokenURI, address(this), address(this)) { signerPublicKey = _signerPublicKey; - MasterToken masterToken = new MasterToken(_masterName, _masterSymbol, _masterBaseTokenURI, address(this)); - emit MasterTokenCreated(address(masterToken)); address[] memory addresses = new address[](1); - addresses[0] = msg.sender; + addresses[0] = _receiver; _mintTo(addresses); } diff --git a/contracts/interfaces/IAddressRegistry.sol b/contracts/interfaces/IAddressRegistry.sol new file mode 100644 index 0000000..c715b95 --- /dev/null +++ b/contracts/interfaces/IAddressRegistry.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.17; + +interface IAddressRegistry { + function addEntry(address, address) external; + function getEntry(address) external returns (address); +} diff --git a/script/DeployOwnerToken.s.sol b/script/DeployOwnerToken.s.sol index ddf9749..829756d 100644 --- a/script/DeployOwnerToken.s.sol +++ b/script/DeployOwnerToken.s.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.17; -import { Vm } from "forge-std/Vm.sol"; import { BaseScript } from "./Base.s.sol"; import { DeploymentConfig } from "./DeploymentConfig.s.sol"; import { OwnerToken } from "../contracts/OwnerToken.sol"; @@ -14,24 +13,21 @@ contract DeployOwnerToken is BaseScript { DeploymentConfig.TokenConfig memory ownerTokenConfig = deploymentConfig.getOwnerTokenConfig(); DeploymentConfig.TokenConfig memory masterTokenConfig = deploymentConfig.getMasterTokenConfig(); - vm.recordLogs(); vm.startBroadcast(broadcaster); OwnerToken ownerToken = new OwnerToken( - ownerTokenConfig.name, - ownerTokenConfig.symbol, - ownerTokenConfig.baseURI, - masterTokenConfig.name, - masterTokenConfig.symbol, - masterTokenConfig.baseURI, - ownerTokenConfig.signerPublicKey - ); + ownerTokenConfig.name, + ownerTokenConfig.symbol, + ownerTokenConfig.baseURI, + broadcaster, + ownerTokenConfig.signerPublicKey + ); - // Need to retrieve master token address from logs as - // we can't access it otherwise - Vm.Log[] memory entries = vm.getRecordedLogs(); - address masterTokenAddress = abi.decode(entries[0].data, (address)); - - MasterToken masterToken = MasterToken(masterTokenAddress); + MasterToken masterToken = new MasterToken( + masterTokenConfig.name, + masterTokenConfig.symbol, + masterTokenConfig.baseURI, + address(ownerToken) + ); vm.stopBroadcast(); return (ownerToken, masterToken, deploymentConfig); diff --git a/script/DeployTokenDeployer.s.sol b/script/DeployTokenDeployer.s.sol new file mode 100644 index 0000000..4596641 --- /dev/null +++ b/script/DeployTokenDeployer.s.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.17; + +import { BaseScript } from "./Base.s.sol"; +import { DeploymentConfig } from "./DeploymentConfig.s.sol"; +import { CommunityOwnerTokenRegistry } from "../contracts/CommunityOwnerTokenRegistry.sol"; +import { CommunityTokenDeployer } from "../contracts/CommunityTokenDeployer.sol"; + +contract DeployTokenDeployer is BaseScript { + function run() external returns (CommunityTokenDeployer, CommunityOwnerTokenRegistry, DeploymentConfig) { + DeploymentConfig deploymentConfig = new DeploymentConfig(broadcaster); + + vm.startBroadcast(broadcaster); + CommunityOwnerTokenRegistry tokenRegistry = new CommunityOwnerTokenRegistry(); + CommunityTokenDeployer tokenDeployer = new CommunityTokenDeployer(address(tokenRegistry)); + tokenRegistry.setCommunityTokenDeployerAddress(address(tokenDeployer)); + vm.stopBroadcast(); + + return (tokenDeployer, tokenRegistry, deploymentConfig); + } +} diff --git a/test/CommunityOwnerTokenRegistry.t.sol b/test/CommunityOwnerTokenRegistry.t.sol new file mode 100644 index 0000000..7db2f7e --- /dev/null +++ b/test/CommunityOwnerTokenRegistry.t.sol @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import { Test } from "forge-std/Test.sol"; +import { DeployTokenDeployer } from "../script/DeployTokenDeployer.s.sol"; +import { DeploymentConfig } from "../script/DeploymentConfig.s.sol"; +import { CommunityOwnerTokenRegistry } from "../contracts/CommunityOwnerTokenRegistry.sol"; +import { CommunityTokenDeployer } from "../contracts/CommunityTokenDeployer.sol"; + +contract CommunityOwnerTokenRegistryTest is Test { + event CommunityDeployerAddressChange(address indexed); + event AddEntry(address indexed, address indexed); + + DeploymentConfig internal deploymentConfig; + + CommunityTokenDeployer internal tokenDeployer; + + CommunityOwnerTokenRegistry internal tokenRegistry; + + address internal deployer; + + address internal TOKEN_DEPLOYER = makeAddr("tokenDeployer"); + + address internal communityAddress = makeAddr("communityAddress"); + + address internal tokenAddress = makeAddr("tokenAddress"); + + function setUp() public virtual { + DeployTokenDeployer deployment = new DeployTokenDeployer(); + (tokenDeployer, tokenRegistry, deploymentConfig) = deployment.run(); + deployer = deploymentConfig.deployer(); + } +} + +contract DeploymentTest is CommunityOwnerTokenRegistryTest { + function setUp() public virtual override { + CommunityOwnerTokenRegistryTest.setUp(); + } + + function test_Deployment() public { + assertEq(tokenDeployer.owner(), deployer); + assertEq(tokenRegistry.tokenDeployer(), address(tokenDeployer)); + } +} + +contract SetCommunityTokenDeployerAddressTest is CommunityOwnerTokenRegistryTest { + function setUp() public virtual override { + CommunityOwnerTokenRegistryTest.setUp(); + } + + function test_RevertWhen_SenderIsNotOwner() public { + vm.expectRevert(bytes("Ownable: caller is not the owner")); + tokenRegistry.setCommunityTokenDeployerAddress(makeAddr("someAddress")); + } + + function test_RevertWhen_InvalidTokenDeployerAddress() public { + vm.prank(deployer); + vm.expectRevert(CommunityOwnerTokenRegistry.CommunityOwnerTokenRegistry_InvalidTokenDeployerAddress.selector); + tokenRegistry.setCommunityTokenDeployerAddress(address(0)); + } + + function test_SetCommunityTokenDeployerAddress() public { + address newAddress = makeAddr("someAddress"); + vm.prank(deployer); + vm.expectEmit(true, true, true, false); + emit CommunityDeployerAddressChange(newAddress); + tokenRegistry.setCommunityTokenDeployerAddress(newAddress); + assertEq(tokenRegistry.tokenDeployer(), newAddress); + } +} + +contract AddEntryTest is CommunityOwnerTokenRegistryTest { + function setUp() public virtual override { + CommunityOwnerTokenRegistryTest.setUp(); + vm.prank(deployer); + tokenRegistry.setCommunityTokenDeployerAddress(TOKEN_DEPLOYER); + } + + function test_RevertWhen_SenderIsNotTokenDeployer() public { + vm.expectRevert(CommunityOwnerTokenRegistry.CommunityOwnerTokenRegistry_NotAuthorized.selector); + tokenRegistry.addEntry(communityAddress, tokenAddress); + } + + function test_RevertWhen_InvalidAddress() public { + vm.startPrank(TOKEN_DEPLOYER); + vm.expectRevert(CommunityOwnerTokenRegistry.CommunityOwnerTokenRegistry_InvalidAddress.selector); + tokenRegistry.addEntry(address(0), tokenAddress); + vm.expectRevert(CommunityOwnerTokenRegistry.CommunityOwnerTokenRegistry_InvalidAddress.selector); + tokenRegistry.addEntry(communityAddress, address(0)); + } + + function test_RevertWhen_EntryAlreadyExists() public { + vm.startPrank(TOKEN_DEPLOYER); + tokenRegistry.addEntry(communityAddress, tokenAddress); + vm.expectRevert(CommunityOwnerTokenRegistry.CommunityOwnerTokenRegistry_EntryAlreadyExists.selector); + tokenRegistry.addEntry(communityAddress, tokenAddress); + } + + function test_AddEntry() public { + vm.startPrank(TOKEN_DEPLOYER); + vm.expectEmit(true, true, true, true); + emit AddEntry(communityAddress, tokenAddress); + tokenRegistry.addEntry(communityAddress, tokenAddress); + + assertEq(tokenRegistry.getEntry(communityAddress), tokenAddress); + } +} + +contract GetEntryTest is CommunityOwnerTokenRegistryTest { + function setUp() public virtual override { + CommunityOwnerTokenRegistryTest.setUp(); + } + + function test_ReturnZeroAddressIfEntryDoesNotExist() public { + assertEq(tokenRegistry.getEntry(makeAddr("someAddress")), address(0)); + } +} diff --git a/test/CommunityTokenDeployer.t.sol b/test/CommunityTokenDeployer.t.sol new file mode 100644 index 0000000..5adde33 --- /dev/null +++ b/test/CommunityTokenDeployer.t.sol @@ -0,0 +1,202 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import { Test } from "forge-std/Test.sol"; +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { DeployTokenDeployer } from "../script/DeployTokenDeployer.s.sol"; +import { DeploymentConfig } from "../script/DeploymentConfig.s.sol"; +import { OwnerToken } from "../contracts/OwnerToken.sol"; +import { MasterToken } from "../contracts/MasterToken.sol"; +import { CommunityOwnerTokenRegistry } from "../contracts/CommunityOwnerTokenRegistry.sol"; +import { CommunityTokenDeployer } from "../contracts/CommunityTokenDeployer.sol"; + +contract CommunityTokenDeployerTest is Test { + DeploymentConfig internal deploymentConfig; + + CommunityTokenDeployer internal tokenDeployer; + CommunityOwnerTokenRegistry internal tokenRegistry; + + address internal deployer; + + address internal immutable OWNER = makeAddr("owner"); + + address internal communityAddress; + uint256 internal communityKey; + + function setUp() public virtual { + DeployTokenDeployer deployment = new DeployTokenDeployer(); + (tokenDeployer, tokenRegistry, deploymentConfig) = deployment.run(); + deployer = deploymentConfig.deployer(); + (communityAddress, communityKey) = makeAddrAndKey("community"); + } + + function test_Deployment() public { + assertEq(tokenDeployer.deploymentRegistry(), address(tokenRegistry)); + assertEq(tokenDeployer.owner(), deployer); + } + + function _getOwnerTokenConfig() internal view returns (CommunityTokenDeployer.TokenConfig memory, bytes memory) { + ( + string memory ownerTokenName, + string memory ownerTokenSymbol, + string memory ownerTokenBaseURI, + bytes memory signerPublicKey + ) = deploymentConfig.ownerTokenConfig(); + + CommunityTokenDeployer.TokenConfig memory ownerTokenConfig = + CommunityTokenDeployer.TokenConfig(ownerTokenName, ownerTokenSymbol, ownerTokenBaseURI); + return (ownerTokenConfig, signerPublicKey); + } + + function _getMasterTokenConfig() internal view returns (CommunityTokenDeployer.TokenConfig memory) { + (string memory masterTokenName, string memory masterTokenSymbol, string memory masterTokenBaseURI,) = + deploymentConfig.masterTokenConfig(); + + CommunityTokenDeployer.TokenConfig memory masterTokenConfig = + CommunityTokenDeployer.TokenConfig(masterTokenName, masterTokenSymbol, masterTokenBaseURI); + return masterTokenConfig; + } + + function _createDeploymentSignature( + uint256 _signerKey, + address _signer, + address _deployer, + uint256 _deadline + ) + internal + view + returns (CommunityTokenDeployer.DeploymentSignature memory) + { + bytes32 digest = ECDSA.toTypedDataHash( + tokenDeployer.DOMAIN_SEPARATOR(), + keccak256(abi.encode(tokenDeployer.DEPLOYMENT_SIGNATURE_TYPEHASH(), _signer, _deployer, _deadline)) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_signerKey, digest); + return CommunityTokenDeployer.DeploymentSignature(_signer, _deployer, _deadline, v, r, s); + } +} + +contract DeployTest is CommunityTokenDeployerTest { + function setUp() public virtual override { + CommunityTokenDeployerTest.setUp(); + } + + function test_RevertWhen_InvalidDeployerAddress() public { + (CommunityTokenDeployer.TokenConfig memory ownerTokenConfig, bytes memory signerPublicKey) = + _getOwnerTokenConfig(); + CommunityTokenDeployer.TokenConfig memory masterTokenConfig = _getMasterTokenConfig(); + CommunityTokenDeployer.DeploymentSignature memory signature = + _createDeploymentSignature(communityKey, communityAddress, makeAddr("someone else"), block.timestamp); + vm.prank(OWNER); + vm.expectRevert(CommunityTokenDeployer.CommunityTokenDeployer_InvalidDeployerAddress.selector); + tokenDeployer.deploy(ownerTokenConfig, masterTokenConfig, signature, signerPublicKey); + } + + function test_RevertWhen_InvalidDeploymentSignature() public { + (CommunityTokenDeployer.TokenConfig memory ownerTokenConfig, bytes memory signerPublicKey) = + _getOwnerTokenConfig(); + CommunityTokenDeployer.TokenConfig memory masterTokenConfig = _getMasterTokenConfig(); + CommunityTokenDeployer.DeploymentSignature memory signature = + _createDeploymentSignature(communityKey, makeAddr("invalid address"), OWNER, block.timestamp); + vm.prank(OWNER); + vm.expectRevert(CommunityTokenDeployer.CommunityTokenDeployer_InvalidDeploymentSignature.selector); + tokenDeployer.deploy(ownerTokenConfig, masterTokenConfig, signature, signerPublicKey); + } + + function test_RevertWhen_DeploymentSignatureExpired() public { + (CommunityTokenDeployer.TokenConfig memory ownerTokenConfig, bytes memory signerPublicKey) = + _getOwnerTokenConfig(); + CommunityTokenDeployer.TokenConfig memory masterTokenConfig = _getMasterTokenConfig(); + CommunityTokenDeployer.DeploymentSignature memory signature = + _createDeploymentSignature(communityKey, communityAddress, OWNER, block.timestamp - 1); + vm.prank(OWNER); + vm.expectRevert(CommunityTokenDeployer.CommunityTokenDeployer_DeploymentSignatureExpired.selector); + tokenDeployer.deploy(ownerTokenConfig, masterTokenConfig, signature, signerPublicKey); + } + + function test_RevertWhen_InvalidTokenMetadata() public { + (, bytes memory signerPublicKey) = _getOwnerTokenConfig(); + CommunityTokenDeployer.TokenConfig memory ownerTokenConfig = CommunityTokenDeployer.TokenConfig("", "", ""); + CommunityTokenDeployer.TokenConfig memory masterTokenConfig = CommunityTokenDeployer.TokenConfig("", "", ""); + CommunityTokenDeployer.DeploymentSignature memory signature = + _createDeploymentSignature(communityKey, communityAddress, OWNER, block.timestamp); + + vm.prank(OWNER); + vm.expectRevert(CommunityTokenDeployer.CommunityTokenDeployer_InvalidTokenMetadata.selector); + tokenDeployer.deploy(ownerTokenConfig, masterTokenConfig, signature, signerPublicKey); + + // fill `masterTokenConfig` with data + masterTokenConfig = _getMasterTokenConfig(); + + vm.prank(OWNER); + vm.expectRevert(CommunityTokenDeployer.CommunityTokenDeployer_InvalidTokenMetadata.selector); + tokenDeployer.deploy(ownerTokenConfig, masterTokenConfig, signature, signerPublicKey); + + // fill `ownerTokenConfig` with data and reset `masterTokenConfig` + (ownerTokenConfig,) = _getOwnerTokenConfig(); + masterTokenConfig = CommunityTokenDeployer.TokenConfig("", "", ""); + + vm.prank(OWNER); + vm.expectRevert(CommunityTokenDeployer.CommunityTokenDeployer_InvalidTokenMetadata.selector); + tokenDeployer.deploy(ownerTokenConfig, masterTokenConfig, signature, signerPublicKey); + } + + function test_RevertWhen_InvalidSignerPublicKey() public { + (CommunityTokenDeployer.TokenConfig memory ownerTokenConfig,) = _getOwnerTokenConfig(); + CommunityTokenDeployer.TokenConfig memory masterTokenConfig = _getMasterTokenConfig(); + CommunityTokenDeployer.DeploymentSignature memory signature = + _createDeploymentSignature(communityKey, communityAddress, OWNER, block.timestamp); + + vm.prank(OWNER); + vm.expectRevert(CommunityTokenDeployer.CommunityTokenDeployer_InvalidSignerKeyOrCommunityAddress.selector); + tokenDeployer.deploy(ownerTokenConfig, masterTokenConfig, signature, bytes("")); + } + + function test_RevertWhen_InvalidCommunityAddress() public { + (CommunityTokenDeployer.TokenConfig memory ownerTokenConfig, bytes memory signerPublicKey) = + _getOwnerTokenConfig(); + CommunityTokenDeployer.TokenConfig memory masterTokenConfig = _getMasterTokenConfig(); + CommunityTokenDeployer.DeploymentSignature memory signature = + _createDeploymentSignature(communityKey, address(0), OWNER, block.timestamp); + + vm.prank(OWNER); + vm.expectRevert(CommunityTokenDeployer.CommunityTokenDeployer_InvalidSignerKeyOrCommunityAddress.selector); + tokenDeployer.deploy(ownerTokenConfig, masterTokenConfig, signature, signerPublicKey); + } + + function test_RevertWhen_AlreadyDeployed() public { + (CommunityTokenDeployer.TokenConfig memory ownerTokenConfig, bytes memory signerPublicKey) = + _getOwnerTokenConfig(); + CommunityTokenDeployer.TokenConfig memory masterTokenConfig = _getMasterTokenConfig(); + CommunityTokenDeployer.DeploymentSignature memory signature = + _createDeploymentSignature(communityKey, communityAddress, OWNER, block.timestamp); + + vm.startPrank(OWNER); + tokenDeployer.deploy(ownerTokenConfig, masterTokenConfig, signature, signerPublicKey); + + vm.expectRevert(CommunityTokenDeployer.CommunityTokenDeployer_AlreadyDeployed.selector); + tokenDeployer.deploy(ownerTokenConfig, masterTokenConfig, signature, signerPublicKey); + } + + function test_Deploy() public { + (CommunityTokenDeployer.TokenConfig memory ownerTokenConfig, bytes memory signerPublicKey) = + _getOwnerTokenConfig(); + CommunityTokenDeployer.TokenConfig memory masterTokenConfig = _getMasterTokenConfig(); + CommunityTokenDeployer.DeploymentSignature memory signature = + _createDeploymentSignature(communityKey, communityAddress, OWNER, block.timestamp); + + vm.prank(OWNER); + (address ownerTokenAddress, address masterTokenAddress) = + tokenDeployer.deploy(ownerTokenConfig, masterTokenConfig, signature, signerPublicKey); + + assertEq(ownerTokenAddress, tokenRegistry.getEntry(communityAddress)); + assertEq(OwnerToken(ownerTokenAddress).balanceOf(OWNER), 1); + + MasterToken masterToken = MasterToken(masterTokenAddress); + + assertEq(masterToken.ownerToken(), ownerTokenAddress); + assertEq(masterToken.balanceOf(OWNER), 0); + assertEq(masterToken.remoteBurnable(), true); + assertEq(masterToken.transferable(), false); + } +}