Skip to content

Commit

Permalink
feat: implement CommunityTokenDeployer contract
Browse files Browse the repository at this point in the history
This commit introduces the `CommunityTokenDeployer` contract discussed
in status-im/status-desktop#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.
  • Loading branch information
0x-r4bbit committed Sep 4, 2023
1 parent 0c4e1d1 commit 770e715
Show file tree
Hide file tree
Showing 9 changed files with 652 additions and 22 deletions.
19 changes: 19 additions & 0 deletions .gas-snapshot
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
AddEntryTest:test_AddEntry() (gas: 44369)
AddEntryTest:test_RevertWhen_EntryAlreadyExists() (gas: 42598)
AddEntryTest:test_RevertWhen_InvalidAddress() (gas: 25087)
AddEntryTest:test_RevertWhen_SenderIsNotTokenDeployer() (gas: 14804)
CollectibleV1Test:test_Deployment() (gas: 38626)
CommunityERC20Test:test_Deployment() (gas: 29720)
CommunityTokenDeployerTest:test_Deployment() (gas: 14794)
DeployTest:test_Deploy() (gas: 4820243)
DeployTest:test_Deployment() (gas: 14914)
DeployTest:test_RevertWhen_AlreadyDeployed() (gas: 4817349)
DeployTest:test_RevertWhen_DeploymentSignatureExpired() (gas: 54842)
DeployTest:test_RevertWhen_InvalidCommunityAddress() (gas: 52541)
DeployTest:test_RevertWhen_InvalidDeployerAddress() (gas: 56529)
DeployTest:test_RevertWhen_InvalidDeploymentSignature() (gas: 66863)
DeployTest:test_RevertWhen_InvalidSignerPublicKey() (gas: 54635)
DeployTest:test_RevertWhen_InvalidTokenMetadata() (gas: 70764)
DeploymentTest:test_Deployment() (gas: 17239)
GetEntryTest:test_ReturnZeroAddressIfEntryDoesNotExist() (gas: 11906)
MintToTest:test_Deployment() (gas: 29742)
MintToTest:test_Deployment() (gas: 38626)
MintToTest:test_Deployment() (gas: 85415)
Expand All @@ -15,6 +31,9 @@ RemoteBurnTest:test_Deployment() (gas: 85437)
RemoteBurnTest:test_RemoteBurn() (gas: 455285)
RemoteBurnTest:test_RevertWhen_RemoteBurn() (gas: 19499)
RemoteBurnTest:test_RevertWhen_SenderIsNotOwner() (gas: 25211)
SetCommunityTokenDeployerAddressTest:test_RevertWhen_InvalidTokenDeployerAddress() (gas: 12963)
SetCommunityTokenDeployerAddressTest:test_RevertWhen_SenderIsNotOwner() (gas: 12504)
SetCommunityTokenDeployerAddressTest:test_SetCommunityTokenDeployerAddress() (gas: 22807)
SetMaxSupplyTest:test_Deployment() (gas: 29720)
SetMaxSupplyTest:test_Deployment() (gas: 85437)
SetMaxSupplyTest:test_RevertWhen_CalledBecauseMaxSupplyIsLocked() (gas: 16521)
Expand Down
92 changes: 92 additions & 0 deletions contracts/CommunityOwnerTokenRegistry.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// SPDX-License-Identifier: UNLICENSED

pragma solidity ^0.8.17;

import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { IAddressRegistry } from "./interfaces/IAddressRegistry.sol";
import { OwnerToken } from "./OwnerToken.sol";

/**
* @title CommunityOwnerTokenRegistry contract
* @author 0x-r4bbit
*
* This contract serves as a simple registry to map Status community addresses
* to Status community `OwnerToken` addresses.
* The `CommunityTokenDeployer` contract uses this registry contract to maintain
* a list of community address and their token addresses.
* @notice This contract will be deployed by Status similar to the `CommunityTokenDeployer`
* contract.
* @notice This contract maps community addresses to `OwnerToken` addresses.
* @notice Only one entry per community address can exist in the registry.
* @dev This registry has been extracted into its own contract so that it's possible
* to introduce different version of a `CommunityDeployerContract` without needing to
* migrate existing registry data, as the deployer contract would simply point at this
* registry contract.
* @dev Only `tokenDeployer` can add entries to the registry.
*/
contract CommunityOwnerTokenRegistry is IAddressRegistry, Ownable {
error CommunityOwnerTokenRegistry_InvalidTokenDeployerAddress();
error CommunityOwnerTokenRegistry_NotAuthorized();
error CommunityOwnerTokenRegistry_EntryAlreadyExists();
error CommunityOwnerTokenRegistry_InvalidAddress();

event CommunityDeployerAddressChange(address indexed);
event AddEntry(address indexed, address indexed);

/// @dev The address of the token deployer contract.
address public tokenDeployer;

mapping(address => address) public communityAddressToTokenAddress;

modifier onlyTokenDeployer() {
if (msg.sender != tokenDeployer) {
revert CommunityOwnerTokenRegistry_NotAuthorized();
}
_;
}

/**
* @notice Sets the address of the community token deployer contract. This is needed to
* ensure only the known token deployer contract can add new entries to the registry.
* @dev Only the owner of this contract can call this function.
* @dev Emits a {CommunityDeployerAddressChange} event.
*
* @param _tokenDeployer The address of the community token deployer contract
*/
function setCommunityTokenDeployerAddress(address _tokenDeployer) external onlyOwner {
if (_tokenDeployer == address(0)) {
revert CommunityOwnerTokenRegistry_InvalidTokenDeployerAddress();
}
tokenDeployer = _tokenDeployer;
emit CommunityDeployerAddressChange(tokenDeployer);
}

/**
* @notice Adds an entry to the registry. Only one entry per community address can exist.
* @dev Only the token deployer contract can call this function.
* @dev Reverts when the entry already exists.
* @dev Reverts when either `_communityAddress` or `_tokenAddress` are zero addresses.
* @dev Emits a {AddEntry} event.
*/
function addEntry(address _communityAddress, address _tokenAddress) external onlyTokenDeployer {
if (getEntry(_communityAddress) != address(0)) {
revert CommunityOwnerTokenRegistry_EntryAlreadyExists();
}

if (_communityAddress == address(0) || _tokenAddress == address(0)) {
revert CommunityOwnerTokenRegistry_InvalidAddress();
}

communityAddressToTokenAddress[_communityAddress] = _tokenAddress;
emit AddEntry(_communityAddress, _tokenAddress);
}

/**
* @notice Returns the owner token address for a given community address.
* @param _communityAddress The community address to look up an owner token address.
* @return address The owner token address for the community addres, or zero address .
*/
function getEntry(address _communityAddress) public view returns (address) {
return communityAddressToTokenAddress[_communityAddress];
}
}
178 changes: 178 additions & 0 deletions contracts/CommunityTokenDeployer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// SPDX-License-Identifier: UNLICENSED

pragma solidity ^0.8.17;

import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import { EIP712 } from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import { OwnerToken } from "./OwnerToken.sol";
import { MasterToken } from "./MasterToken.sol";
import { IAddressRegistry } from "./interfaces/IAddressRegistry.sol";

/**
* @title CommunityTokenDeployer contract
* @author 0x-r4bbit
*
* This contract serves as a deployment process for Status community owners
* to deploy access control token contracts on behalf of their Status community.
* The contract deploys the two token contracts `OwnerToken` and `MasterToken`.
* The contract maintains a registry which keeps track of `OwnerToken` contract
* addresses per community.
*
* Only one deployment per community can be done.
* Status community owners have to provide an EIP712 hash signature that was
* created using their community's private key to successfully execute a deployment.
*
* @notice This contract is used by Status community owners to deploy
* community access control token contracts.
* @notice This contract maintains a registry that tracks contract addresses
* and community addresses
* @dev This contract will be deployed by Status, making Status the owner
* of the contract.
* @dev A contract address for a `CommunityTokenRegistry` contract has to be provided
* to create this contract.
* @dev The `CommunityTokenRegistry` can be changed by the owner of this contract.
*/
contract CommunityTokenDeployer is EIP712("CommunityTokenDeployer", "1"), Ownable {
using ECDSA for bytes32;

error CommunityTokenDeployer_InvalidDeploymentRegistryAddress();
error CommunityTokenDeployer_AlreadyDeployed();
error CommunityTokenDeployer_InvalidSignerKeyOrCommunityAddress();
error CommunityTokenDeployer_InvalidTokenMetadata();
error CommunityTokenDeployer_InvalidDeployerAddress();
error CommunityTokenDeployer_InvalidDeploymentSignature();
error CommunityTokenDeployer_DeploymentSignatureExpired();

/// @dev Needed to avoid "Stack too deep" error.
struct TokenConfig {
string name;
string symbol;
string baseURI;
}

/// @dev Used to verify signatures.
struct DeploymentSignature {
address signer;
address deployer;
/// a `deadline` must be provided to avoid unlimited lifetimes
uint256 deadline;
uint8 v;
bytes32 r;
bytes32 s;
}

bytes32 public constant DEPLOYMENT_SIGNATURE_TYPEHASH =
keccak256("Deploy(address signer,address deployer,uint256 deadline)");

address public deploymentRegistry;

event Deploy(address indexed, address indexed);

/// @param _registry The address of the `CommunityTokenRegistry` contract.
constructor(address _registry) {
if (_registry == address(0)) {
revert CommunityTokenDeployer_InvalidDeploymentRegistryAddress();
}
deploymentRegistry = _registry;
}

/**
* @notice Deploys an instance of `OwnerToken` and `MasterToken` on behalf
* of a Status community account, provided `_signature` is valid and was signed
* by that Status community account.
* @dev Anyone can call this function but a valid EIP712 hash signature has to be
* provided for a successful deployment.
* @dev Emits a {Deploy} event.
*
* @param _ownerToken A `TokenConfig` containing ERC721 metadata for `OwnerToken`
* @param _masterToken A `TokenConfig` containing ERC721 metadata for `MasterToken`
* @param _signature A `DeploymentSignature` containing a signer and deployer address,
* and a signature created by a Status community
* @return address The address of the deployed `OwnerToken` contract.
* @return address The address of the deployed `MasterToken` contract.
*/
function deploy(
TokenConfig calldata _ownerToken,
TokenConfig calldata _masterToken,
DeploymentSignature calldata _signature,
bytes memory _signerPublicKey
)
external
returns (address, address)
{
if (
bytes(_ownerToken.name).length == 0 || bytes(_ownerToken.symbol).length == 0
|| bytes(_ownerToken.baseURI).length == 0 || bytes(_masterToken.name).length == 0
|| bytes(_masterToken.symbol).length == 0 || bytes(_masterToken.baseURI).length == 0
) {
revert CommunityTokenDeployer_InvalidTokenMetadata();
}

if (_signature.signer == address(0) || _signerPublicKey.length == 0) {
revert CommunityTokenDeployer_InvalidSignerKeyOrCommunityAddress();
}

if (_signature.deadline < block.timestamp) {
revert CommunityTokenDeployer_DeploymentSignatureExpired();
}

if (_signature.deployer != msg.sender) {
revert CommunityTokenDeployer_InvalidDeployerAddress();
}

if (IAddressRegistry(deploymentRegistry).getEntry(_signature.signer) != address(0)) {
revert CommunityTokenDeployer_AlreadyDeployed();
}

if (!_verifySignature(_signature)) {
revert CommunityTokenDeployer_InvalidDeploymentSignature();
}

OwnerToken ownerToken = new OwnerToken(
_ownerToken.name,
_ownerToken.symbol,
_ownerToken.baseURI,
msg.sender,
_signerPublicKey
);

MasterToken masterToken = new MasterToken(
_masterToken.name,
_masterToken.symbol,
_masterToken.baseURI,
address(ownerToken)
);

IAddressRegistry(deploymentRegistry).addEntry(_signature.signer, address(ownerToken));
emit Deploy(address(ownerToken), address(masterToken));

return (address(ownerToken), address(masterToken));
}

/**
* @notice Returns an EIP712 domain separator hash
* @return bytes32 An EIP712 domain separator hash
*/
function DOMAIN_SEPARATOR() external view returns (bytes32) {
return _domainSeparatorV4();
}

/**
* @notice Verifies provided `DeploymentSignature` which was created by
* the Status community account for which the access control token contracts
* will be deployed.
* @dev This contract does not maintain nonces for the typed data hash, which
* is typically done to prevent signature replay attacks. The `deploy()` function
* allows only one deployment per Status community, so replay attacks are not possible.
* @return bool Whether the provided signature could be recovered.
*/
function _verifySignature(DeploymentSignature calldata signature) internal view returns (bool) {
bytes32 digest = _hashTypedDataV4(
keccak256(
abi.encode(DEPLOYMENT_SIGNATURE_TYPEHASH, signature.signer, signature.deployer, signature.deadline)
)
);
return signature.signer == digest.recover(signature.v, signature.r, signature.s);
}
}
8 changes: 2 additions & 6 deletions contracts/OwnerToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,14 @@ contract OwnerToken is BaseToken {
string memory _name,
string memory _symbol,
string memory _baseTokenURI,
string memory _masterName,
string memory _masterSymbol,
string memory _masterBaseTokenURI,
address _receiver,
bytes memory _signerPublicKey
)
BaseToken(_name, _symbol, 1, false, true, _baseTokenURI, address(this), address(this))
{
signerPublicKey = _signerPublicKey;
MasterToken masterToken = new MasterToken(_masterName, _masterSymbol, _masterBaseTokenURI, address(this));
emit MasterTokenCreated(address(masterToken));
address[] memory addresses = new address[](1);
addresses[0] = msg.sender;
addresses[0] = _receiver;
_mintTo(addresses);
}

Expand Down
8 changes: 8 additions & 0 deletions contracts/interfaces/IAddressRegistry.sol
Original file line number Diff line number Diff line change
@@ -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);
}
28 changes: 12 additions & 16 deletions script/DeployOwnerToken.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

pragma solidity ^0.8.17;

import { Vm } from "forge-std/Vm.sol";
import { BaseScript } from "./Base.s.sol";
import { DeploymentConfig } from "./DeploymentConfig.s.sol";
import { OwnerToken } from "../contracts/OwnerToken.sol";
Expand All @@ -14,24 +13,21 @@ contract DeployOwnerToken is BaseScript {
DeploymentConfig.TokenConfig memory ownerTokenConfig = deploymentConfig.getOwnerTokenConfig();
DeploymentConfig.TokenConfig memory masterTokenConfig = deploymentConfig.getMasterTokenConfig();

vm.recordLogs();
vm.startBroadcast(broadcaster);
OwnerToken ownerToken = new OwnerToken(
ownerTokenConfig.name,
ownerTokenConfig.symbol,
ownerTokenConfig.baseURI,
masterTokenConfig.name,
masterTokenConfig.symbol,
masterTokenConfig.baseURI,
ownerTokenConfig.signerPublicKey
);
ownerTokenConfig.name,
ownerTokenConfig.symbol,
ownerTokenConfig.baseURI,
broadcaster,
ownerTokenConfig.signerPublicKey
);

// Need to retrieve master token address from logs as
// we can't access it otherwise
Vm.Log[] memory entries = vm.getRecordedLogs();
address masterTokenAddress = abi.decode(entries[0].data, (address));

MasterToken masterToken = MasterToken(masterTokenAddress);
MasterToken masterToken = new MasterToken(
masterTokenConfig.name,
masterTokenConfig.symbol,
masterTokenConfig.baseURI,
address(ownerToken)
);
vm.stopBroadcast();

return (ownerToken, masterToken, deploymentConfig);
Expand Down
Loading

0 comments on commit 770e715

Please sign in to comment.