diff --git a/contracts/erc20/ERC20Facet.sol b/contracts/erc20/ERC20Facet.sol new file mode 100644 index 0000000..7ac497a --- /dev/null +++ b/contracts/erc20/ERC20Facet.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Apache-2.0 + +/** + * Authors: Moonstream Engineering (engineering@moonstream.to) + * GitHub: https://github.com/bugout-dev/dao + * + * This is an implementation of the ERC20 governance token for the Moonstream DAO. + */ + +pragma solidity ^0.8.0; + +import "./ERC20WithCommonStorage.sol"; +import "./LibERC20.sol"; + +contract ERC20Facet is ERC20WithCommonStorage { + constructor() ERC20WithCommonStorage("Moonstream", "MNSTR") {} + + function mint(address account, uint256 amount) external { + LibERC20.enforceIsController(); + _mint(account, amount); + } +} diff --git a/dao/ERC20Facet.py b/dao/ERC20Facet.py new file mode 100644 index 0000000..e5d608a --- /dev/null +++ b/dao/ERC20Facet.py @@ -0,0 +1,414 @@ +# 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 ERC20Facet: + def __init__(self, contract_address: Optional[ChecksumAddress]): + self.contract_name = "ERC20Facet" + self.address = contract_address + self.contract = None + self.abi = get_abi_json("ERC20Facet") + 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 allowance(self, owner: ChecksumAddress, spender: ChecksumAddress) -> Any: + self.assert_contract_is_instantiated() + return self.contract.allowance.call(owner, spender) + + def approve(self, spender: ChecksumAddress, amount: int, transaction_config) -> Any: + self.assert_contract_is_instantiated() + return self.contract.approve(spender, amount, transaction_config) + + def balance_of(self, account: ChecksumAddress) -> Any: + self.assert_contract_is_instantiated() + return self.contract.balanceOf.call(account) + + def decimals(self) -> Any: + self.assert_contract_is_instantiated() + return self.contract.decimals.call() + + def decrease_allowance( + self, spender: ChecksumAddress, subtracted_value: int, transaction_config + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.decreaseAllowance( + spender, subtracted_value, transaction_config + ) + + def increase_allowance( + self, spender: ChecksumAddress, added_value: int, transaction_config + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.increaseAllowance(spender, added_value, transaction_config) + + def mint(self, account: ChecksumAddress, amount: int, transaction_config) -> Any: + self.assert_contract_is_instantiated() + return self.contract.mint(account, amount, transaction_config) + + def name(self) -> Any: + self.assert_contract_is_instantiated() + return self.contract.name.call() + + def symbol(self) -> Any: + self.assert_contract_is_instantiated() + return self.contract.symbol.call() + + def total_supply(self) -> Any: + self.assert_contract_is_instantiated() + return self.contract.totalSupply.call() + + def transfer( + self, recipient: ChecksumAddress, amount: int, transaction_config + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.transfer(recipient, amount, transaction_config) + + def transfer_from( + self, + sender: ChecksumAddress, + recipient: ChecksumAddress, + amount: int, + transaction_config, + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.transferFrom(sender, recipient, amount, 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 = ERC20Facet(None) + result = contract.deploy(transaction_config=transaction_config) + print(result) + + +def handle_allowance(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = ERC20Facet(args.address) + result = contract.allowance(owner=args.owner, spender=args.spender) + print(result) + + +def handle_approve(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = ERC20Facet(args.address) + transaction_config = get_transaction_config(args) + result = contract.approve( + spender=args.spender, amount=args.amount, transaction_config=transaction_config + ) + print(result) + + +def handle_balance_of(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = ERC20Facet(args.address) + result = contract.balance_of(account=args.account) + print(result) + + +def handle_decimals(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = ERC20Facet(args.address) + result = contract.decimals() + print(result) + + +def handle_decrease_allowance(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = ERC20Facet(args.address) + transaction_config = get_transaction_config(args) + result = contract.decrease_allowance( + spender=args.spender, + subtracted_value=args.subtracted_value, + transaction_config=transaction_config, + ) + print(result) + + +def handle_increase_allowance(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = ERC20Facet(args.address) + transaction_config = get_transaction_config(args) + result = contract.increase_allowance( + spender=args.spender, + added_value=args.added_value, + transaction_config=transaction_config, + ) + print(result) + + +def handle_mint(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = ERC20Facet(args.address) + transaction_config = get_transaction_config(args) + result = contract.mint( + account=args.account, amount=args.amount, transaction_config=transaction_config + ) + print(result) + + +def handle_name(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = ERC20Facet(args.address) + result = contract.name() + print(result) + + +def handle_symbol(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = ERC20Facet(args.address) + result = contract.symbol() + print(result) + + +def handle_total_supply(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = ERC20Facet(args.address) + result = contract.total_supply() + print(result) + + +def handle_transfer(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = ERC20Facet(args.address) + transaction_config = get_transaction_config(args) + result = contract.transfer( + recipient=args.recipient, + amount=args.amount, + transaction_config=transaction_config, + ) + print(result) + + +def handle_transfer_from(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = ERC20Facet(args.address) + transaction_config = get_transaction_config(args) + result = contract.transfer_from( + sender=args.sender_arg, + recipient=args.recipient, + amount=args.amount, + transaction_config=transaction_config, + ) + print(result) + + +def generate_cli() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="CLI for ERC20Facet") + 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) + + allowance_parser = subcommands.add_parser("allowance") + add_default_arguments(allowance_parser, False) + allowance_parser.add_argument("--owner", required=True, help="Type: address") + allowance_parser.add_argument("--spender", required=True, help="Type: address") + allowance_parser.set_defaults(func=handle_allowance) + + approve_parser = subcommands.add_parser("approve") + add_default_arguments(approve_parser, True) + approve_parser.add_argument("--spender", required=True, help="Type: address") + approve_parser.add_argument( + "--amount", required=True, help="Type: uint256", type=int + ) + approve_parser.set_defaults(func=handle_approve) + + balance_of_parser = subcommands.add_parser("balance-of") + add_default_arguments(balance_of_parser, False) + balance_of_parser.add_argument("--account", required=True, help="Type: address") + balance_of_parser.set_defaults(func=handle_balance_of) + + decimals_parser = subcommands.add_parser("decimals") + add_default_arguments(decimals_parser, False) + decimals_parser.set_defaults(func=handle_decimals) + + decrease_allowance_parser = subcommands.add_parser("decrease-allowance") + add_default_arguments(decrease_allowance_parser, True) + decrease_allowance_parser.add_argument( + "--spender", required=True, help="Type: address" + ) + decrease_allowance_parser.add_argument( + "--subtracted-value", required=True, help="Type: uint256", type=int + ) + decrease_allowance_parser.set_defaults(func=handle_decrease_allowance) + + increase_allowance_parser = subcommands.add_parser("increase-allowance") + add_default_arguments(increase_allowance_parser, True) + increase_allowance_parser.add_argument( + "--spender", required=True, help="Type: address" + ) + increase_allowance_parser.add_argument( + "--added-value", required=True, help="Type: uint256", type=int + ) + increase_allowance_parser.set_defaults(func=handle_increase_allowance) + + mint_parser = subcommands.add_parser("mint") + add_default_arguments(mint_parser, True) + mint_parser.add_argument("--account", required=True, help="Type: address") + mint_parser.add_argument("--amount", required=True, help="Type: uint256", type=int) + mint_parser.set_defaults(func=handle_mint) + + name_parser = subcommands.add_parser("name") + add_default_arguments(name_parser, False) + name_parser.set_defaults(func=handle_name) + + symbol_parser = subcommands.add_parser("symbol") + add_default_arguments(symbol_parser, False) + symbol_parser.set_defaults(func=handle_symbol) + + total_supply_parser = subcommands.add_parser("total-supply") + add_default_arguments(total_supply_parser, False) + total_supply_parser.set_defaults(func=handle_total_supply) + + transfer_parser = subcommands.add_parser("transfer") + add_default_arguments(transfer_parser, True) + transfer_parser.add_argument("--recipient", required=True, help="Type: address") + transfer_parser.add_argument( + "--amount", required=True, help="Type: uint256", type=int + ) + transfer_parser.set_defaults(func=handle_transfer) + + transfer_from_parser = subcommands.add_parser("transfer-from") + add_default_arguments(transfer_from_parser, True) + transfer_from_parser.add_argument( + "--sender-arg", required=True, help="Type: address" + ) + transfer_from_parser.add_argument( + "--recipient", required=True, help="Type: address" + ) + transfer_from_parser.add_argument( + "--amount", required=True, help="Type: uint256", type=int + ) + transfer_from_parser.set_defaults(func=handle_transfer_from) + + return parser + + +def main() -> None: + parser = generate_cli() + args = parser.parse_args() + args.func(args) + + +if __name__ == "__main__": + main() diff --git a/dao/cli.py b/dao/cli.py index af12ee9..e5a8c25 100644 --- a/dao/cli.py +++ b/dao/cli.py @@ -1,6 +1,6 @@ import argparse -from . import diamond +from . import core, ERC20Facet def main(): @@ -10,8 +10,11 @@ def main(): parser.set_defaults(func=lambda _: parser.print_help()) dao_subparsers = parser.add_subparsers() - diamond_parser = diamond.generate_cli() - dao_subparsers.add_parser("diamond", parents=[diamond_parser], add_help=False) + core_parser = core.generate_cli() + dao_subparsers.add_parser("core", parents=[core_parser], add_help=False) + + moonstream_parser = ERC20Facet.generate_cli() + dao_subparsers.add_parser("moonstream", parents=[moonstream_parser], add_help=False) args = parser.parse_args() args.func(args) diff --git a/dao/diamond.py b/dao/core.py similarity index 76% rename from dao/diamond.py rename to dao/core.py index 5f00854..ec84b03 100644 --- a/dao/diamond.py +++ b/dao/core.py @@ -4,7 +4,7 @@ Generic diamond functionality for Moonstream contracts. import argparse import os -from typing import Any, Dict, List, Set +from typing import Any, Dict, List, Optional, Set from brownie import network @@ -13,12 +13,14 @@ from . import ( Diamond, DiamondCutFacet, DiamondLoupeFacet, + ERC20Facet, OwnershipFacet, ) FACETS: Dict[str, Any] = { "DiamondCutFacet": DiamondCutFacet, "DiamondLoupeFacet": DiamondLoupeFacet, + "ERC20Facet": ERC20Facet, "OwnershipFacet": OwnershipFacet, } @@ -26,6 +28,7 @@ FACET_PRECEDENCE: List[str] = [ "DiamondCutFacet", "OwnershipFacet", "DiamondLoupeFacet", + "ERC20Facet", ] FACET_ACTIONS: Dict[str, int] = {"add": 0, "replace": 1, "remove": 2} @@ -39,6 +42,8 @@ def facet_cut( facet_address: str, action: str, transaction_config: Dict[str, Any], + ignore_methods: Optional[List[str]] = None, + ignore_selectors: Optional[List[str]] = None, ) -> Any: """ Cuts the given facet onto the given Diamond contract. @@ -53,6 +58,11 @@ def facet_cut( action in FACET_ACTIONS ), f"Invalid cut action: {action}. Choices: {','.join(FACET_ACTIONS)}." + if ignore_methods is None: + ignore_methods = [] + if ignore_selectors is None: + ignore_selectors = [] + project_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) abis = abi.project_abis(project_dir) @@ -70,9 +80,13 @@ def facet_cut( facet_abi = abis.get(facet_name, []) for item in facet_abi: if item["type"] == "function": - function_selector = abi.encode_function_signature(item) - if function_selector not in reserved_selectors: - facet_function_selectors.append(function_selector) + if item["name"] not in ignore_methods: + function_selector = abi.encode_function_signature(item) + if ( + function_selector not in reserved_selectors + and function_selector not in ignore_selectors + ): + facet_function_selectors.append(function_selector) target_address = facet_address if FACET_ACTIONS[action] == 2: @@ -98,7 +112,15 @@ def handle_facet_cut(args: argparse.Namespace) -> None: facet_name = args.facet_name facet_address = args.facet_address transaction_config = Diamond.get_transaction_config(args) - facet_cut(diamond_address, facet_name, facet_address, action, transaction_config) + facet_cut( + diamond_address, + facet_name, + facet_address, + action, + transaction_config, + ignore_methods=args.ignore_methods, + ignore_selectors=args.ignore_selectors, + ) def generate_cli() -> argparse.ArgumentParser: @@ -131,6 +153,16 @@ def generate_cli() -> argparse.ArgumentParser: choices=FACET_ACTIONS, help="Diamond cut action to take on entire facet", ) + facet_cut_parser.add_argument( + "--ignore-methods", + nargs="+", + help="Names of methods to ignore when cutting a facet onto or off of the diamond", + ) + facet_cut_parser.add_argument( + "--ignore-selectors", + nargs="+", + help="Method selectors to ignore when cutting a facet onto or off of the diamond", + ) facet_cut_parser.set_defaults(func=handle_facet_cut) DiamondCutFacet_parser = DiamondCutFacet.generate_cli()