-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: implement
CommunityTokenDeployer
contract
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
Showing
9 changed files
with
652 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.