feat(CommunityTokens): New deployment contracts and handling signer pub key

New contracts and contract go functions.
Adjust owner&master tokens deployment flow.
Create deployment signature.
CommunityTokens API for handling signer pubkey.

Issue #11954
This commit is contained in:
Michal Iskierko 2023-08-29 15:17:37 +02:00 committed by Michał Iskierko
parent e1354016a0
commit c85a110a31
24 changed files with 3946 additions and 186 deletions

View File

@ -1 +1 @@
0.167.6
0.168.0

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,70 @@
// SPDX-License-Identifier: Mozilla Public License 2.0
pragma solidity ^0.8.17;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/utils/Context.sol";
contract CommunityERC20 is Context, Ownable, ERC20 {
error CommunityERC20_MaxSupplyLowerThanTotalSupply();
error CommunityERC20_MaxSupplyReached();
error CommunityERC20_MismatchingAddressesAndAmountsLengths();
/**
* If we want unlimited total supply we should set maxSupply to 2^256-1.
*/
uint256 public maxSupply;
uint8 private immutable customDecimals;
constructor(
string memory _name,
string memory _symbol,
uint8 _decimals,
uint256 _maxSupply
)
ERC20(_name, _symbol)
{
maxSupply = _maxSupply;
customDecimals = _decimals;
}
// Events
// External functions
function setMaxSupply(uint256 newMaxSupply) external onlyOwner {
if (newMaxSupply < totalSupply()) {
revert CommunityERC20_MaxSupplyLowerThanTotalSupply();
}
maxSupply = newMaxSupply;
}
/**
* @dev Mint tokens for each address in `addresses` each one with
* an amount specified in `amounts`.
*
*/
function mintTo(address[] memory addresses, uint256[] memory amounts) external onlyOwner {
if (addresses.length != amounts.length) {
revert CommunityERC20_MismatchingAddressesAndAmountsLengths();
}
for (uint256 i = 0; i < addresses.length; i++) {
uint256 amount = amounts[i];
if (totalSupply() + amount > maxSupply) {
revert CommunityERC20_MaxSupplyReached();
}
_mint(addresses[i], amount);
}
}
// Public functions
function decimals() public view virtual override returns (uint8) {
return customDecimals;
}
// Internal functions
// Private functions
}

View File

@ -0,0 +1,19 @@
// SPDX-License-Identifier: Mozilla Public License 2.0
pragma solidity ^0.8.17;
import "./BaseToken.sol";
contract CollectibleV1 is BaseToken {
constructor(
string memory _name,
string memory _symbol,
uint256 _maxSupply,
bool _remoteBurnable,
bool _transferable,
string memory _baseTokenURI,
address _ownerToken,
address _masterToken
)
BaseToken(_name, _symbol, _maxSupply, _remoteBurnable, _transferable, _baseTokenURI, _ownerToken, _masterToken)
{ }
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,233 @@
// 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);
}
}

View File

@ -0,0 +1,22 @@
package communitytokendeployer
import (
"errors"
"github.com/ethereum/go-ethereum/common"
)
var errorNotAvailableOnChainID = errors.New("deployer contract not available for chainID")
// TODO add addresses for other chains
var contractAddressByChainID = map[uint64]common.Address{
420: common.HexToAddress("0xfFa8A255D905c909379859eA45B959D090DDC2d4"), // Optimism Goerli
}
func ContractAddress(chainID uint64) (common.Address, error) {
addr, exists := contractAddressByChainID[chainID]
if !exists {
return *new(common.Address), errorNotAvailableOnChainID
}
return addr, nil
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,15 @@
// SPDX-License-Identifier: Mozilla Public License 2.0
pragma solidity ^0.8.17;
import "./BaseToken.sol";
contract MasterToken is BaseToken {
constructor(
string memory _name,
string memory _symbol,
string memory _baseTokenURI,
address _ownerToken
)
BaseToken(_name, _symbol, type(uint256).max, true, false, _baseTokenURI, _ownerToken, address(0x0))
{ }
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,31 @@
// SPDX-License-Identifier: Mozilla Public License 2.0
pragma solidity ^0.8.17;
import "./BaseToken.sol";
contract OwnerToken is BaseToken {
bytes public signerPublicKey;
constructor(
string memory _name,
string memory _symbol,
string memory _baseTokenURI,
address _receiver,
bytes memory _signerPublicKey
)
BaseToken(_name, _symbol, 1, false, true, _baseTokenURI, address(this), address(this))
{
signerPublicKey = _signerPublicKey;
address[] memory addresses = new address[](1);
addresses[0] = _receiver;
_mintTo(addresses);
}
function setMaxSupply(uint256 _newMaxSupply) external override onlyOwner {
revert("max supply locked");
}
function setSignerPublicKey(bytes memory _newSignerPublicKey) external onlyOwner {
signerPublicKey = _newSignerPublicKey;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,91 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.17;
import { Ownable2Step } from "@openzeppelin/contracts/access/Ownable2Step.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, Ownable2Step {
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

@ -4933,3 +4933,21 @@ func (m *Manager) GetCommunityRequestsToJoinWithRevealedAddresses(communityID ty
func (m *Manager) SaveCommunity(community *Community) error {
return m.persistence.SaveCommunity(community)
}
func (m *Manager) CreateCommunityTokenDeploymentSignature(ctx context.Context, chainID uint64, addressFrom string, communityID string) ([]byte, error) {
community, err := m.GetByIDString(communityID)
if err != nil {
return nil, err
}
if community == nil {
return nil, ErrOrgNotFound
}
if !community.IsControlNode() {
return nil, ErrNotControlNode
}
digest, err := m.communityTokensService.DeploymentSignatureDigest(chainID, addressFrom, communityID)
if err != nil {
return nil, err
}
return crypto.Sign(digest, community.PrivateKey())
}

View File

@ -118,6 +118,10 @@ func (c *CollectiblesServiceMock) SetMockAssetContractData(chainID uint64, contr
c.Assets[chainID][contractAddress] = assetData
}
func (c *CollectiblesServiceMock) DeploymentSignatureDigest(chainID uint64, addressFrom string, communityID string) ([]byte, error) {
return gethcommon.Hex2Bytes("ccbb375343347491706cf4b43796f7b96ccc89c9e191a8b78679daeba1684ec7"), nil
}
func newMessenger(s *suite.Suite, shh types.Waku, logger *zap.Logger, password string, walletAddresses []string,
mockedBalances *map[uint64]map[gethcommon.Address]map[gethcommon.Address]*hexutil.Big, collectiblesService communitytokens.ServiceInterface) *Messenger {
accountsManagerMock := &AccountManagerMock{}

View File

@ -5077,6 +5077,10 @@ func (m *Messenger) SignMessage(message string) ([]byte, error) {
return crypto.Sign(hash, m.identity)
}
func (m *Messenger) CreateCommunityTokenDeploymentSignature(ctx context.Context, chainID uint64, addressFrom string, communityID string) ([]byte, error) {
return m.communitiesManager.CreateCommunityTokenDeploymentSignature(ctx, chainID, addressFrom, communityID)
}
func (m *Messenger) getTimesource() common.TimeSource {
return m.transport
}

View File

@ -14,8 +14,10 @@ import (
"github.com/ethereum/go-ethereum/log"
"github.com/status-im/status-go/contracts/community-tokens/assets"
"github.com/status-im/status-go/contracts/community-tokens/collectibles"
communitytokendeployer "github.com/status-im/status-go/contracts/community-tokens/deployer"
"github.com/status-im/status-go/contracts/community-tokens/mastertoken"
"github.com/status-im/status-go/contracts/community-tokens/ownertoken"
communityownertokenregistry "github.com/status-im/status-go/contracts/community-tokens/registry"
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/protocol/protobuf"
"github.com/status-im/status-go/services/utils"
@ -129,12 +131,48 @@ func (api *API) DeployCollectibles(ctx context.Context, chainID uint64, deployme
return DeploymentDetails{address.Hex(), tx.Hash().Hex()}, nil
}
func (api *API) DeployOwnerToken(ctx context.Context, chainID uint64, ownerTokenParameters DeploymentParameters, masterTokenParameters DeploymentParameters, txArgs transactions.SendTxArgs, password string) (DeploymentDetails, error) {
func decodeSignature(sig []byte) (r [32]byte, s [32]byte, v uint8, err error) {
if len(sig) != crypto.SignatureLength {
return [32]byte{}, [32]byte{}, 0, fmt.Errorf("wrong size for signature: got %d, want %d", len(sig), crypto.SignatureLength)
}
copy(r[:], sig[:32])
copy(s[:], sig[32:64])
v = sig[64] + 27
return r, s, v, nil
}
func prepareDeploymentSignatureStruct(signature string, communityID string, addressFrom common.Address) (communitytokendeployer.CommunityTokenDeployerDeploymentSignature, error) {
r, s, v, err := decodeSignature(common.FromHex(signature))
if err != nil {
return communitytokendeployer.CommunityTokenDeployerDeploymentSignature{}, err
}
communityEthAddress, err := convert33BytesPubKeyToEthAddress(communityID)
if err != nil {
return communitytokendeployer.CommunityTokenDeployerDeploymentSignature{}, err
}
communitySignature := communitytokendeployer.CommunityTokenDeployerDeploymentSignature{
V: v,
R: r,
S: s,
Deployer: addressFrom,
Signer: communityEthAddress,
}
return communitySignature, nil
}
func (api *API) DeployOwnerToken(ctx context.Context, chainID uint64,
ownerTokenParameters DeploymentParameters, masterTokenParameters DeploymentParameters,
signature string, communityID string, signerPubKey string,
txArgs transactions.SendTxArgs, password string) (DeploymentDetails, error) {
err := ownerTokenParameters.Validate(false)
if err != nil {
return DeploymentDetails{}, err
}
if len(signerPubKey) <= 0 {
return DeploymentDetails{}, fmt.Errorf("signerPubKey is empty")
}
err = masterTokenParameters.Validate(false)
if err != nil {
return DeploymentDetails{}, err
@ -142,18 +180,32 @@ func (api *API) DeployOwnerToken(ctx context.Context, chainID uint64, ownerToken
transactOpts := txArgs.ToTransactOpts(utils.GetSigner(chainID, api.s.accountsManager, api.s.config.KeyStoreDir, txArgs.From, password))
ethClient, err := api.s.manager.rpcClient.EthClient(chainID)
deployerContractInst, err := api.NewCommunityTokenDeployerInstance(chainID)
if err != nil {
log.Error(err.Error())
return DeploymentDetails{}, err
}
signerPubKey := []byte{}
ownerTokenConfig := communitytokendeployer.CommunityTokenDeployerTokenConfig{
Name: ownerTokenParameters.Name,
Symbol: ownerTokenParameters.Symbol,
BaseURI: ownerTokenParameters.TokenURI,
}
masterTokenConfig := communitytokendeployer.CommunityTokenDeployerTokenConfig{
Name: masterTokenParameters.Name,
Symbol: masterTokenParameters.Symbol,
BaseURI: masterTokenParameters.TokenURI,
}
communitySignature, err := prepareDeploymentSignatureStruct(signature, communityID, common.Address(txArgs.From))
if err != nil {
return DeploymentDetails{}, err
}
log.Debug("Signature:", communitySignature)
tx, err := deployerContractInst.Deploy(transactOpts, ownerTokenConfig, masterTokenConfig, communitySignature, common.FromHex(signerPubKey))
address, tx, _, err := ownertoken.DeployOwnerToken(transactOpts, ethClient, ownerTokenParameters.Name,
ownerTokenParameters.Symbol, ownerTokenParameters.TokenURI,
masterTokenParameters.Name, masterTokenParameters.Symbol,
masterTokenParameters.TokenURI, signerPubKey)
if err != nil {
log.Error(err.Error())
return DeploymentDetails{}, err
@ -171,7 +223,7 @@ func (api *API) DeployOwnerToken(ctx context.Context, chainID uint64, ownerToken
return DeploymentDetails{}, err
}
return DeploymentDetails{address.Hex(), tx.Hash().Hex()}, nil
return DeploymentDetails{"", tx.Hash().Hex()}, nil
}
func (api *API) GetMasterTokenContractAddressFromHash(ctx context.Context, chainID uint64, txHash string) (string, error) {
@ -185,26 +237,57 @@ func (api *API) GetMasterTokenContractAddressFromHash(ctx context.Context, chain
return "", err
}
logMasterTokenCreatedSig := []byte("MasterTokenCreated(address)")
deployerContractInst, err := api.NewCommunityTokenDeployerInstance(chainID)
if err != nil {
return "", err
}
logMasterTokenCreatedSig := []byte("DeployMasterToken(address)")
logMasterTokenCreatedSigHash := crypto.Keccak256Hash(logMasterTokenCreatedSig)
for _, vLog := range receipt.Logs {
if vLog.Topics[0].Hex() == logMasterTokenCreatedSigHash.Hex() {
ownerTokenABI, err := abi.JSON(strings.NewReader(ownertoken.OwnerTokenABI))
event, err := deployerContractInst.ParseDeployMasterToken(*vLog)
if err != nil {
return "", err
}
event := new(ownertoken.OwnerTokenMasterTokenCreated)
err = ownerTokenABI.UnpackIntoInterface(event, "MasterTokenCreated", vLog.Data)
if err != nil {
return "", err
}
return event.MasterToken.Hex(), nil
return event.Arg0.Hex(), nil
}
}
return "", fmt.Errorf("can't find master token address in transaction: %v", txHash)
}
func (api *API) GetOwnerTokenContractAddressFromHash(ctx context.Context, chainID uint64, txHash string) (string, error) {
ethClient, err := api.s.manager.rpcClient.EthClient(chainID)
if err != nil {
return "", err
}
receipt, err := ethClient.TransactionReceipt(ctx, common.HexToHash(txHash))
if err != nil {
return "", err
}
deployerContractInst, err := api.NewCommunityTokenDeployerInstance(chainID)
if err != nil {
return "", err
}
logOwnerTokenCreatedSig := []byte("DeployOwnerToken(address)")
logOwnerTokenCreatedSigHash := crypto.Keccak256Hash(logOwnerTokenCreatedSig)
for _, vLog := range receipt.Logs {
if vLog.Topics[0].Hex() == logOwnerTokenCreatedSigHash.Hex() {
event, err := deployerContractInst.ParseDeployOwnerToken(*vLog)
if err != nil {
return "", err
}
return event.Arg0.Hex(), nil
}
}
return "", fmt.Errorf("can't find owner token address in transaction: %v", txHash)
}
func (api *API) DeployAssets(ctx context.Context, chainID uint64, deploymentParameters DeploymentParameters, txArgs transactions.SendTxArgs, password string) (DeploymentDetails, error) {
err := deploymentParameters.Validate(true)
@ -220,8 +303,9 @@ func (api *API) DeployAssets(ctx context.Context, chainID uint64, deploymentPara
return DeploymentDetails{}, err
}
const decimals = 18
address, tx, _, err := assets.DeployAssets(transactOpts, ethClient, deploymentParameters.Name,
deploymentParameters.Symbol, deploymentParameters.GetSupply())
deploymentParameters.Symbol, decimals, deploymentParameters.GetSupply())
if err != nil {
log.Error(err.Error())
return DeploymentDetails{}, err
@ -244,20 +328,105 @@ func (api *API) DeployAssets(ctx context.Context, chainID uint64, deploymentPara
// Returns gas units + 10%
func (api *API) DeployCollectiblesEstimate(ctx context.Context) (uint64, error) {
gasAmount := uint64(2091605)
// TODO investigate why the code below does not return correct values
/*ethClient, err := api.s.manager.rpcClient.EthClient(420)
if err != nil {
log.Error(err.Error())
return 0, err
}
collectiblesABI, err := abi.JSON(strings.NewReader(collectibles.CollectiblesABI))
if err != nil {
return 0, err
}
data, err := collectiblesABI.Pack("", "name", "SYMBOL", big.NewInt(20), true, false, "tokenUriwhcih is very long asdkfjlsdkjflk",
common.HexToAddress("0x77b48394c650520012795a1a25696d7eb542d110"), common.HexToAddress("0x77b48394c650520012795a1a25696d7eb542d110"))
if err != nil {
return 0, err
}
callMsg := ethereum.CallMsg{
From: common.HexToAddress("0x77b48394c650520012795a1a25696d7eb542d110"),
To: nil,
Value: big.NewInt(0),
Data: data,
}
estimate, err := ethClient.EstimateGas(ctx, callMsg)
if err != nil {
return 0, err
}
return estimate + uint64(float32(estimate)*0.1), nil*/
// TODO compute fee dynamically
// the code above returns too low fees, need to investigate
gasAmount := uint64(2500000)
return gasAmount + uint64(float32(gasAmount)*0.1), nil
}
// Returns gas units + 10%
func (api *API) DeployAssetsEstimate(ctx context.Context) (uint64, error) {
gasAmount := uint64(957483)
// TODO compute fee dynamically
gasAmount := uint64(1500000)
return gasAmount + uint64(float32(gasAmount)*0.1), nil
}
// Returns gas units + 10%
func (api *API) DeployOwnerTokenEstimate(ctx context.Context) (uint64, error) {
ownerGasAmount := uint64(4389457)
return ownerGasAmount + uint64(float32(ownerGasAmount)*0.1), nil
func (api *API) DeployOwnerTokenEstimate(ctx context.Context, chainID uint64, fromAddress string,
ownerTokenParameters DeploymentParameters, masterTokenParameters DeploymentParameters,
signature string, communityID string, signerPubKey string) (uint64, error) {
ethClient, err := api.s.manager.rpcClient.EthClient(chainID)
if err != nil {
log.Error(err.Error())
return 0, err
}
deployerAddress, err := communitytokendeployer.ContractAddress(chainID)
if err != nil {
return 0, err
}
deployerABI, err := abi.JSON(strings.NewReader(communitytokendeployer.CommunityTokenDeployerABI))
if err != nil {
return 0, err
}
ownerTokenConfig := communitytokendeployer.CommunityTokenDeployerTokenConfig{
Name: ownerTokenParameters.Name,
Symbol: ownerTokenParameters.Symbol,
BaseURI: ownerTokenParameters.TokenURI,
}
masterTokenConfig := communitytokendeployer.CommunityTokenDeployerTokenConfig{
Name: masterTokenParameters.Name,
Symbol: masterTokenParameters.Symbol,
BaseURI: masterTokenParameters.TokenURI,
}
communitySignature, err := prepareDeploymentSignatureStruct(signature, communityID, common.HexToAddress(fromAddress))
if err != nil {
return 0, err
}
data, err := deployerABI.Pack("deploy", ownerTokenConfig, masterTokenConfig, communitySignature, common.FromHex(signerPubKey))
if err != nil {
return 0, err
}
toAddr := deployerAddress
fromAddr := common.HexToAddress(fromAddress)
callMsg := ethereum.CallMsg{
From: fromAddr,
To: &toAddr,
Value: big.NewInt(0),
Data: data,
}
estimate, err := ethClient.EstimateGas(ctx, callMsg)
if err != nil {
return 0, err
}
return estimate + uint64(float32(estimate)*0.1), nil
}
func (api *API) NewMasterTokenInstance(chainID uint64, contractAddress string) (*mastertoken.MasterToken, error) {
@ -268,6 +437,26 @@ func (api *API) NewMasterTokenInstance(chainID uint64, contractAddress string) (
return mastertoken.NewMasterToken(common.HexToAddress(contractAddress), backend)
}
func (api *API) NewOwnerTokenInstance(chainID uint64, contractAddress string) (*ownertoken.OwnerToken, error) {
backend, err := api.s.manager.rpcClient.EthClient(chainID)
if err != nil {
return nil, err
}
return ownertoken.NewOwnerToken(common.HexToAddress(contractAddress), backend)
}
func (api *API) NewCommunityTokenDeployerInstance(chainID uint64) (*communitytokendeployer.CommunityTokenDeployer, error) {
return api.s.manager.NewCommunityTokenDeployerInstance(chainID)
}
func (api *API) NewCommunityOwnerTokenRegistryInstance(chainID uint64, contractAddress string) (*communityownertokenregistry.CommunityOwnerTokenRegistry, error) {
backend, err := api.s.manager.rpcClient.EthClient(chainID)
if err != nil {
return nil, err
}
return communityownertokenregistry.NewCommunityOwnerTokenRegistry(common.HexToAddress(contractAddress), backend)
}
func (api *API) NewCollectiblesInstance(chainID uint64, contractAddress string) (*collectibles.Collectibles, error) {
return api.s.manager.NewCollectiblesInstance(chainID, contractAddress)
}
@ -679,3 +868,91 @@ func (api *API) estimateMethod(ctx context.Context, chainID uint64, contractAddr
}
return estimate + uint64(float32(estimate)*0.1), nil
}
// Gets signer public key from smart contract with a given chainId and address
func (api *API) GetSignerPubKey(ctx context.Context, chainID uint64, contractAddress string) (string, error) {
callOpts := &bind.CallOpts{Context: ctx, Pending: false}
contractInst, err := api.NewOwnerTokenInstance(chainID, contractAddress)
if err != nil {
return "", err
}
signerPubKey, err := contractInst.SignerPublicKey(callOpts)
if err != nil {
return "", err
}
return common.Bytes2Hex(signerPubKey), nil
}
// Gets signer public key directly from deployer contract
func (api *API) SafeGetSignerPubKey(ctx context.Context, chainID uint64, communityID string) (string, error) {
// 1. Get Owner Token contract address from deployer contract - SafeGetOwnerTokenAddress()
ownerTokenAddr, err := api.SafeGetOwnerTokenAddress(ctx, chainID, communityID)
if err != nil {
return "", err
}
// 2. Get Signer from owner token contract - GetSignerPubKey()
return api.GetSignerPubKey(ctx, chainID, ownerTokenAddr)
}
// Gets owner token contract address from deployer contract
func (api *API) SafeGetOwnerTokenAddress(ctx context.Context, chainID uint64, communityID string) (string, error) {
callOpts := &bind.CallOpts{Context: ctx, Pending: false}
deployerContractInst, err := api.NewCommunityTokenDeployerInstance(chainID)
if err != nil {
return "", err
}
registryAddr, err := deployerContractInst.DeploymentRegistry(callOpts)
if err != nil {
return "", err
}
registryContractInst, err := api.NewCommunityOwnerTokenRegistryInstance(chainID, registryAddr.Hex())
if err != nil {
return "", err
}
communityEthAddress, err := convert33BytesPubKeyToEthAddress(communityID)
if err != nil {
return "", err
}
ownerTokenAddress, err := registryContractInst.GetEntry(callOpts, communityEthAddress)
return ownerTokenAddress.Hex(), err
}
func (api *API) SetSignerPubKey(ctx context.Context, chainID uint64, contractAddress string, txArgs transactions.SendTxArgs, password string, newSignerPubKey string) (string, error) {
if len(newSignerPubKey) <= 0 {
return "", fmt.Errorf("signerPubKey is empty")
}
transactOpts := txArgs.ToTransactOpts(utils.GetSigner(chainID, api.s.accountsManager, api.s.config.KeyStoreDir, txArgs.From, password))
contractInst, err := api.NewOwnerTokenInstance(chainID, contractAddress)
if err != nil {
return "", err
}
tx, err := contractInst.SetSignerPublicKey(transactOpts, common.FromHex(newSignerPubKey))
if err != nil {
return "", err
}
err = api.s.pendingTracker.TrackPendingTransaction(
wcommon.ChainID(chainID),
tx.Hash(),
common.Address(txArgs.From),
transactions.SetSignerPublicKey,
transactions.AutoDelete,
)
if err != nil {
log.Error("TrackPendingTransaction error", "error", err)
return "", err
}
return tx.Hash().Hex(), nil
}
func (api *API) EstimateSetSignerPubKey(ctx context.Context, chainID uint64, contractAddress string, fromAddress string, newSignerPubKey string) (uint64, error) {
if len(newSignerPubKey) <= 0 {
return 0, fmt.Errorf("signerPubKey is empty")
}
return api.estimateMethod(ctx, chainID, contractAddress, fromAddress, "setSignerPublicKey", newSignerPubKey)
}

View File

@ -6,6 +6,8 @@ import (
"github.com/stretchr/testify/require"
"github.com/ethereum/go-ethereum/common"
"github.com/status-im/status-go/services/wallet/bigint"
)
@ -70,3 +72,25 @@ func TestDeploymentParameters(t *testing.T) {
requiredSupply = infiniteSupplyParams.GetInfiniteSupply()
require.Equal(t, infiniteSupplyParams.GetSupply(), requiredSupply)
}
func TestTypedDataHash(t *testing.T) {
sigHash := common.Hex2Bytes("dd91c30357aafeb2792b5f0facbd83995943c1ea113a906ebbeb58bfeb27dfc2")
domainSep := common.Hex2Bytes("4a672b5a08e88d37f7239165a0c9e03a01196587d52c638c0c99cbee5ba527c8")
contractAddr := "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
signer := "0x54e3922e97e334905fb489be7c5df1f83cb1ce58"
deployer := "0x7c8999dC9a822c1f0Df42023113EDB4FDd543266"
goodHashResult := "0xccbb375343347491706cf4b43796f7b96ccc89c9e191a8b78679daeba1684ec7"
typedHash, err := typedStructuredDataHash(domainSep, signer, deployer, contractAddr, 420)
require.NoError(t, err, "creating typed structured data hash")
require.Equal(t, goodHashResult, typedHash.String())
customTypedHash := customTypedStructuredDataHash(domainSep, sigHash, signer, deployer)
require.Equal(t, goodHashResult, customTypedHash.String())
}
func TestCompressedKeyToEthAddress(t *testing.T) {
ethAddr, err := convert33BytesPubKeyToEthAddress("0x02bcbe39785b55a22383f82ac631ea7500e204627369c4ea01d9296af0ea573f57")
require.NoError(t, err, "converting pub key to address")
require.Equal(t, "0x0A1ec0002dDB927B03049F1aD8D589aBEA4Ba4b3", ethAddr.Hex())
}

View File

@ -2,11 +2,17 @@ package communitytokens
import (
"context"
"fmt"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/math"
"github.com/ethereum/go-ethereum/signer/core/apitypes"
"github.com/status-im/status-go/contracts/community-tokens/assets"
"github.com/status-im/status-go/contracts/community-tokens/collectibles"
communitytokendeployer "github.com/status-im/status-go/contracts/community-tokens/deployer"
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/rpc"
"github.com/status-im/status-go/services/wallet/bigint"
)
@ -29,6 +35,18 @@ func (m *Manager) NewCollectiblesInstance(chainID uint64, contractAddress string
return collectibles.NewCollectibles(common.HexToAddress(contractAddress), backend)
}
func (m *Manager) NewCommunityTokenDeployerInstance(chainID uint64) (*communitytokendeployer.CommunityTokenDeployer, error) {
backend, err := m.rpcClient.EthClient(chainID)
if err != nil {
return nil, err
}
deployerAddr, err := communitytokendeployer.ContractAddress(chainID)
if err != nil {
return nil, err
}
return communitytokendeployer.NewCommunityTokenDeployer(deployerAddr, backend)
}
func (m *Manager) GetCollectiblesContractInstance(chainID uint64, contractAddress string) (*collectibles.Collectibles, error) {
contractInst, err := m.NewCollectiblesInstance(chainID, contractAddress)
if err != nil {
@ -97,3 +115,94 @@ func (m *Manager) GetAssetContractData(chainID uint64, contractAddress string) (
InfiniteSupply: GetInfiniteSupply().Cmp(totalSupply) == 0,
}, nil
}
func convert33BytesPubKeyToEthAddress(pubKey string) (common.Address, error) {
decoded, err := types.DecodeHex(pubKey)
if err != nil {
return common.Address{}, err
}
communityPubKey, err := crypto.DecompressPubkey(decoded)
if err != nil {
return common.Address{}, err
}
return common.Address(crypto.PubkeyToAddress(*communityPubKey)), nil
}
// Simpler version of hashing typed structured data alternative to typedStructuredDataHash. Keeping this for reference.
func customTypedStructuredDataHash(domainSeparator []byte, signatureTypedHash []byte, signer string, deployer string) types.Hash {
// every field should be 32 bytes, eth address is 20 bytes so padding should be added
emptyOffset := [12]byte{}
hashedEncoded := crypto.Keccak256Hash(signatureTypedHash, emptyOffset[:], common.HexToAddress(signer).Bytes(),
emptyOffset[:], common.HexToAddress(deployer).Bytes())
rawData := []byte(fmt.Sprintf("\x19\x01%s%s", domainSeparator, hashedEncoded.Bytes()))
return crypto.Keccak256Hash(rawData)
}
// Returns a typed structured hash according to https://eips.ethereum.org/EIPS/eip-712
// Domain separator from smart contract is used.
func typedStructuredDataHash(domainSeparator []byte, signer string, addressFrom string, deployerContractAddress string, chainID uint64) (types.Hash, error) {
myTypedData := apitypes.TypedData{
Types: apitypes.Types{
"Deploy": []apitypes.Type{
{Name: "signer", Type: "address"},
{Name: "deployer", Type: "address"},
},
"EIP712Domain": []apitypes.Type{
{Name: "name", Type: "string"},
{Name: "version", Type: "string"},
{Name: "chainId", Type: "uint256"},
{Name: "verifyingContract", Type: "address"},
},
},
PrimaryType: "Deploy",
// Domain field should be here to keep correct structure but
// domainSeparator from smart contract is used.
Domain: apitypes.TypedDataDomain{
Name: "CommunityTokenDeployer", // name from Deployer smart contract
Version: "1", // version from Deployer smart contract
ChainId: math.NewHexOrDecimal256(int64(chainID)),
VerifyingContract: deployerContractAddress,
},
Message: apitypes.TypedDataMessage{
"signer": signer,
"deployer": addressFrom,
},
}
typedDataHash, err := myTypedData.HashStruct(myTypedData.PrimaryType, myTypedData.Message)
if err != nil {
return types.Hash{}, err
}
rawData := []byte(fmt.Sprintf("\x19\x01%s%s", domainSeparator, string(typedDataHash)))
return crypto.Keccak256Hash(rawData), nil
}
// Creates
func (m *Manager) DeploymentSignatureDigest(chainID uint64, addressFrom string, communityID string) ([]byte, error) {
callOpts := &bind.CallOpts{Pending: false}
communityEthAddr, err := convert33BytesPubKeyToEthAddress(communityID)
if err != nil {
return nil, err
}
deployerAddr, err := communitytokendeployer.ContractAddress(chainID)
if err != nil {
return nil, err
}
deployerContractInst, err := m.NewCommunityTokenDeployerInstance(chainID)
if err != nil {
return nil, err
}
domainSeparator, err := deployerContractInst.DOMAINSEPARATOR(callOpts)
if err != nil {
return nil, err
}
structedHash, err := typedStructuredDataHash(domainSeparator[:], communityEthAddr.Hex(), addressFrom, deployerAddr.Hex(), chainID)
if err != nil {
return nil, err
}
return structedHash.Bytes(), nil
}

View File

@ -14,6 +14,7 @@ import (
type ServiceInterface interface {
GetCollectibleContractData(chainID uint64, contractAddress string) (*CollectibleContractData, error)
GetAssetContractData(chainID uint64, contractAddress string) (*AssetContractData, error)
DeploymentSignatureDigest(chainID uint64, addressFrom string, communityID string) ([]byte, error)
}
// Collectibles service
@ -70,3 +71,7 @@ func (s *Service) GetCollectibleContractData(chainID uint64, contractAddress str
func (s *Service) GetAssetContractData(chainID uint64, contractAddress string) (*AssetContractData, error) {
return s.manager.GetAssetContractData(chainID, contractAddress)
}
func (s *Service) DeploymentSignatureDigest(chainID uint64, addressFrom string, communityID string) ([]byte, error) {
return s.manager.DeploymentSignatureDigest(chainID, addressFrom, communityID)
}

View File

@ -12,6 +12,7 @@ import (
"github.com/status-im/status-go/contracts/community-tokens/assets"
"github.com/status-im/status-go/contracts/community-tokens/collectibles"
"github.com/status-im/status-go/contracts/community-tokens/mastertoken"
"github.com/status-im/status-go/contracts/community-tokens/ownertoken"
"github.com/status-im/status-go/protocol/communities/token"
"github.com/status-im/status-go/protocol/protobuf"
"github.com/status-im/status-go/services/wallet/bigint"
@ -24,6 +25,32 @@ type TokenInstance interface {
PackMethod(ctx context.Context, methodName string, args ...interface{}) ([]byte, error)
}
// Owner Token
type OwnerTokenInstance struct {
TokenInstance
instance *ownertoken.OwnerToken
}
func (t OwnerTokenInstance) RemoteBurn(transactOpts *bind.TransactOpts, tokenIds []*big.Int) (*types.Transaction, error) {
return nil, fmt.Errorf("remote destruction for owner token not implemented")
}
func (t OwnerTokenInstance) Mint(transactOpts *bind.TransactOpts, walletAddresses []string, amount *bigint.BigInt) (*types.Transaction, error) {
return nil, fmt.Errorf("minting for owner token not implemented")
}
func (t OwnerTokenInstance) SetMaxSupply(transactOpts *bind.TransactOpts, maxSupply *big.Int) (*types.Transaction, error) {
return nil, fmt.Errorf("setting max supply for owner token not implemented")
}
func (t OwnerTokenInstance) PackMethod(ctx context.Context, methodName string, args ...interface{}) ([]byte, error) {
ownerTokenABI, err := abi.JSON(strings.NewReader(ownertoken.OwnerTokenABI))
if err != nil {
return []byte{}, err
}
return ownerTokenABI.Pack(methodName, args...)
}
// Master Token
type MasterTokenInstance struct {
TokenInstance
@ -88,7 +115,7 @@ type AssetInstance struct {
}
func (t AssetInstance) RemoteBurn(transactOpts *bind.TransactOpts, tokenIds []*big.Int) (*types.Transaction, error) {
return nil, fmt.Errorf("remote burn not implemented")
return nil, fmt.Errorf("remote destruction for assets not implemented")
}
// The amount should be in smallest denomination of the asset (like wei) with decimal = 18, eg.
@ -122,6 +149,12 @@ func NewTokenInstance(api *API, chainID uint64, contractAddress string) (TokenIn
return nil, err
}
switch {
case privLevel == token.OwnerLevel:
contractInst, err := api.NewOwnerTokenInstance(chainID, contractAddress)
if err != nil {
return nil, err
}
return &OwnerTokenInstance{instance: contractInst}, nil
case privLevel == token.MasterLevel:
contractInst, err := api.NewMasterTokenInstance(chainID, contractAddress)
if err != nil {

View File

@ -994,6 +994,10 @@ func (api *PublicAPI) SignMessageWithChatKey(ctx context.Context, message string
return api.service.messenger.SignMessage(message)
}
func (api *PublicAPI) CreateCommunityTokenDeploymentSignature(ctx context.Context, chainID uint64, addressFrom string, communityID string) (types.HexBytes, error) {
return api.service.messenger.CreateCommunityTokenDeploymentSignature(ctx, chainID, addressFrom, communityID)
}
// wallet connect session apis
func (api *PublicAPI) AddWalletConnectSession(ctx context.Context, request *requests.AddWalletConnectSession) error {
return api.service.messenger.AddWalletConnectSession(request)

View File

@ -358,6 +358,7 @@ const (
RemoteDestructCollectible PendingTrxType = "RemoteDestructCollectible"
BurnCommunityToken PendingTrxType = "BurnCommunityToken"
DeployOwnerToken PendingTrxType = "DeployOwnerToken"
SetSignerPublicKey PendingTrxType = "SetSignerPublicKey"
)
type PendingTransaction struct {