diff --git a/.gas-snapshot b/.gas-snapshot index 848be72..57cdd8a 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,27 +1,77 @@ +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: 14805) +CreateTest:test_Create() (gas: 2252539) +CreateTest:test_Create() (gas: 2517349) +CreateTest:test_RevertWhen_InvalidOwnerTokenAddress() (gas: 15523) +CreateTest:test_RevertWhen_InvalidReceiverAddress() (gas: 15656) +CreateTest:test_RevertWhen_InvalidSignerPublicKey() (gas: 17057) +CreateTest:test_RevertWhen_InvalidTokenMetadata() (gas: 27936) +CreateTest:test_RevertWhen_InvalidTokenMetadata() (gas: 28173) +CreateTest:test_RevertWhen_SenderIsNotTokenDeployer() (gas: 16421) +CreateTest:test_RevertWhen_SenderIsNotTokenDeployer() (gas: 16524) +DeployContracts:test() (gas: 120) +DeployOwnerAndMasterToken:test() (gas: 120) +DeployTest:test_Deploy() (gas: 4840232) +DeployTest:test_Deployment() (gas: 14925) +DeployTest:test_RevertWhen_AlreadyDeployed() (gas: 4836115) +DeployTest:test_RevertWhen_DeploymentSignatureExpired() (gas: 53642) +DeployTest:test_RevertWhen_InvalidCommunityAddress() (gas: 51341) +DeployTest:test_RevertWhen_InvalidDeployerAddress() (gas: 55329) +DeployTest:test_RevertWhen_InvalidDeploymentSignature() (gas: 65663) +DeployTest:test_RevertWhen_InvalidSignerPublicKey() (gas: 53435) +DeployTest:test_RevertWhen_InvalidTokenMetadata() (gas: 2639807) +DeploymentTest:test_Deployment() (gas: 14671) +DeploymentTest:test_Deployment() (gas: 14671) +DeploymentTest:test_Deployment() (gas: 17250) +GetEntryTest:test_ReturnZeroAddressIfEntryDoesNotExist() (gas: 11906) MintToTest:test_Deployment() (gas: 29742) MintToTest:test_Deployment() (gas: 38626) -MintToTest:test_Deployment() (gas: 85415) -MintToTest:test_MintTo() (gas: 509977) +MintToTest:test_Deployment() (gas: 85460) +MintToTest:test_MintTo() (gas: 509082) MintToTest:test_RevertWhen_AddressesAndAmountsAreNotEqualLength() (gas: 24193) MintToTest:test_RevertWhen_MaxSupplyIsReached() (gas: 23267) MintToTest:test_RevertWhen_MaxSupplyIsReached() (gas: 505463) MintToTest:test_RevertWhen_MaxSupplyReached() (gas: 123426) MintToTest:test_RevertWhen_SenderIsNotOwner() (gas: 36358) -OwnerTokenTest:test_Deployment() (gas: 85415) +OwnerTokenTest:test_Deployment() (gas: 85460) RemoteBurnTest:test_Deployment() (gas: 38626) -RemoteBurnTest:test_Deployment() (gas: 85437) +RemoteBurnTest:test_Deployment() (gas: 85482) 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) +SetDeploymentRegistryAddressTest:test_Deployment() (gas: 14827) +SetDeploymentRegistryAddressTest:test_RevertWhen_InvalidDeploymentRegistryAddress() (gas: 13014) +SetDeploymentRegistryAddressTest:test_RevertWhen_SenderIsNotOwner() (gas: 12508) +SetDeploymentRegistryAddressTest:test_SetDeploymentRegistryAddress() (gas: 22903) +SetMasterTokenFactoryAddressTest:test_Deployment() (gas: 14805) +SetMasterTokenFactoryAddressTest:test_RevertWhen_InvalidTokenFactoryAddress() (gas: 13014) +SetMasterTokenFactoryAddressTest:test_RevertWhen_SenderIsNotOwner() (gas: 12487) +SetMasterTokenFactoryAddressTest:test_SetOwnerTokenFactoryAddress() (gas: 22861) SetMaxSupplyTest:test_Deployment() (gas: 29720) -SetMaxSupplyTest:test_Deployment() (gas: 85437) +SetMaxSupplyTest:test_Deployment() (gas: 85482) SetMaxSupplyTest:test_RevertWhen_CalledBecauseMaxSupplyIsLocked() (gas: 16521) SetMaxSupplyTest:test_RevertWhen_MaxSupplyLowerThanTotalSupply() (gas: 149095) -SetMaxSupplyTest:test_RevertWhen_SenderIsNotOwner() (gas: 12852) +SetMaxSupplyTest:test_RevertWhen_SenderIsNotOwner() (gas: 12823) SetMaxSupplyTest:test_RevertWhen_SenderIsNotOwner() (gas: 17335) SetMaxSupplyTest:test_SetMaxSupply() (gas: 15597) -SetSignerPublicKeyTest:test_Deployment() (gas: 85415) +SetOwnerTokenFactoryAddressTest:test_Deployment() (gas: 14805) +SetOwnerTokenFactoryAddressTest:test_RevertWhen_InvalidTokenFactoryAddress() (gas: 12992) +SetOwnerTokenFactoryAddressTest:test_RevertWhen_SenderIsNotOwner() (gas: 12465) +SetOwnerTokenFactoryAddressTest:test_SetOwnerTokenFactoryAddress() (gas: 22862) +SetSignerPublicKeyTest:test_Deployment() (gas: 85460) SetSignerPublicKeyTest:test_RevertWhen_SenderIsNotOwner() (gas: 18036) -SetSignerPublicKeyTest:test_SetSignerPublicKey() (gas: 26357) \ No newline at end of file +SetSignerPublicKeyTest:test_SetSignerPublicKey() (gas: 26357) +SetTokenDeployerAddressTest:test_RevertWhen_InvalidTokenDeployerAddress() (gas: 12964) +SetTokenDeployerAddressTest:test_RevertWhen_InvalidTokenDeployerAddress() (gas: 12964) +SetTokenDeployerAddressTest:test_RevertWhen_SenderIsNotOwner() (gas: 12438) +SetTokenDeployerAddressTest:test_RevertWhen_SenderIsNotOwner() (gas: 12438) +SetTokenDeployerAddressTest:test_SetTokenDeployerAddress() (gas: 22768) +SetTokenDeployerAddressTest:test_SetTokenDeployerAddress() (gas: 22768) \ No newline at end of file diff --git a/contracts/CommunityOwnerTokenRegistry.sol b/contracts/CommunityOwnerTokenRegistry.sol new file mode 100644 index 0000000..45bbe7b --- /dev/null +++ b/contracts/CommunityOwnerTokenRegistry.sol @@ -0,0 +1,91 @@ +// 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 "./tokens/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_NotAuthorized(); + error CommunityOwnerTokenRegistry_EntryAlreadyExists(); + error CommunityOwnerTokenRegistry_InvalidAddress(); + + event TokenDeployerAddressChange(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 {TokenDeployerAddressChange} event. + * + * @param _tokenDeployer The address of the community token deployer contract + */ + function setCommunityTokenDeployerAddress(address _tokenDeployer) external onlyOwner { + if (_tokenDeployer == address(0)) { + revert CommunityOwnerTokenRegistry_InvalidAddress(); + } + tokenDeployer = _tokenDeployer; + emit TokenDeployerAddressChange(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..791ef12 --- /dev/null +++ b/contracts/CommunityTokenDeployer.sol @@ -0,0 +1,225 @@ +// 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 { ITokenFactory } from "./interfaces/ITokenFactory.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 keep a reference to token factories that are used for deploying tokens. + * The contract deploys the two token contracts `OwnerToken` and `MasterToken` those factories. + * 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 A contract address for a `CommunityOwnerTokenFactory` contract has to be provided + * to create this contract. + * @dev A contract address for a `CommunityMasterTokenFactory` contract has to be provided + * to create this contract. + * @dev The `CommunityTokenRegistry` address can be changed by the owner of this contract. + * @dev The `CommunityOwnerTokenFactory` address can be changed by the owner of this contract. + * @dev The `CommunityMasterTokenFactory` address 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_InvalidTokenFactoryAddress(); + error CommunityTokenDeployer_EqualFactoryAddresses(); + error CommunityTokenDeployer_AlreadyDeployed(); + error CommunityTokenDeployer_InvalidSignerKeyOrCommunityAddress(); + error CommunityTokenDeployer_InvalidTokenMetadata(); + error CommunityTokenDeployer_InvalidDeployerAddress(); + error CommunityTokenDeployer_InvalidDeploymentSignature(); + + event OwnerTokenFactoryAddressChange(address indexed); + event MasterTokenFactoryAddressChange(address indexed); + event DeploymentRegistryAddressChange(address indexed); + + /// @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; + uint8 v; + bytes32 r; + bytes32 s; + } + + bytes32 public constant DEPLOYMENT_SIGNATURE_TYPEHASH = keccak256("Deploy(address signer,address deployer)"); + + /// @dev Address of the `CommunityTokenRegistry` contract instance. + address public deploymentRegistry; + + /// @dev Address of the `CommunityOwnerTokenFactory` contract instance. + address public ownerTokenFactory; + + /// @dev Address of the `CommunityMasterTokenFactory` contract instance. + address public masterTokenFactory; + + /// @param _registry The address of the `CommunityTokenRegistry` contract. + /// @param _ownerTokenFactory The address of the `CommunityOwnerTokenFactory` contract. + /// @param _masterTokenFactory The address of the `CommunityMasterTokenFactory` contract. + constructor(address _registry, address _ownerTokenFactory, address _masterTokenFactory) { + if (_registry == address(0)) { + revert CommunityTokenDeployer_InvalidDeploymentRegistryAddress(); + } + if (_ownerTokenFactory == address(0) || _masterTokenFactory == address(0)) { + revert CommunityTokenDeployer_InvalidTokenFactoryAddress(); + } + if (_ownerTokenFactory == _masterTokenFactory) { + revert CommunityTokenDeployer_EqualFactoryAddresses(); + } + deploymentRegistry = _registry; + ownerTokenFactory = _ownerTokenFactory; + masterTokenFactory = _masterTokenFactory; + } + + /** + * @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, using the configured factory contracts. + * @dev Anyone can call this function but a valid EIP712 hash signature has to be + * provided for a successful deployment. + * @dev Emits {CreateToken} events via underlying token factories. + * + * @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 (_signature.signer == address(0) || _signerPublicKey.length == 0) { + revert CommunityTokenDeployer_InvalidSignerKeyOrCommunityAddress(); + } + + 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(); + } + + address ownerToken = ITokenFactory(ownerTokenFactory).create( + _ownerToken.name, _ownerToken.symbol, _ownerToken.baseURI, msg.sender, _signerPublicKey + ); + + address masterToken = ITokenFactory(masterTokenFactory).create( + _masterToken.name, _masterToken.symbol, _masterToken.baseURI, ownerToken, bytes("") + ); + + IAddressRegistry(deploymentRegistry).addEntry(_signature.signer, ownerToken); + return (ownerToken, masterToken); + } + + /** + * @notice Sets a deployment registry address. + * @dev Only the owner can call this function. + * @dev Emits a {DeploymentRegistryAddressChange} event. + * @dev Reverts if the provided address is a zero address. + * + * @param _deploymentRegistry The address of the deployment registry contract. + */ + function setDeploymentRegistryAddress(address _deploymentRegistry) external onlyOwner { + if (_deploymentRegistry == address(0)) { + revert CommunityTokenDeployer_InvalidDeploymentRegistryAddress(); + } + deploymentRegistry = _deploymentRegistry; + emit DeploymentRegistryAddressChange(deploymentRegistry); + } + + /** + * @notice Sets the `OwnerToken` factory contract address. + * @dev Only the owner can call this function. + * @dev Emits a {OwnerTokenFactoryChange} event. + * @dev Reverts if the provided address is a zero address. + * + * @param _ownerTokenFactory The address of the `OwnerToken` factory contract. + */ + function setOwnerTokenFactoryAddress(address _ownerTokenFactory) external onlyOwner { + if (_ownerTokenFactory == address(0)) { + revert CommunityTokenDeployer_InvalidTokenFactoryAddress(); + } + ownerTokenFactory = _ownerTokenFactory; + emit OwnerTokenFactoryAddressChange(ownerTokenFactory); + } + + /** + * @notice Sets the `MasterToken` factory contract address. + * @dev Only the owner can call this function. + * @dev Emits a {MasterTokenFactoryChange} event. + * @dev Reverts if the provided address is a zero address. + * + * @param _masterTokenFactory The address of the `MasterToken` factory contract. + */ + function setMasterTokenFactoryAddress(address _masterTokenFactory) external onlyOwner { + if (_masterTokenFactory == address(0)) { + revert CommunityTokenDeployer_InvalidTokenFactoryAddress(); + } + masterTokenFactory = _masterTokenFactory; + emit MasterTokenFactoryAddressChange(masterTokenFactory); + } + + /** + * @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))); + return signature.signer == digest.recover(signature.v, signature.r, signature.s); + } +} diff --git a/contracts/factories/BaseTokenFactory.sol b/contracts/factories/BaseTokenFactory.sol new file mode 100644 index 0000000..e3cb3a8 --- /dev/null +++ b/contracts/factories/BaseTokenFactory.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import { ITokenFactory } from "../interfaces/ITokenFactory.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; + +/** + * @title BaseTokenFactory contract + * @author 0x-r4bbit + * + * This contract provides shared functionality across token factory contracts + * that are used to create instances of `OwnerToken` and `MasterToken`. + * This includes a custom modifiers as well as a function to set the token deployer + * address that is needed for it. + * + * @dev Other factory contract inherit from this contract. + */ +abstract contract BaseTokenFactory is ITokenFactory, Ownable { + error BaseTokenFactory_InvalidTokenDeployerAddress(); + error BaseTokenFactory_NotAuthorized(); + error BaseTokenFactory_InvalidTokenMetadata(); + + event TokenDeployerAddressChange(address indexed); + + /// @dev The address of the token deployer contract. + address public tokenDeployer; + + modifier onlyTokenDeployer() { + if (msg.sender != tokenDeployer) { + revert BaseTokenFactory_NotAuthorized(); + } + _; + } + + modifier onlyValidTokenMetadata(string calldata name, string calldata symbol, string calldata baseURI) { + if (bytes(name).length == 0 || bytes(symbol).length == 0 || bytes(baseURI).length == 0) { + revert BaseTokenFactory_InvalidTokenMetadata(); + } + _; + } + + /** + * @notice Sets the token deployer address. + * @dev Only the owner can call this function. + * @dev Reverts if provided address is a zero address. + * @dev Emits a {TokenDeployerAddressChange} event. + * @param _tokenDeployer The address of the token deployer contract. + */ + function setTokenDeployerAddress(address _tokenDeployer) external onlyOwner { + if (_tokenDeployer == address(0)) { + revert BaseTokenFactory_InvalidTokenDeployerAddress(); + } + tokenDeployer = _tokenDeployer; + emit TokenDeployerAddressChange(tokenDeployer); + } +} diff --git a/contracts/factories/CommunityMasterTokenFactory.sol b/contracts/factories/CommunityMasterTokenFactory.sol new file mode 100644 index 0000000..beb8298 --- /dev/null +++ b/contracts/factories/CommunityMasterTokenFactory.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import { BaseTokenFactory } from "./BaseTokenFactory.sol"; +import { MasterToken } from "../tokens/MasterToken.sol"; + +/** + * @title CommunityMasterTokenFactory contract + * @author 0x-r4bbit + * + * @notice This contract creates instances of `MasterToken`. + * @dev This contract inherits `BaseTokenFactory` to get access to + * shared modifiers and other functions. + */ +contract CommunityMasterTokenFactory is BaseTokenFactory { + error CommunityMasterTokenFactory_InvalidOwnerTokenAddress(); + + event CreateToken(address indexed); + + /** + * @notice Creates an instance of `MasterToken`. + * @dev Only the token deployer contract can call this function. + * @dev Emits a {CreateToken} event. + * @param _name The name of the `MasterToken`. + * @param _symbol The symbol of the `MasterToken`. + * @param _baseURI The base token URI of the `MasterToken`. + * @param _ownerToken The address of the `OwnerToken`. + * @return address The address of the created `MasterToken` instance. + */ + function create( + string calldata _name, + string calldata _symbol, + string calldata _baseURI, + address _ownerToken, + bytes memory + ) + external + onlyTokenDeployer + onlyValidTokenMetadata(_name, _symbol, _baseURI) + returns (address) + { + if (_ownerToken == address(0)) { + revert CommunityMasterTokenFactory_InvalidOwnerTokenAddress(); + } + + MasterToken masterToken = new MasterToken( + _name, + _symbol, + _baseURI, + _ownerToken + ); + emit CreateToken(address(masterToken)); + return address(masterToken); + } +} diff --git a/contracts/factories/CommunityOwnerTokenFactory.sol b/contracts/factories/CommunityOwnerTokenFactory.sol new file mode 100644 index 0000000..1929073 --- /dev/null +++ b/contracts/factories/CommunityOwnerTokenFactory.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import { BaseTokenFactory } from "./BaseTokenFactory.sol"; +import { OwnerToken } from "../tokens/OwnerToken.sol"; + +/** + * @title CommunityOwnerTokenFactory contract + * @author 0x-r4bbit + * + * @notice This contract creates instances of `OwnerToken`. + * @dev This contract inherits `BaseTokenFactory` to get access to + * shared modifiers and other functions. + */ +contract CommunityOwnerTokenFactory is BaseTokenFactory { + error CommunityOwnerTokenFactory_InvalidReceiverAddress(); + error CommunityOwnerTokenFactory_InvalidSignerPublicKey(); + + event CreateToken(address indexed); + + /** + * @notice Creates an instance of `OwnerToken`. + * @dev Only the token deployer contract can call this function. + * @dev Emits a {CreateToken} event. + * @param _name The name of the `OwnerToken`. + * @param _symbol The symbol of the `OwnerToken`. + * @param _baseURI The base token URI of the `OwnerToken`. + * @param _receiver The address of the token owner. + * @param _signerPublicKey The public key of the trusted signer of the community + * that the `OwnerToken` instance belongs to. + * @return address The address of the created `OwnerToken` instance. + */ + function create( + string calldata _name, + string calldata _symbol, + string calldata _baseURI, + address _receiver, + bytes memory _signerPublicKey + ) + external + onlyTokenDeployer + onlyValidTokenMetadata(_name, _symbol, _baseURI) + returns (address) + { + if (_receiver == address(0)) { + revert CommunityOwnerTokenFactory_InvalidReceiverAddress(); + } + + if (_signerPublicKey.length == 0) { + revert CommunityOwnerTokenFactory_InvalidSignerPublicKey(); + } + + OwnerToken ownerToken = new OwnerToken( + _name, + _symbol, + _baseURI, + _receiver, + _signerPublicKey + ); + emit CreateToken(address(ownerToken)); + return address(ownerToken); + } +} 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/contracts/interfaces/ITokenFactory.sol b/contracts/interfaces/ITokenFactory.sol new file mode 100644 index 0000000..fb9c402 --- /dev/null +++ b/contracts/interfaces/ITokenFactory.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.17; + +interface ITokenFactory { + function create( + string calldata, + string calldata, + string calldata, + address, + bytes calldata + ) + external + returns (address); + function setTokenDeployerAddress(address) external; +} diff --git a/contracts/BaseToken.sol b/contracts/tokens/BaseToken.sol similarity index 100% rename from contracts/BaseToken.sol rename to contracts/tokens/BaseToken.sol diff --git a/contracts/CollectibleV1.sol b/contracts/tokens/CollectibleV1.sol similarity index 100% rename from contracts/CollectibleV1.sol rename to contracts/tokens/CollectibleV1.sol diff --git a/contracts/CommunityERC20.sol b/contracts/tokens/CommunityERC20.sol similarity index 100% rename from contracts/CommunityERC20.sol rename to contracts/tokens/CommunityERC20.sol diff --git a/contracts/MasterToken.sol b/contracts/tokens/MasterToken.sol similarity index 100% rename from contracts/MasterToken.sol rename to contracts/tokens/MasterToken.sol diff --git a/contracts/OwnerToken.sol b/contracts/tokens/OwnerToken.sol similarity index 67% rename from contracts/OwnerToken.sol rename to contracts/tokens/OwnerToken.sol index 4075707..49ee7b5 100644 --- a/contracts/OwnerToken.sol +++ b/contracts/tokens/OwnerToken.sol @@ -2,29 +2,22 @@ pragma solidity ^0.8.17; import "./BaseToken.sol"; -import "./MasterToken.sol"; contract OwnerToken is BaseToken { - event MasterTokenCreated(address masterToken); - bytes public signerPublicKey; constructor( 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/script/DeployContracts.s.sol b/script/DeployContracts.s.sol new file mode 100644 index 0000000..d5e9220 --- /dev/null +++ b/script/DeployContracts.s.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.17; + +import { BaseScript } from "./Base.s.sol"; +import { DeploymentConfig } from "./DeploymentConfig.s.sol"; +import { CommunityOwnerTokenFactory } from "../contracts/factories/CommunityOwnerTokenFactory.sol"; +import { CommunityMasterTokenFactory } from "../contracts/factories/CommunityMasterTokenFactory.sol"; +import { CommunityOwnerTokenRegistry } from "../contracts/CommunityOwnerTokenRegistry.sol"; +import { CommunityTokenDeployer } from "../contracts/CommunityTokenDeployer.sol"; + +contract DeployContracts is BaseScript { + function run() + external + returns ( + CommunityTokenDeployer, + CommunityOwnerTokenRegistry, + CommunityOwnerTokenFactory, + CommunityMasterTokenFactory, + DeploymentConfig + ) + { + DeploymentConfig deploymentConfig = new DeploymentConfig(broadcaster); + + vm.startBroadcast(broadcaster); + CommunityOwnerTokenFactory ownerTokenFactory = new CommunityOwnerTokenFactory(); + CommunityMasterTokenFactory masterTokenFactory = new CommunityMasterTokenFactory(); + CommunityOwnerTokenRegistry tokenRegistry = new CommunityOwnerTokenRegistry(); + CommunityTokenDeployer tokenDeployer = + new CommunityTokenDeployer(address(tokenRegistry), address(ownerTokenFactory), address(masterTokenFactory)); + + tokenRegistry.setCommunityTokenDeployerAddress(address(tokenDeployer)); + ownerTokenFactory.setTokenDeployerAddress(address(tokenDeployer)); + masterTokenFactory.setTokenDeployerAddress(address(tokenDeployer)); + vm.stopBroadcast(); + + return (tokenDeployer, tokenRegistry, ownerTokenFactory, masterTokenFactory, deploymentConfig); + } + + // This function is a hack to have it excluded by `forge coverage` until + // https://github.com/foundry-rs/foundry/issues/2988 is fixed. + // See: https://github.com/foundry-rs/foundry/issues/2988#issuecomment-1437784542 + // for more info. + // solhint-disable-next-line + function test() public { } +} diff --git a/script/DeployOwnerAndMasterToken.s.sol b/script/DeployOwnerAndMasterToken.s.sol new file mode 100644 index 0000000..de6fa7f --- /dev/null +++ b/script/DeployOwnerAndMasterToken.s.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.17; + +import { BaseScript } from "./Base.s.sol"; +import { DeploymentConfig } from "./DeploymentConfig.s.sol"; +import { OwnerToken } from "../contracts/tokens/OwnerToken.sol"; +import { MasterToken } from "../contracts/tokens/MasterToken.sol"; + +contract DeployOwnerAndMasterToken is BaseScript { + function run() external returns (OwnerToken, MasterToken, DeploymentConfig) { + DeploymentConfig deploymentConfig = new DeploymentConfig(broadcaster); + DeploymentConfig.TokenConfig memory ownerTokenConfig = deploymentConfig.getOwnerTokenConfig(); + DeploymentConfig.TokenConfig memory masterTokenConfig = deploymentConfig.getMasterTokenConfig(); + + vm.startBroadcast(broadcaster); + OwnerToken ownerToken = new OwnerToken( + ownerTokenConfig.name, + ownerTokenConfig.symbol, + ownerTokenConfig.baseURI, + broadcaster, + ownerTokenConfig.signerPublicKey + ); + + MasterToken masterToken = new MasterToken( + masterTokenConfig.name, + masterTokenConfig.symbol, + masterTokenConfig.baseURI, + address(ownerToken) + ); + vm.stopBroadcast(); + + return (ownerToken, masterToken, deploymentConfig); + } + + // This function is a hack to have it excluded by `forge coverage` until + // https://github.com/foundry-rs/foundry/issues/2988 is fixed. + // See: https://github.com/foundry-rs/foundry/issues/2988#issuecomment-1437784542 + // for more info. + // solhint-disable-next-line + function test() public { } +} diff --git a/script/DeployOwnerToken.s.sol b/script/DeployOwnerToken.s.sol deleted file mode 100644 index ddf9749..0000000 --- a/script/DeployOwnerToken.s.sol +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED - -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"; -import { MasterToken } from "../contracts/MasterToken.sol"; - -contract DeployOwnerToken is BaseScript { - function run() external returns (OwnerToken, MasterToken, DeploymentConfig) { - DeploymentConfig deploymentConfig = new DeploymentConfig(broadcaster); - 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 - ); - - // 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); - vm.stopBroadcast(); - - return (ownerToken, masterToken, deploymentConfig); - } -} diff --git a/script/DeploymentConfig.s.sol b/script/DeploymentConfig.s.sol index c7214a6..ef0e1dc 100644 --- a/script/DeploymentConfig.s.sol +++ b/script/DeploymentConfig.s.sol @@ -48,4 +48,11 @@ contract DeploymentConfig is Script { function getMasterTokenConfig() public view returns (TokenConfig memory) { return masterTokenConfig; } + + // This function is a hack to have it excluded by `forge coverage` until + // https://github.com/foundry-rs/foundry/issues/2988 is fixed. + // See: https://github.com/foundry-rs/foundry/issues/2988#issuecomment-1437784542 + // for more info. + // solhint-disable-next-line + function test() public { } } diff --git a/specs.sh b/specs.sh index 8ad34af..3bee9ea 100755 --- a/specs.sh +++ b/specs.sh @@ -9,7 +9,7 @@ then fi certoraRun \ - ./contracts/CollectibleV1.sol \ + ./contracts/tokens/CollectibleV1.sol \ --verify CollectibleV1:./specs/CollectibleV1.spec \ --packages @openzeppelin=lib/openzeppelin-contracts \ --optimistic_loop \ diff --git a/test/CollectibleV1.t.sol b/test/CollectibleV1.t.sol index febfcd7..616a215 100644 --- a/test/CollectibleV1.t.sol +++ b/test/CollectibleV1.t.sol @@ -2,11 +2,11 @@ pragma solidity ^0.8.17; import { Test } from "forge-std/Test.sol"; -import { DeployOwnerToken } from "../script/DeployOwnerToken.s.sol"; +import { DeployOwnerAndMasterToken } from "../script/DeployOwnerAndMasterToken.s.sol"; import { DeploymentConfig } from "../script/DeploymentConfig.s.sol"; -import { OwnerToken } from "../contracts/OwnerToken.sol"; -import { MasterToken } from "../contracts/MasterToken.sol"; -import { CollectibleV1 } from "../contracts/CollectibleV1.sol"; +import { OwnerToken } from "../contracts/tokens/OwnerToken.sol"; +import { MasterToken } from "../contracts/tokens/MasterToken.sol"; +import { CollectibleV1 } from "../contracts/tokens/CollectibleV1.sol"; contract CollectibleV1Test is Test { CollectibleV1 internal collectibleV1; @@ -22,7 +22,7 @@ contract CollectibleV1Test is Test { bool internal transferable = true; function setUp() public virtual { - DeployOwnerToken deployment = new DeployOwnerToken(); + DeployOwnerAndMasterToken deployment = new DeployOwnerAndMasterToken(); (OwnerToken ownerToken, MasterToken masterToken, DeploymentConfig deploymentConfig) = deployment.run(); deployer = deploymentConfig.deployer(); diff --git a/test/CommunityERC20.t.sol b/test/CommunityERC20.t.sol index a4af9e6..095d1f4 100644 --- a/test/CommunityERC20.t.sol +++ b/test/CommunityERC20.t.sol @@ -2,14 +2,11 @@ pragma solidity ^0.8.17; import { Test } from "forge-std/Test.sol"; -import { DeployOwnerToken } from "../script/DeployOwnerToken.s.sol"; -import { DeploymentConfig } from "../script/DeploymentConfig.s.sol"; -import { CommunityERC20 } from "../contracts/CommunityERC20.sol"; +import { CommunityERC20 } from "../contracts/tokens/CommunityERC20.sol"; contract CommunityERC20Test is Test { CommunityERC20 internal communityToken; - address internal deployer; address[] internal accounts = new address[](4); string internal name = "Test"; @@ -18,10 +15,6 @@ contract CommunityERC20Test is Test { uint8 internal decimals = 18; function setUp() public virtual { - DeployOwnerToken deployment = new DeployOwnerToken(); - (,, DeploymentConfig deploymentConfig) = deployment.run(); - deployer = deploymentConfig.deployer(); - communityToken = new CommunityERC20(name, symbol, decimals, maxSupply); accounts[0] = makeAddr("one"); diff --git a/test/CommunityMasterTokenFactory.t.sol b/test/CommunityMasterTokenFactory.t.sol new file mode 100644 index 0000000..d9d5b6d --- /dev/null +++ b/test/CommunityMasterTokenFactory.t.sol @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import { Test } from "forge-std/Test.sol"; +import { DeployContracts } from "../script/DeployContracts.s.sol"; +import { DeploymentConfig } from "../script/DeploymentConfig.s.sol"; +import { BaseTokenFactory } from "../contracts/factories/BaseTokenFactory.sol"; +import { CommunityMasterTokenFactory } from "../contracts/factories/CommunityMasterTokenFactory.sol"; +import { MasterToken } from "../contracts/tokens/MasterToken.sol"; +import { CommunityTokenDeployer } from "../contracts/CommunityTokenDeployer.sol"; + +contract CommunityMasterTokenFactoryTest is Test { + DeploymentConfig internal deploymentConfig; + + address internal deployer; + + CommunityTokenDeployer internal tokenDeployer; + + CommunityMasterTokenFactory internal masterTokenFactory; + + function setUp() public virtual { + DeployContracts deployment = new DeployContracts(); + (tokenDeployer,,, masterTokenFactory, deploymentConfig) = deployment.run(); + deployer = deploymentConfig.deployer(); + } +} + +contract DeploymentTest is CommunityMasterTokenFactoryTest { + function setUp() public virtual override { + CommunityMasterTokenFactoryTest.setUp(); + } + + function test_Deployment() public { + assertEq(masterTokenFactory.owner(), deployer); + assertEq(masterTokenFactory.tokenDeployer(), address(tokenDeployer)); + } +} + +contract SetTokenDeployerAddressTest is CommunityMasterTokenFactoryTest { + event TokenDeployerAddressChange(address indexed); + + function setUp() public virtual override { + CommunityMasterTokenFactoryTest.setUp(); + } + + function test_RevertWhen_SenderIsNotOwner() public { + vm.expectRevert(bytes("Ownable: caller is not the owner")); + masterTokenFactory.setTokenDeployerAddress(makeAddr("something")); + } + + function test_RevertWhen_InvalidTokenDeployerAddress() public { + vm.prank(deployer); + vm.expectRevert(BaseTokenFactory.BaseTokenFactory_InvalidTokenDeployerAddress.selector); + masterTokenFactory.setTokenDeployerAddress(address(0)); + } + + function test_SetTokenDeployerAddress() public { + address someAddress = makeAddr("someAddress"); + vm.prank(deployer); + vm.expectEmit(true, true, true, true); + emit TokenDeployerAddressChange(someAddress); + masterTokenFactory.setTokenDeployerAddress(someAddress); + assertEq(masterTokenFactory.tokenDeployer(), someAddress); + } +} + +contract CreateTest is CommunityMasterTokenFactoryTest { + event CreateToken(address indexed); + + function setUp() public virtual override { + CommunityMasterTokenFactoryTest.setUp(); + } + + function test_RevertWhen_SenderIsNotTokenDeployer() public { + string memory name = "TestToken"; + string memory symbol = "TEST"; + string memory baseURI = "http://test.dev"; + address ownerToken = makeAddr("ownerToken"); + bytes memory signerPublicKey = bytes(""); + + vm.prank(makeAddr("notTokenDeployer")); + vm.expectRevert(BaseTokenFactory.BaseTokenFactory_NotAuthorized.selector); + masterTokenFactory.create(name, symbol, baseURI, ownerToken, signerPublicKey); + } + + function test_RevertWhen_InvalidTokenMetadata() public { + string memory name = ""; + string memory symbol = ""; + string memory baseURI = ""; + address ownerToken = makeAddr("ownerToken"); + bytes memory signerPublicKey = bytes(""); + + vm.startPrank(address(tokenDeployer)); + vm.expectRevert(BaseTokenFactory.BaseTokenFactory_InvalidTokenMetadata.selector); + masterTokenFactory.create(name, symbol, baseURI, ownerToken, signerPublicKey); + + baseURI = "http://test.dev"; + vm.expectRevert(BaseTokenFactory.BaseTokenFactory_InvalidTokenMetadata.selector); + masterTokenFactory.create(name, symbol, baseURI, ownerToken, signerPublicKey); + + symbol = "TEST"; + vm.expectRevert(BaseTokenFactory.BaseTokenFactory_InvalidTokenMetadata.selector); + masterTokenFactory.create(name, symbol, baseURI, ownerToken, signerPublicKey); + } + + function test_RevertWhen_InvalidOwnerTokenAddress() public { + string memory name = "TestToken"; + string memory symbol = "TEST"; + string memory baseURI = "http://test.dev"; + address ownerToken = address(0); + bytes memory signerPublicKey = bytes(""); + + vm.prank(address(tokenDeployer)); + vm.expectRevert(CommunityMasterTokenFactory.CommunityMasterTokenFactory_InvalidOwnerTokenAddress.selector); + masterTokenFactory.create(name, symbol, baseURI, ownerToken, signerPublicKey); + } + + function test_Create() public { + string memory name = "TestToken"; + string memory symbol = "TEST"; + string memory baseURI = "http://test.dev"; + address ownerToken = makeAddr("ownerToken"); + bytes memory signerPublicKey = bytes("some public key"); + + vm.prank(address(tokenDeployer)); + vm.expectEmit(false, false, false, false); + emit CreateToken(makeAddr("some address")); + address masterTokenAddress = masterTokenFactory.create(name, symbol, baseURI, ownerToken, signerPublicKey); + + assertEq(MasterToken(masterTokenAddress).totalSupply(), 0); + assertEq(MasterToken(masterTokenAddress).maxSupply(), type(uint256).max); + assertEq(MasterToken(masterTokenAddress).transferable(), false); + assertEq(MasterToken(masterTokenAddress).remoteBurnable(), true); + assertEq(MasterToken(masterTokenAddress).ownerToken(), ownerToken); + } +} diff --git a/test/CommunityOwnerTokenFactory.t.sol b/test/CommunityOwnerTokenFactory.t.sol new file mode 100644 index 0000000..ed6255a --- /dev/null +++ b/test/CommunityOwnerTokenFactory.t.sol @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import { Test } from "forge-std/Test.sol"; +import { DeployContracts } from "../script/DeployContracts.s.sol"; +import { DeploymentConfig } from "../script/DeploymentConfig.s.sol"; +import { BaseTokenFactory } from "../contracts/factories/BaseTokenFactory.sol"; +import { CommunityOwnerTokenFactory } from "../contracts/factories/CommunityOwnerTokenFactory.sol"; +import { OwnerToken } from "../contracts/tokens/OwnerToken.sol"; +import { CommunityTokenDeployer } from "../contracts/CommunityTokenDeployer.sol"; + +contract CommunityOwnerTokenFactoryTest is Test { + DeploymentConfig internal deploymentConfig; + + address internal deployer; + + CommunityTokenDeployer internal tokenDeployer; + + CommunityOwnerTokenFactory internal ownerTokenFactory; + + function setUp() public virtual { + DeployContracts deployment = new DeployContracts(); + (tokenDeployer,, ownerTokenFactory,, deploymentConfig) = deployment.run(); + deployer = deploymentConfig.deployer(); + } +} + +contract DeploymentTest is CommunityOwnerTokenFactoryTest { + function setUp() public virtual override { + CommunityOwnerTokenFactoryTest.setUp(); + } + + function test_Deployment() public { + assertEq(ownerTokenFactory.owner(), deployer); + assertEq(ownerTokenFactory.tokenDeployer(), address(tokenDeployer)); + } +} + +contract SetTokenDeployerAddressTest is CommunityOwnerTokenFactoryTest { + event TokenDeployerAddressChange(address indexed); + + function setUp() public virtual override { + CommunityOwnerTokenFactoryTest.setUp(); + } + + function test_RevertWhen_SenderIsNotOwner() public { + vm.expectRevert(bytes("Ownable: caller is not the owner")); + ownerTokenFactory.setTokenDeployerAddress(makeAddr("something")); + } + + function test_RevertWhen_InvalidTokenDeployerAddress() public { + vm.prank(deployer); + vm.expectRevert(BaseTokenFactory.BaseTokenFactory_InvalidTokenDeployerAddress.selector); + ownerTokenFactory.setTokenDeployerAddress(address(0)); + } + + function test_SetTokenDeployerAddress() public { + address someAddress = makeAddr("someAddress"); + vm.prank(deployer); + vm.expectEmit(true, true, true, true); + emit TokenDeployerAddressChange(someAddress); + ownerTokenFactory.setTokenDeployerAddress(someAddress); + assertEq(ownerTokenFactory.tokenDeployer(), someAddress); + } +} + +contract CreateTest is CommunityOwnerTokenFactoryTest { + event CreateToken(address indexed); + + function setUp() public virtual override { + CommunityOwnerTokenFactoryTest.setUp(); + } + + function test_RevertWhen_SenderIsNotTokenDeployer() public { + string memory name = "TestToken"; + string memory symbol = "TEST"; + string memory baseURI = "http://test.dev"; + address receiver = makeAddr("receiver"); + bytes memory signerPublicKey = bytes("some public key"); + + vm.prank(makeAddr("notTokenDeployer")); + vm.expectRevert(BaseTokenFactory.BaseTokenFactory_NotAuthorized.selector); + ownerTokenFactory.create(name, symbol, baseURI, receiver, signerPublicKey); + } + + function test_RevertWhen_InvalidTokenMetadata() public { + string memory name = ""; + string memory symbol = ""; + string memory baseURI = ""; + address receiver = makeAddr("receiver"); + bytes memory signerPublicKey = bytes("some public key"); + + vm.startPrank(address(tokenDeployer)); + vm.expectRevert(BaseTokenFactory.BaseTokenFactory_InvalidTokenMetadata.selector); + ownerTokenFactory.create(name, symbol, baseURI, receiver, signerPublicKey); + + baseURI = "http://test.dev"; + vm.expectRevert(BaseTokenFactory.BaseTokenFactory_InvalidTokenMetadata.selector); + ownerTokenFactory.create(name, symbol, baseURI, receiver, signerPublicKey); + + symbol = "TEST"; + vm.expectRevert(BaseTokenFactory.BaseTokenFactory_InvalidTokenMetadata.selector); + ownerTokenFactory.create(name, symbol, baseURI, receiver, signerPublicKey); + } + + function test_RevertWhen_InvalidReceiverAddress() public { + string memory name = "TestToken"; + string memory symbol = "TEST"; + string memory baseURI = "http://test.dev"; + address receiver = address(0); + bytes memory signerPublicKey = bytes("some public key"); + + vm.prank(address(tokenDeployer)); + vm.expectRevert(CommunityOwnerTokenFactory.CommunityOwnerTokenFactory_InvalidReceiverAddress.selector); + ownerTokenFactory.create(name, symbol, baseURI, receiver, signerPublicKey); + } + + function test_RevertWhen_InvalidSignerPublicKey() public { + string memory name = "TestToken"; + string memory symbol = "TEST"; + string memory baseURI = "http://test.dev"; + address receiver = makeAddr("receiver"); + bytes memory signerPublicKey = bytes(""); + + vm.prank(address(tokenDeployer)); + vm.expectRevert(CommunityOwnerTokenFactory.CommunityOwnerTokenFactory_InvalidSignerPublicKey.selector); + ownerTokenFactory.create(name, symbol, baseURI, receiver, signerPublicKey); + } + + function test_Create() public { + string memory name = "TestToken"; + string memory symbol = "TEST"; + string memory baseURI = "http://test.dev"; + address receiver = makeAddr("receiver"); + bytes memory signerPublicKey = bytes("some public key"); + + vm.prank(address(tokenDeployer)); + vm.expectEmit(false, false, false, false); + emit CreateToken(makeAddr("some address")); + address ownerTokenAddress = ownerTokenFactory.create(name, symbol, baseURI, receiver, signerPublicKey); + + assertEq(OwnerToken(ownerTokenAddress).totalSupply(), 1); + assertEq(OwnerToken(ownerTokenAddress).maxSupply(), 1); + assertEq(OwnerToken(ownerTokenAddress).balanceOf(receiver), 1); + } +} diff --git a/test/CommunityOwnerTokenRegistry.t.sol b/test/CommunityOwnerTokenRegistry.t.sol new file mode 100644 index 0000000..cb5b5bc --- /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 { DeployContracts } from "../script/DeployContracts.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 TokenDeployerAddressChange(address indexed); + event AddEntry(address indexed, address indexed); + + DeploymentConfig internal deploymentConfig; + + CommunityTokenDeployer internal tokenDeployer; + + CommunityOwnerTokenRegistry internal tokenRegistry; + + address internal deployer; + + address internal tokenDeployerAccount = makeAddr("tokenDeployer"); + + address internal communityAddress = makeAddr("communityAddress"); + + address internal tokenAddress = makeAddr("tokenAddress"); + + function setUp() public virtual { + DeployContracts deployment = new DeployContracts(); + (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_InvalidAddress.selector); + tokenRegistry.setCommunityTokenDeployerAddress(address(0)); + } + + function test_SetCommunityTokenDeployerAddress() public { + address newAddress = makeAddr("someAddress"); + vm.prank(deployer); + vm.expectEmit(true, true, true, false); + emit TokenDeployerAddressChange(newAddress); + tokenRegistry.setCommunityTokenDeployerAddress(newAddress); + assertEq(tokenRegistry.tokenDeployer(), newAddress); + } +} + +contract AddEntryTest is CommunityOwnerTokenRegistryTest { + function setUp() public virtual override { + CommunityOwnerTokenRegistryTest.setUp(); + vm.prank(deployer); + tokenRegistry.setCommunityTokenDeployerAddress(tokenDeployerAccount); + } + + function test_RevertWhen_SenderIsNotTokenDeployer() public { + vm.expectRevert(CommunityOwnerTokenRegistry.CommunityOwnerTokenRegistry_NotAuthorized.selector); + tokenRegistry.addEntry(communityAddress, tokenAddress); + } + + function test_RevertWhen_InvalidAddress() public { + vm.startPrank(tokenDeployerAccount); + 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(tokenDeployerAccount); + tokenRegistry.addEntry(communityAddress, tokenAddress); + vm.expectRevert(CommunityOwnerTokenRegistry.CommunityOwnerTokenRegistry_EntryAlreadyExists.selector); + tokenRegistry.addEntry(communityAddress, tokenAddress); + } + + function test_AddEntry() public { + vm.startPrank(tokenDeployerAccount); + 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..b3d5fdc --- /dev/null +++ b/test/CommunityTokenDeployer.t.sol @@ -0,0 +1,278 @@ +// 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 { DeployContracts } from "../script/DeployContracts.s.sol"; +import { DeploymentConfig } from "../script/DeploymentConfig.s.sol"; +import { BaseTokenFactory } from "../contracts/factories/BaseTokenFactory.sol"; +import { OwnerToken } from "../contracts/tokens/OwnerToken.sol"; +import { MasterToken } from "../contracts/tokens/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 { + DeployContracts deployment = new DeployContracts(); + (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 + ) + internal + view + returns (CommunityTokenDeployer.DeploymentSignature memory) + { + bytes32 digest = ECDSA.toTypedDataHash( + tokenDeployer.DOMAIN_SEPARATOR(), + keccak256(abi.encode(tokenDeployer.DEPLOYMENT_SIGNATURE_TYPEHASH(), _signer, _deployer)) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_signerKey, digest); + return CommunityTokenDeployer.DeploymentSignature(_signer, _deployer, v, r, s); + } +} + +contract SetDeploymentRegistryAddressTest is CommunityTokenDeployerTest { + event DeploymentRegistryAddressChange(address indexed); + + function setUp() public virtual override { + CommunityTokenDeployerTest.setUp(); + } + + function test_RevertWhen_SenderIsNotOwner() public { + vm.expectRevert(bytes("Ownable: caller is not the owner")); + tokenDeployer.setDeploymentRegistryAddress(makeAddr("someAddress")); + } + + function test_RevertWhen_InvalidDeploymentRegistryAddress() public { + vm.prank(deployer); + vm.expectRevert(CommunityTokenDeployer.CommunityTokenDeployer_InvalidDeploymentRegistryAddress.selector); + tokenDeployer.setDeploymentRegistryAddress(address(0)); + } + + function test_SetDeploymentRegistryAddress() public { + address newAddress = makeAddr("newAddress"); + vm.prank(deployer); + vm.expectEmit(true, true, true, true); + emit DeploymentRegistryAddressChange(newAddress); + tokenDeployer.setDeploymentRegistryAddress(newAddress); + + assertEq(tokenDeployer.deploymentRegistry(), newAddress); + } +} + +contract SetOwnerTokenFactoryAddressTest is CommunityTokenDeployerTest { + event OwnerTokenFactoryAddressChange(address indexed); + + function setUp() public virtual override { + CommunityTokenDeployerTest.setUp(); + } + + function test_RevertWhen_SenderIsNotOwner() public { + vm.expectRevert(bytes("Ownable: caller is not the owner")); + tokenDeployer.setOwnerTokenFactoryAddress(makeAddr("someAddress")); + } + + function test_RevertWhen_InvalidTokenFactoryAddress() public { + vm.prank(deployer); + vm.expectRevert(CommunityTokenDeployer.CommunityTokenDeployer_InvalidTokenFactoryAddress.selector); + tokenDeployer.setOwnerTokenFactoryAddress(address(0)); + } + + function test_SetOwnerTokenFactoryAddress() public { + address newAddress = makeAddr("newAddress"); + vm.prank(deployer); + vm.expectEmit(true, true, true, true); + emit OwnerTokenFactoryAddressChange(newAddress); + tokenDeployer.setOwnerTokenFactoryAddress(newAddress); + + assertEq(tokenDeployer.ownerTokenFactory(), newAddress); + } +} + +contract SetMasterTokenFactoryAddressTest is CommunityTokenDeployerTest { + event MasterTokenFactoryAddressChange(address indexed); + + function setUp() public virtual override { + CommunityTokenDeployerTest.setUp(); + } + + function test_RevertWhen_SenderIsNotOwner() public { + vm.expectRevert(bytes("Ownable: caller is not the owner")); + tokenDeployer.setMasterTokenFactoryAddress(makeAddr("someAddress")); + } + + function test_RevertWhen_InvalidTokenFactoryAddress() public { + vm.prank(deployer); + vm.expectRevert(CommunityTokenDeployer.CommunityTokenDeployer_InvalidTokenFactoryAddress.selector); + tokenDeployer.setMasterTokenFactoryAddress(address(0)); + } + + function test_SetOwnerTokenFactoryAddress() public { + address newAddress = makeAddr("newAddress"); + vm.prank(deployer); + vm.expectEmit(true, true, true, true); + emit MasterTokenFactoryAddressChange(newAddress); + tokenDeployer.setMasterTokenFactoryAddress(newAddress); + + assertEq(tokenDeployer.masterTokenFactory(), newAddress); + } +} + +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")); + 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); + vm.prank(owner); + vm.expectRevert(CommunityTokenDeployer.CommunityTokenDeployer_InvalidDeploymentSignature.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); + + vm.prank(owner); + vm.expectRevert(BaseTokenFactory.BaseTokenFactory_InvalidTokenMetadata.selector); + tokenDeployer.deploy(ownerTokenConfig, masterTokenConfig, signature, signerPublicKey); + + // fill `masterTokenConfig` with data + masterTokenConfig = _getMasterTokenConfig(); + + vm.prank(owner); + vm.expectRevert(BaseTokenFactory.BaseTokenFactory_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(BaseTokenFactory.BaseTokenFactory_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); + + 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); + + 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); + + 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); + + 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); + } +} diff --git a/test/OwnerToken.t.sol b/test/OwnerToken.t.sol index 67026e3..87d57f0 100644 --- a/test/OwnerToken.t.sol +++ b/test/OwnerToken.t.sol @@ -2,10 +2,10 @@ pragma solidity ^0.8.17; import { Test } from "forge-std/Test.sol"; -import { DeployOwnerToken } from "../script/DeployOwnerToken.s.sol"; +import { DeployOwnerAndMasterToken } from "../script/DeployOwnerAndMasterToken.s.sol"; import { DeploymentConfig } from "../script/DeploymentConfig.s.sol"; -import { OwnerToken } from "../contracts/OwnerToken.sol"; -import { MasterToken } from "../contracts/MasterToken.sol"; +import { OwnerToken } from "../contracts/tokens/OwnerToken.sol"; +import { MasterToken } from "../contracts/tokens/MasterToken.sol"; contract OwnerTokenTest is Test { OwnerToken internal ownerToken; @@ -14,7 +14,7 @@ contract OwnerTokenTest is Test { address internal deployer; function setUp() public virtual { - DeployOwnerToken deployment = new DeployOwnerToken(); + DeployOwnerAndMasterToken deployment = new DeployOwnerAndMasterToken(); (ownerToken, masterToken, deploymentConfig) = deployment.run(); deployer = deploymentConfig.deployer(); }