Added ERC20 initialization when facet is cut onto diamond

[WIP] ERC20 deployment checklist template
[WIP] clean up code a bit
pull/7/head
Neeraj Kashyap 2021-12-17 13:01:51 -08:00
rodzic 3ac420c409
commit 7b832c785b
12 zmienionych plików z 587 dodań i 4 usunięć

Wyświetl plik

@ -0,0 +1,28 @@
# Deploy a diamond proxy
Moonstream DAO uses the EIP2535 Diamond proxy to manage each of its smart contracts.
This checklist describes how to deploy the proxy contract.
## Environment variables
1. `export DAO_NETWORK=<desired brownie network>`
2. `export DAO_OWNER=<path to keystore file for owner account>`
3. `export DAO_OWNER_ADDRESS=$(jq -r .address $DAO_OWNER)`
4. `export GAS_PRICE="<N> gwei"`
5. `export CONFIRMATIONS=<M>`
6. `export OUTPUT_FILE=<path to JSON file in which to store diamond addresses>`
## Deploy diamond proxy
- [ ] Deploy diamond with all core facets
```bash
dao core gogogo \
--network $DAO_NETWORK \
--sender $DAO_OWNER \
--gas-price "$GAS_PRICE" \
--confirmations $CONFIRMATIONS \
--owner $DAO_OWNER_ADDRESS \
--outfile $OUTPUT_FILE
```

Wyświetl plik

@ -0,0 +1,78 @@
# Deploy the Moonstream governance token
The Moonstream DAO governance token is deployed as an EIP2535 Diamond proxy contract with an ERC20
facet attached to it.
This checklist describes how to deploy the token.
## Deployed addresses
You will modify this section as you go through the checklist
### Diamond addresses
```json
```
### ERC20Facet address
``
## Environment variables
1. `export DAO_NETWORK=<desired brownie network>`
2. `export DAO_OWNER=<path to keystore file for owner account>`
3. `export DAO_OWNER_ADDRESS=$(jq -r .address $DAO_OWNER)`
4. `export GAS_PRICE="<N> gwei"`
5. `export CONFIRMATIONS=<M>`
6. `export MOONSTREAM_ADDRESSES=<path to JSON file in which to store diamond addresses>`
## Deploy diamond proxy
- [ ] Deploy diamond with all core facets
```bash
dao core gogogo \
--network $DAO_NETWORK \
--sender $DAO_OWNER \
--gas-price "$GAS_PRICE" \
--confirmations $CONFIRMATIONS \
--owner $DAO_OWNER_ADDRESS \
--outfile $MOONSTREAM_ADDRESSES
```
- [ ] Store JSON output under `Deployed addresses / Diamond addresses` above.
## Attach ERC20 functionality
- [ ] Export diamond proxy address: `export MOONSTREAM_DIAMOND="$(jq -r .Diamond $MOONSTREAM_ADDRESSES)"`
- [ ] Deploy `ERC20Facet` contract.
```bash
dao moonstream deploy \
--network $DAO_NETWORK \
--sender $DAO_OWNER \
--gas-price "$GAS_PRICE" \
--confirmations $CONFIRMATIONS
```
- [ ] Export address of deployed contract as `export ERC20FACET_ADDRESS=<address>`
- [ ] Store address of deployed contract under `Deployed addresses / ERC20Facet address` above
- [ ] Attach `ERC20Facet` to diamond:
```bash
dao core facet-cut \
--address $MOONSTREAM_DIAMOND \
--network $DAO_NETWORK \
--sender $DAO_OWNER \
--gas-price "$GAS_PRICE" \
--confirmations $CONFIRMATIONS \
--facet-name ERC20Facet \
--facet-address $ERC20FACET_ADDRESS \
--action add
```
- [ ] Check the ERC20 name of the diamond contract

Wyświetl plik

@ -11,9 +11,10 @@ pragma solidity ^0.8.0;
import "./ERC20WithCommonStorage.sol";
import "./LibERC20.sol";
import "../diamond/libraries/LibDiamond.sol";
contract ERC20Facet is ERC20WithCommonStorage {
constructor() ERC20WithCommonStorage("Moonstream", "MNSTR") {}
constructor() {}
function mint(address account, uint256 amount) external {
LibERC20.enforceIsController();

Wyświetl plik

@ -0,0 +1,27 @@
// SPDX-License-Identifier: Apache-2.0
/**
* Authors: Moonstream Engineering (engineering@moonstream.to)
* GitHub: https://github.com/bugout-dev/dao
*
* Initializer for Moonstream DAO governance token. Used when mounting a new ERC20Facet onto its
* diamond proxy.
*/
pragma solidity ^0.8.0;
import "@openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import "../diamond/libraries/LibDiamond.sol";
import "./LibERC20.sol";
contract ERC20Initializer {
function init() external {
LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
ds.supportedInterfaces[type(IERC20).interfaceId] = true;
LibERC20.ERC20Storage storage es = LibERC20.erc20Storage();
es.controller = msg.sender;
es.name = "Moonstream DAO";
es.symbol = "MNSTR";
}
}

Wyświetl plik

@ -32,7 +32,10 @@ contract ERC20WithCommonStorage is Context, IERC20, IERC20Metadata {
* All two of these values are immutable: they can only be set once during
* construction.
*/
constructor(string memory name_, string memory symbol_) {
function setERC20Metadata(string memory name_, string memory symbol_)
external
{
LibERC20.enforceIsController();
LibERC20.ERC20Storage storage es = LibERC20.erc20Storage();
es.name = name_;
es.symbol = symbol_;

Wyświetl plik

@ -128,6 +128,10 @@ class ERC20Facet:
self.assert_contract_is_instantiated()
return self.contract.name.call()
def set_erc20_metadata(self, name_: str, symbol_: str, transaction_config) -> Any:
self.assert_contract_is_instantiated()
return self.contract.setERC20Metadata(name_, symbol_, transaction_config)
def symbol(self) -> Any:
self.assert_contract_is_instantiated()
return self.contract.symbol.call()
@ -271,6 +275,18 @@ def handle_name(args: argparse.Namespace) -> None:
print(result)
def handle_set_erc20_metadata(args: argparse.Namespace) -> None:
network.connect(args.network)
contract = ERC20Facet(args.address)
transaction_config = get_transaction_config(args)
result = contract.set_erc20_metadata(
name_=args.name_arg,
symbol_=args.symbol_arg,
transaction_config=transaction_config,
)
print(result)
def handle_symbol(args: argparse.Namespace) -> None:
network.connect(args.network)
contract = ERC20Facet(args.address)
@ -372,6 +388,16 @@ def generate_cli() -> argparse.ArgumentParser:
add_default_arguments(name_parser, False)
name_parser.set_defaults(func=handle_name)
set_erc20_metadata_parser = subcommands.add_parser("set-erc20-metadata")
add_default_arguments(set_erc20_metadata_parser, True)
set_erc20_metadata_parser.add_argument(
"--name-arg", required=True, help="Type: string", type=str
)
set_erc20_metadata_parser.add_argument(
"--symbol-arg", required=True, help="Type: string", type=str
)
set_erc20_metadata_parser.set_defaults(func=handle_set_erc20_metadata)
symbol_parser = subcommands.add_parser("symbol")
add_default_arguments(symbol_parser, False)
symbol_parser.set_defaults(func=handle_symbol)

Wyświetl plik

@ -0,0 +1,175 @@
# Code generated by moonworm : https://github.com/bugout-dev/moonworm
# Moonworm version : 0.1.8
import argparse
import json
import os
from pathlib import Path
from typing import Any, Dict, List, Optional, Union
from brownie import Contract, network, project
from brownie.network.contract import ContractContainer
from eth_typing.evm import ChecksumAddress
PROJECT_DIRECTORY = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
BUILD_DIRECTORY = os.path.join(PROJECT_DIRECTORY, "build", "contracts")
def boolean_argument_type(raw_value: str) -> bool:
TRUE_VALUES = ["1", "t", "y", "true", "yes"]
FALSE_VALUES = ["0", "f", "n", "false", "no"]
if raw_value.lower() in TRUE_VALUES:
return True
elif raw_value.lower() in FALSE_VALUES:
return False
raise ValueError(
f"Invalid boolean argument: {raw_value}. Value must be one of: {','.join(TRUE_VALUES + FALSE_VALUES)}"
)
def bytes_argument_type(raw_value: str) -> bytes:
return raw_value.encode()
def get_abi_json(abi_name: str) -> List[Dict[str, Any]]:
abi_full_path = os.path.join(BUILD_DIRECTORY, f"{abi_name}.json")
if not os.path.isfile(abi_full_path):
raise IOError(
f"File does not exist: {abi_full_path}. Maybe you have to compile the smart contracts?"
)
with open(abi_full_path, "r") as ifp:
build = json.load(ifp)
abi_json = build.get("abi")
if abi_json is None:
raise ValueError(f"Could not find ABI definition in: {abi_full_path}")
return abi_json
def contract_from_build(abi_name: str) -> ContractContainer:
# This is workaround because brownie currently doesn't support loading the same project multiple
# times. This causes problems when using multiple contracts from the same project in the same
# python project.
PROJECT = project.main.Project("moonworm", Path(PROJECT_DIRECTORY))
abi_full_path = os.path.join(BUILD_DIRECTORY, f"{abi_name}.json")
if not os.path.isfile(abi_full_path):
raise IOError(
f"File does not exist: {abi_full_path}. Maybe you have to compile the smart contracts?"
)
with open(abi_full_path, "r") as ifp:
build = json.load(ifp)
return ContractContainer(PROJECT, build)
class ERC20Initializer:
def __init__(self, contract_address: Optional[ChecksumAddress]):
self.contract_name = "ERC20Initializer"
self.address = contract_address
self.contract = None
self.abi = get_abi_json("ERC20Initializer")
if self.address is not None:
self.contract: Optional[Contract] = Contract.from_abi(
self.contract_name, self.address, self.abi
)
def deploy(self, transaction_config):
contract_class = contract_from_build(self.contract_name)
deployed_contract = contract_class.deploy(transaction_config)
self.address = deployed_contract.address
self.contract = deployed_contract
def assert_contract_is_instantiated(self) -> None:
if self.contract is None:
raise Exception("contract has not been instantiated")
def init(self, transaction_config) -> Any:
self.assert_contract_is_instantiated()
return self.contract.init(transaction_config)
def get_transaction_config(args: argparse.Namespace) -> Dict[str, Any]:
signer = network.accounts.load(args.sender, args.password)
transaction_config: Dict[str, Any] = {"from": signer}
if args.gas_price is not None:
transaction_config["gas_price"] = args.gas_price
if args.confirmations is not None:
transaction_config["required_confs"] = args.confirmations
return transaction_config
def add_default_arguments(parser: argparse.ArgumentParser, transact: bool) -> None:
parser.add_argument(
"--network", required=True, help="Name of brownie network to connect to"
)
parser.add_argument(
"--address", required=False, help="Address of deployed contract to connect to"
)
if not transact:
return
parser.add_argument(
"--sender", required=True, help="Path to keystore file for transaction sender"
)
parser.add_argument(
"--password",
required=False,
help="Password to keystore file (if you do not provide it, you will be prompted for it)",
)
parser.add_argument(
"--gas-price", default=None, help="Gas price at which to submit transaction"
)
parser.add_argument(
"--confirmations",
type=int,
default=None,
help="Number of confirmations to await before considering a transaction completed",
)
def handle_deploy(args: argparse.Namespace) -> None:
network.connect(args.network)
transaction_config = get_transaction_config(args)
contract = ERC20Initializer(None)
result = contract.deploy(transaction_config=transaction_config)
print(result)
def handle_init(args: argparse.Namespace) -> None:
network.connect(args.network)
contract = ERC20Initializer(args.address)
transaction_config = get_transaction_config(args)
result = contract.init(transaction_config=transaction_config)
print(result)
def generate_cli() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="CLI for ERC20Initializer")
parser.set_defaults(func=lambda _: parser.print_help())
subcommands = parser.add_subparsers()
deploy_parser = subcommands.add_parser("deploy")
add_default_arguments(deploy_parser, True)
deploy_parser.set_defaults(func=handle_deploy)
init_parser = subcommands.add_parser("init")
add_default_arguments(init_parser, True)
init_parser.set_defaults(func=handle_init)
return parser
def main() -> None:
parser = generate_cli()
args = parser.parse_args()
args.func(args)
if __name__ == "__main__":
main()

Wyświetl plik

@ -1,6 +1,6 @@
import argparse
from . import core, ERC20Facet
from . import core, ERC20Facet, ERC20Initializer
def main():
@ -16,6 +16,13 @@ def main():
moonstream_parser = ERC20Facet.generate_cli()
dao_subparsers.add_parser("moonstream", parents=[moonstream_parser], add_help=False)
moonstream_initializer_parser = ERC20Initializer.generate_cli()
dao_subparsers.add_parser(
"moonstream-initializer",
parents=[moonstream_initializer_parser],
add_help=False,
)
args = parser.parse_args()
args.func(args)

Wyświetl plik

@ -3,7 +3,9 @@ Generic diamond functionality for Moonstream contracts.
"""
import argparse
import json
import os
import sys
from typing import Any, Dict, List, Optional, Set
from brownie import network
@ -14,6 +16,7 @@ from . import (
DiamondCutFacet,
DiamondLoupeFacet,
ERC20Facet,
ERC20Initializer,
OwnershipFacet,
)
@ -42,6 +45,7 @@ def facet_cut(
facet_address: str,
action: str,
transaction_config: Dict[str, Any],
initializer_address: str = ZERO_ADDRESS,
ignore_methods: Optional[List[str]] = None,
ignore_selectors: Optional[List[str]] = None,
) -> Any:
@ -98,13 +102,96 @@ def facet_cut(
facet_function_selectors,
]
calldata = b""
if facet_name == "ERC20Facet":
if initializer_address != ZERO_ADDRESS and action != "remove":
erc20_initializer = ERC20Initializer.ERC20Initializer(initializer_address)
calldata = erc20_initializer.contract.init.encode_input()
diamond = DiamondCutFacet.DiamondCutFacet(diamond_address)
transaction = diamond.diamond_cut(
[diamond_cut_action], ZERO_ADDRESS, b"", transaction_config
[diamond_cut_action], initializer_address, calldata, transaction_config
)
return transaction
def gogogo(owner_address: str, transaction_config: Dict[str, Any]) -> Dict[str, Any]:
"""
Deploy diamond along with all its basic facets and attach those facets to the diamond.
Returns addresses of all the deployed contracts with the contract names as keys.
"""
result: Dict[str, Any] = {}
try:
diamond_cut_facet = DiamondCutFacet.DiamondCutFacet(None)
diamond_cut_facet.deploy(transaction_config)
except Exception as e:
print(e)
result["error"] = "Failed to deploy DiamondCutFacet"
return result
result["DiamondCutFacet"] = diamond_cut_facet.address
try:
diamond = Diamond.Diamond(None)
diamond.deploy(owner_address, diamond_cut_facet.address, transaction_config)
except Exception as e:
print(e)
result["error"] = "Failed to deploy Diamond"
return result
result["Diamond"] = diamond.address
try:
diamond_loupe_facet = DiamondLoupeFacet.DiamondLoupeFacet(None)
diamond_loupe_facet.deploy(transaction_config)
except Exception as e:
print(e)
result["error"] = "Failed to deploy DiamondLoupeFacet"
return result
result["DiamondLoupeFacet"] = diamond_loupe_facet.address
try:
ownership_facet = OwnershipFacet.OwnershipFacet(None)
ownership_facet.deploy(transaction_config)
except Exception as e:
print(e)
result["error"] = "Failed to deploy OwnershipFacet"
return result
result["OwnershipFacet"] = ownership_facet.address
result["attached"] = []
try:
facet_cut(
diamond.address,
"DiamondLoupeFacet",
diamond_loupe_facet.address,
"add",
transaction_config,
)
except Exception as e:
print(e)
result["error"] = "Failed to attach DiamondLoupeFacet"
return result
result["attached"].append("DiamondLoupeFacet")
try:
facet_cut(
diamond.address,
"OwnershipFacet",
ownership_facet.address,
"add",
transaction_config,
)
except Exception as e:
print(e)
result["error"] = "Failed to attach OwnershipFacet"
return result
result["attached"].append("OwnershipFacet")
return result
def handle_facet_cut(args: argparse.Namespace) -> None:
network.connect(args.network)
diamond_address = args.address
@ -118,11 +205,23 @@ def handle_facet_cut(args: argparse.Namespace) -> None:
facet_address,
action,
transaction_config,
initializer_address=args.initializer_address,
ignore_methods=args.ignore_methods,
ignore_selectors=args.ignore_selectors,
)
def handle_gogogo(args: argparse.Namespace) -> None:
network.connect(args.network)
owner_address = args.owner
transaction_config = Diamond.get_transaction_config(args)
result = gogogo(owner_address, transaction_config)
if args.outfile is not None:
with args.outfile:
json.dump(result, args.outfile)
json.dump(result, sys.stdout, indent=4)
def generate_cli() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="CLI to manage Moonstream DAO diamond contracts",
@ -153,6 +252,11 @@ def generate_cli() -> argparse.ArgumentParser:
choices=FACET_ACTIONS,
help="Diamond cut action to take on entire facet",
)
facet_cut_parser.add_argument(
"--initializer-address",
default=ZERO_ADDRESS,
help=f"Address of contract to run as initializer after cut (default: {ZERO_ADDRESS})",
)
facet_cut_parser.add_argument(
"--ignore-methods",
nargs="+",
@ -165,6 +269,20 @@ def generate_cli() -> argparse.ArgumentParser:
)
facet_cut_parser.set_defaults(func=handle_facet_cut)
gogogo_parser = subcommands.add_parser("gogogo")
Diamond.add_default_arguments(gogogo_parser, transact=True)
gogogo_parser.add_argument(
"--owner", required=True, help="Address of owner of diamond proxy"
)
gogogo_parser.add_argument(
"-o",
"--outfile",
type=argparse.FileType("w"),
default=None,
help="(Optional) file to write deployed addresses to",
)
gogogo_parser.set_defaults(func=handle_gogogo)
DiamondCutFacet_parser = DiamondCutFacet.generate_cli()
subcommands.add_parser(
"diamond-cut", parents=[DiamondCutFacet_parser], add_help=False

33
dao/test_core.py 100644
Wyświetl plik

@ -0,0 +1,33 @@
import unittest
from brownie import accounts, network
from dao.core import gogogo
class MoonstreamDAOTestCase(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
try:
network.connect()
except:
pass
cls.contracts = gogogo(accounts[0], {"from": accounts[0]})
class TestCoreDeployment(MoonstreamDAOTestCase):
def test_gogogo(self):
self.assertIn("DiamondCutFacet", self.contracts)
self.assertIn("Diamond", self.contracts)
self.assertIn("DiamondLoupeFacet", self.contracts)
self.assertIn("OwnershipFacet", self.contracts)
self.assertIn("attached", self.contracts)
self.assertListEqual(
self.contracts["attached"],
["DiamondLoupeFacet", "OwnershipFacet"],
)
if __name__ == "__main__":
unittest.main()

Wyświetl plik

@ -0,0 +1,69 @@
from brownie import accounts
from . import ERC20Facet, ERC20Initializer
from .core import facet_cut
from .test_core import MoonstreamDAOTestCase
class TestDeployment(MoonstreamDAOTestCase):
def test_add_and_replace(self):
initializer = ERC20Initializer.ERC20Initializer(None)
initializer.deploy({"from": accounts[0]})
erc20_facet = ERC20Facet.ERC20Facet(None)
erc20_facet.deploy({"from": accounts[0]})
diamond_address = self.contracts["Diamond"]
facet_cut(
diamond_address,
"ERC20Facet",
erc20_facet.address,
"add",
{"from": accounts[0]},
initializer.address,
)
diamond_erc20 = ERC20Facet.ERC20Facet(diamond_address)
name = diamond_erc20.name()
expected_name = "Moonstream DAO"
self.assertEqual(name, expected_name)
symbol = diamond_erc20.symbol()
expected_symbol = "MNSTR"
self.assertEqual(symbol, expected_symbol)
decimals = diamond_erc20.decimals()
expected_decimals = 18
self.assertEqual(decimals, expected_decimals)
with self.assertRaises(Exception):
diamond_erc20.set_erc20_metadata("LOL", "ROFL", {"from": accounts[1]})
diamond_erc20.set_erc20_metadata("LOL", "ROFL", {"from": accounts[0]})
name = diamond_erc20.name()
expected_name = "LOL"
self.assertEqual(name, expected_name)
symbol = diamond_erc20.symbol()
expected_symbol = "ROFL"
self.assertEqual(symbol, expected_symbol)
new_erc20_facet = ERC20Facet.ERC20Facet(None)
new_erc20_facet.deploy({"from": accounts[0]})
facet_cut(
diamond_address,
"ERC20Facet",
new_erc20_facet.address,
"replace",
{"from": accounts[0]},
initializer.address,
)
name = diamond_erc20.name()
expected_name = "Moonstream DAO"
self.assertEqual(name, expected_name)
symbol = diamond_erc20.symbol()
expected_symbol = "MNSTR"
self.assertEqual(symbol, expected_symbol)

18
test.sh 100755
Wyświetl plik

@ -0,0 +1,18 @@
#!/usr/bin/env sh
# Expects a Python environment to be active in which `dao` has been installed for development.
# You can set up the local copy of `dao` for development using:
# pip install -e .[dev]
usage() {
echo "Usage: $0"
}
if [ "$1" = "-h" ] || [ "$1" = "--help" ]
then
usage
exit 2
fi
brownie compile
python -m unittest discover