feat: implement `CommunityTokenDeployer` contract

This commit introduces the `CommunityTokenDeployer` contract discussed
in https://github.com/status-im/status-desktop/issues/11954.

The idea is that, instead of having accounts deploy `OwnerToken` and
`MasterToken` directly, they'd use a deployer contract instead, which
maintains a registry of known `OwnerToken` addresses, mapped to Status
community addresses.

The following changes have been made:

It was, and still is, a requirement that both, `OwnerToken` and
`MasterToken` are deployed within a single transaction, so that when
something goes wrong, we don't end up in an inconsistent state.

That's why `OwnerToken` used to instantiated `MasterToken` and required
all of its constructor arguments as well.

Unfortunately, this resulted in compilation issues in the context of the
newly introduce deployer contract, where there are too many function
arguments.

Because we now delegate deployment to a dedicated contract, we can
instantiate both `OwnerToken` and `MasterToken` in a single transaction,
without having `OwnerToken` being responsible to instantiate
`MasterToken`.

This fixes the compilation issues and simplifies the constructor of
`OwnerToken`.

The new `CommunityTokenDeployer` contract is now responsble for
deploying the aforementioned tokens and ensures that they are deployed
within a single transaction.

To deploy an `OwnerToken` and `MasterToken` accounts can now call
`CommunityDeloyerToken.deploy(TokenConfig, TokenConfig,
DeploymentSignature)`.

The `DeploymentSignature` uses `EIP712` structured type hash data to let
the contract verify that the deployer is allowed to deploy the contracts
on behalf of a community account.
This commit is contained in:
r4bbit 2023-08-30 11:27:44 +02:00
parent e52eff0254
commit 5542cc05ae
No known key found for this signature in database
GPG Key ID: E95F1E9447DC91A9
25 changed files with 1357 additions and 74 deletions

View File

@ -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)
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)

View File

@ -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];
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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 { }
}

View File

@ -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 { }
}

View File

@ -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);
}
}

View File

@ -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 { }
}

View File

@ -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 \

View File

@ -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();

View File

@ -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");

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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));
}
}

View File

@ -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);
}
}

View File

@ -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();
}