status-go/contracts/community-tokens/deployer/CommunityTokenDeployer.sol

234 lines
10 KiB
Solidity

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.17;
import { Ownable2Step } from "@openzeppelin/contracts/access/Ownable2Step.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"), Ownable2Step {
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);
event DeployOwnerToken(address indexed);
event DeployMasterToken(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.
* @dev Emits {DeployOwnerToken} event.
* @dev Emits {DeployMasterToken} event.
*
* @param _ownerToken A `TokenConfig` containing ERC721 metadata for `OwnerToken`
* @param _masterToken A `TokenConfig` containing ERC721 metadata for `MasterToken`
* @param _signature A `DeploymentSignature` containing a signer and deployer address,
* and a signature created by a Status community
* @return address The address of the deployed `OwnerToken` contract.
* @return address The address of the deployed `MasterToken` contract.
*/
function deploy(
TokenConfig calldata _ownerToken,
TokenConfig calldata _masterToken,
DeploymentSignature calldata _signature,
bytes memory _signerPublicKey
)
external
returns (address, address)
{
if (_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
);
emit DeployOwnerToken(ownerToken);
address masterToken = ITokenFactory(masterTokenFactory).create(
_masterToken.name, _masterToken.symbol, _masterToken.baseURI, ownerToken, bytes("")
);
emit DeployMasterToken(masterToken);
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);
}
}