Merge pull request #7 from bugout-dev/erc20

The Moonstream DAO governance token
pull/10/head
Neeraj Kashyap 2021-12-18 08:47:33 -08:00 zatwierdzone przez GitHub
commit af85c349b1
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
16 zmienionych plików z 2146 dodań i 162 usunięć

Wyświetl plik

@ -1,2 +1,16 @@
# dao
Moonstream DAO
## Development environment
### VSCode setup
If you are using the Solidity extension in VSCode, merge the following snippet into your settings.json:
```json
{
"solidity.remappings": [
"@openzeppelin-contracts/=<path to your home directory>/.brownie/packages/OpenZeppelin/openzeppelin-contracts@4.3.2"
]
}
```

Wyświetl plik

@ -0,0 +1,144 @@
# 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
{
"DiamondCutFacet": "0x65f8857B204968c492B17344E1139229bD3382e3",
"Diamond": "0x02620263be8A046Ca4812723596934AA20D7DC3C",
"DiamondLoupeFacet": "0xeFdFbAA07AF132AD4b319054d91a2F487b009003",
"OwnershipFacet": "0xA9E3B4BF878d66E213A988B761Ac0774bFc0F1c8",
"attached": [
"DiamondLoupeFacet",
"OwnershipFacet"
]
}
```
### `ERC20Initializer` address
```
export ERC20INITIALIZER_ADDRESS=0x6995cA60BE357a72bFAC88a24A05E978637f7Ffb
```
### `ERC20Facet` address
```
export ERC20FACET_ADDRESS=0xC4E53007B5319E73878E4209450A41307Db9de5C
```
## Environment variables
- [x] `export DAO_NETWORK=mumbai`
- [x] `export DAO_OWNER=.secrets/dao-dev.json`
- [x] `export DAO_OWNER_ADDRESS=$(jq -r .address $DAO_OWNER)`
- [x] `export GAS_PRICE="60 gwei"`
- [x] `export CONFIRMATIONS=2`
- [x] `export MOONSTREAM_ADDRESSES=.secrets/moonstream-mumbai-diamond.json`
- [x] `export MOONSTREAM_TOTAL_SUPPLY=10000000000`
## Deploy diamond proxy
- [x] 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
```
- [x] Store JSON output under `Deployed addresses / Diamond addresses` above.
- [x] Export diamond proxy address: `export MOONSTREAM_DIAMOND="$(jq -r .Diamond $MOONSTREAM_ADDRESSES)"`
## Deploy `ERC20Initializer`
- [x] Deploy `ERC20Initializer` contract
```bash
dao moonstream-initializer deploy \
--network $DAO_NETWORK \
--sender $DAO_OWNER \
--gas-price "$GAS_PRICE" \
--confirmations $CONFIRMATIONS
```
- [x] Export address of deployed contract as `export ERC20INITIALIZER_ADDRESS=0x6995cA60BE357a72bFAC88a24A05E978637f7Ffb`
- [x] Store address of deployed contract under `Deployed addresses / ERC20Initializer address` above
## Deploy `ERC20Facet`
- [x] Deploy `ERC20Facet` contract
```bash
dao moonstream deploy \
--network $DAO_NETWORK \
--sender $DAO_OWNER \
--gas-price "$GAS_PRICE" \
--confirmations $CONFIRMATIONS
```
- [x] Export address of deployed contract as `export ERC20FACET_ADDRESS=0xC4E53007B5319E73878E4209450A41307Db9de5C`
- [x] Store address of deployed contract under `Deployed addresses / ERC20Facet address` above
- [x] 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 \
--initializer-address $ERC20INITIALIZER_ADDRESS
```
- [x] Check the ERC20 name of the diamond contract: `dao moonstream name --network $DAO_NETWORK --address $MOONSTREAM_DIAMOND`
- [x] Name is `Moonstream DAO`
- [x] Check the ERC20 symbol of the diamond contract: `dao moonstream symbol --network $DAO_NETWORK --address $MOONSTREAM_DIAMOND`
- [x] Symbol is `MNSTR`
## Mint Moonstream tokens
- [x] Mint `MOONSTREAM_TOTAL_SUPPLY` worth of tokens to self.
```bash
dao moonstream mint \
--network $DAO_NETWORK \
--address $MOONSTREAM_DIAMOND \
--sender $DAO_OWNER \
--gas-price "$GAS_PRICE" \
--confirmations $CONFIRMATIONS \
--account $DAO_OWNER_ADDRESS \
--amount $MOONSTREAM_TOTAL_SUPPLY
```
- [x] Check the total supply of the diamond contract: `dao moonstream total-supply --network $DAO_NETWORK --address $MOONSTREAM_DIAMOND`
- [x] Total supply should be equal to value of `MOONSTREAM_TOTAL_SUPPLY`
- [x] Check balance of DAO owner address: `dao moonstream balance-of --network $DAO_NETWORK --address $MOONSTREAM_DIAMOND --account $DAO_OWNER_ADDRESS`
- [x] Balance should be equal to value of `MOONSTREAM_TOTAL_SUPPLY`

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,134 @@
# 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
```
### `ERC20Initializer` address
```
export ERC20INITIALIZER_ADDRESS=""
```
### `ERC20Facet` address
```
export ERC20FACET_ADDRESS=""
```
## Environment variables
- [ ] `export DAO_NETWORK=<desired brownie network>`
- [ ] `export DAO_OWNER=<path to keystore file for owner account>`
- [ ] `export DAO_OWNER_ADDRESS=$(jq -r .address $DAO_OWNER)`
- [ ] `export GAS_PRICE="<N> gwei"`
- [ ] `export CONFIRMATIONS=<M>`
- [ ] `export MOONSTREAM_ADDRESSES=<path to JSON file in which to store diamond addresses>`
- [ ] `export MOONSTREAM_TOTAL_SUPPLY=<number of tokens to mint>`
## 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.
- [ ] Export diamond proxy address: `export MOONSTREAM_DIAMOND="$(jq -r .Diamond $MOONSTREAM_ADDRESSES)"`
## Deploy `ERC20Initializer`
- [ ] Deploy `ERC20Initializer` contract
```bash
dao moonstream-initializer deploy \
--network $DAO_NETWORK \
--sender $DAO_OWNER \
--gas-price "$GAS_PRICE" \
--confirmations $CONFIRMATIONS
```
- [ ] Export address of deployed contract as `export ERC20INITIALIZER_ADDRESS=<address>`
- [ ] Store address of deployed contract under `Deployed addresses / ERC20Initializer address` above
## Deploy `ERC20Facet`
- [ ] 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 \
--initializer-address $ERC20INITIALIZER_ADDRESS
```
- [ ] Check the ERC20 name of the diamond contract: `dao moonstream name --network $DAO_NETWORK --address $MOONSTREAM_DIAMOND`
- [ ] Name is `Moonstream DAO`
- [ ] Check the ERC20 symbol of the diamond contract: `dao moonstream symbol --network $DAO_NETWORK --address $MOONSTREAM_DIAMOND`
- [ ] Symbol is `MNSTR`
## Mint Moonstream tokens
- [ ] Mint `MOONSTREAM_TOTAL_SUPPLY` worth of tokens to self.
```bash
dao moonstream mint \
--network $DAO_NETWORK \
--address $MOONSTREAM_DIAMOND \
--sender $DAO_OWNER \
--gas-price "$GAS_PRICE" \
--confirmations $CONFIRMATIONS \
--account $DAO_OWNER_ADDRESS \
--amount $MOONSTREAM_TOTAL_SUPPLY
```
- [ ] Check the total supply of the diamond contract: `dao moonstream total-supply --network $DAO_NETWORK --address $MOONSTREAM_DIAMOND`
- [ ] Total supply should be equal to value of `MOONSTREAM_TOTAL_SUPPLY`
- [ ] Check balance of DAO owner address: `dao moonstream balance-of --network $DAO_NETWORK --address $MOONSTREAM_DIAMOND --account $DAO_OWNER_ADDRESS`
- [ ] Balance should be equal to value of `MOONSTREAM_TOTAL_SUPPLY`

Wyświetl plik

@ -0,0 +1,23 @@
// 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";
import "../diamond/libraries/LibDiamond.sol";
contract ERC20Facet is ERC20WithCommonStorage {
constructor() {}
function mint(address account, uint256 amount) external {
LibERC20.enforceIsController();
_mint(account, amount);
}
}

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

@ -0,0 +1,412 @@
// SPDX-License-Identifier: Apache-2.0
/**
* Authors: Moonstream Engineering (engineering@moonstream.to)
* GitHub: https://github.com/bugout-dev/dao
*
* This is an implementation of an ERC20 contract adapted to use a common storage structure suited to
* DELEGATECALL proxies.
*
* For example, it can be used as a facet on an EIP2535 Diamond proxy.
*
* Our implementation is adapted from the OpenZeppelin implementation at
* https://github.com/OpenZeppelin/openzeppelin-contracts/tree/master/contracts/token/ERC20
* The major difference is that, instead of defining its own state variables, it uses state inherited
* from LibERC20.sol.
*/
pragma solidity ^0.8.0;
import "@openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import "@openzeppelin-contracts/contracts/utils/Context.sol";
import "./LibERC20.sol";
contract ERC20WithCommonStorage is Context, IERC20, IERC20Metadata {
/**
* @dev Sets the values for {name} and {symbol}.
*
* The default value of {decimals} is 18. To select a different value for
* {decimals} you should overload it.
*
* All two of these values are immutable: they can only be set once during
* construction.
*/
function setERC20Metadata(string memory name_, string memory symbol_)
external
{
LibERC20.enforceIsController();
LibERC20.ERC20Storage storage es = LibERC20.erc20Storage();
es.name = name_;
es.symbol = symbol_;
}
/**
* @dev Returns the name of the token.
*/
function name() public view virtual override returns (string memory) {
return LibERC20.erc20Storage().name;
}
/**
* @dev Returns the symbol of the token, usually a shorter version of the
* name.
*/
function symbol() public view virtual override returns (string memory) {
return LibERC20.erc20Storage().symbol;
}
/**
* @dev Returns the number of decimals used to get its user representation.
* For example, if `decimals` equals `2`, a balance of `505` tokens should
* be displayed to a user as `5.05` (`505 / 10 ** 2`).
*
* Tokens usually opt for a value of 18, imitating the relationship between
* Ether and Wei. This is the value {ERC20} uses, unless this function is
* overridden;
*
* NOTE: This information is only used for _display_ purposes: it in
* no way affects any of the arithmetic of the contract, including
* {IERC20-balanceOf} and {IERC20-transfer}.
*/
function decimals() public view virtual override returns (uint8) {
return 18;
}
/**
* @dev See {IERC20-totalSupply}.
*/
function totalSupply() public view virtual override returns (uint256) {
return LibERC20.erc20Storage().totalSupply;
}
/**
* @dev See {IERC20-balanceOf}.
*/
function balanceOf(address account)
public
view
virtual
override
returns (uint256)
{
return LibERC20.erc20Storage().balances[account];
}
/**
* @dev See {IERC20-transfer}.
*
* Requirements:
*
* - `recipient` cannot be the zero address.
* - the caller must have a balance of at least `amount`.
*/
function transfer(address recipient, uint256 amount)
public
virtual
override
returns (bool)
{
_transfer(_msgSender(), recipient, amount);
return true;
}
/**
* @dev See {IERC20-allowance}.
*/
function allowance(address owner, address spender)
public
view
virtual
override
returns (uint256)
{
return LibERC20.erc20Storage().allowances[owner][spender];
}
/**
* @dev See {IERC20-approve}.
*
* Requirements:
*
* - `spender` cannot be the zero address.
*/
function approve(address spender, uint256 amount)
public
virtual
override
returns (bool)
{
_approve(_msgSender(), spender, amount);
return true;
}
/**
* @dev See {IERC20-transferFrom}.
*
* Emits an {Approval} event indicating the updated allowance. This is not
* required by the EIP. See the note at the beginning of {ERC20}.
*
* Requirements:
*
* - `sender` and `recipient` cannot be the zero address.
* - `sender` must have a balance of at least `amount`.
* - the caller must have allowance for ``sender``'s tokens of at least
* `amount`.
*/
function transferFrom(
address sender,
address recipient,
uint256 amount
) public virtual override returns (bool) {
_transfer(sender, recipient, amount);
uint256 currentAllowance = LibERC20.erc20Storage().allowances[sender][
_msgSender()
];
require(
currentAllowance >= amount,
"ERC20WithCommonStorage: transfer amount exceeds allowance"
);
unchecked {
_approve(sender, _msgSender(), currentAllowance - amount);
}
return true;
}
/**
* @dev Atomically increases the allowance granted to `spender` by the caller.
*
* This is an alternative to {approve} that can be used as a mitigation for
* problems described in {IERC20-approve}.
*
* Emits an {Approval} event indicating the updated allowance.
*
* Requirements:
*
* - `spender` cannot be the zero address.
*/
function increaseAllowance(address spender, uint256 addedValue)
public
virtual
returns (bool)
{
_approve(
_msgSender(),
spender,
LibERC20.erc20Storage().allowances[_msgSender()][spender] +
addedValue
);
return true;
}
/**
* @dev Atomically decreases the allowance granted to `spender` by the caller.
*
* This is an alternative to {approve} that can be used as a mitigation for
* problems described in {IERC20-approve}.
*
* Emits an {Approval} event indicating the updated allowance.
*
* Requirements:
*
* - `spender` cannot be the zero address.
* - `spender` must have allowance for the caller of at least
* `subtractedValue`.
*/
function decreaseAllowance(address spender, uint256 subtractedValue)
public
virtual
returns (bool)
{
uint256 currentAllowance = LibERC20.erc20Storage().allowances[
_msgSender()
][spender];
require(
currentAllowance >= subtractedValue,
"ERC20WithCommonStorage: decreased allowance below zero"
);
unchecked {
_approve(_msgSender(), spender, currentAllowance - subtractedValue);
}
return true;
}
/**
* @dev Moves `amount` of tokens from `sender` to `recipient`.
*
* This internal function is equivalent to {transfer}, and can be used to
* e.g. implement automatic token fees, slashing mechanisms, etc.
*
* Emits a {Transfer} event.
*
* Requirements:
*
* - `sender` cannot be the zero address.
* - `recipient` cannot be the zero address.
* - `sender` must have a balance of at least `amount`.
*/
function _transfer(
address sender,
address recipient,
uint256 amount
) internal virtual {
require(
sender != address(0),
"ERC20WithCommonStorage: transfer from the zero address"
);
require(
recipient != address(0),
"ERC20WithCommonStorage: transfer to the zero address"
);
_beforeTokenTransfer(sender, recipient, amount);
LibERC20.ERC20Storage storage es = LibERC20.erc20Storage();
uint256 senderBalance = es.balances[sender];
require(
senderBalance >= amount,
"ERC20WithCommonStorage: transfer amount exceeds balance"
);
unchecked {
es.balances[sender] = senderBalance - amount;
}
es.balances[recipient] += amount;
emit Transfer(sender, recipient, amount);
_afterTokenTransfer(sender, recipient, amount);
}
/** @dev Creates `amount` tokens and assigns them to `account`, increasing
* the total supply.
*
* Emits a {Transfer} event with `from` set to the zero address.
*
* Requirements:
*
* - `account` cannot be the zero address.
*/
function _mint(address account, uint256 amount) internal virtual {
require(
account != address(0),
"ERC20WithCommonStorage: mint to the zero address"
);
_beforeTokenTransfer(address(0), account, amount);
LibERC20.ERC20Storage storage es = LibERC20.erc20Storage();
es.totalSupply += amount;
es.balances[account] += amount;
emit Transfer(address(0), account, amount);
_afterTokenTransfer(address(0), account, amount);
}
/**
* @dev Destroys `amount` tokens from `account`, reducing the
* total supply.
*
* Emits a {Transfer} event with `to` set to the zero address.
*
* Requirements:
*
* - `account` cannot be the zero address.
* - `account` must have at least `amount` tokens.
*/
function _burn(address account, uint256 amount) internal virtual {
require(
account != address(0),
"ERC20WithCommonStorage: burn from the zero address"
);
_beforeTokenTransfer(account, address(0), amount);
LibERC20.ERC20Storage storage es = LibERC20.erc20Storage();
uint256 accountBalance = es.balances[account];
require(
accountBalance >= amount,
"ERC20WithCommonStorage: burn amount exceeds balance"
);
unchecked {
es.balances[account] = accountBalance - amount;
}
es.totalSupply -= amount;
emit Transfer(account, address(0), amount);
_afterTokenTransfer(account, address(0), amount);
}
/**
* @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens.
*
* This internal function is equivalent to `approve`, and can be used to
* e.g. set automatic allowances for certain subsystems, etc.
*
* Emits an {Approval} event.
*
* Requirements:
*
* - `owner` cannot be the zero address.
* - `spender` cannot be the zero address.
*/
function _approve(
address owner,
address spender,
uint256 amount
) internal virtual {
require(
owner != address(0),
"ERC20WithCommonStorage: approve from the zero address"
);
require(
spender != address(0),
"ERC20WithCommonStorage: approve to the zero address"
);
LibERC20.erc20Storage().allowances[owner][spender] = amount;
emit Approval(owner, spender, amount);
}
/**
* @dev Hook that is called before any transfer of tokens. This includes
* minting and burning.
*
* Calling conditions:
*
* - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens
* will be transferred to `to`.
* - when `from` is zero, `amount` tokens will be minted for `to`.
* - when `to` is zero, `amount` of ``from``'s tokens will be burned.
* - `from` and `to` are never both zero.
*
* To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
*/
function _beforeTokenTransfer(
address from,
address to,
uint256 amount
) internal virtual {}
/**
* @dev Hook that is called after any transfer of tokens. This includes
* minting and burning.
*
* Calling conditions:
*
* - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens
* has been transferred to `to`.
* - when `from` is zero, `amount` tokens have been minted for `to`.
* - when `to` is zero, `amount` of ``from``'s tokens have been burned.
* - `from` and `to` are never both zero.
*
* To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
*/
function _afterTokenTransfer(
address from,
address to,
uint256 amount
) internal virtual {}
}

Wyświetl plik

@ -0,0 +1,48 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.0;
library LibERC20 {
bytes32 constant ERC20_STORAGE_POSITION =
keccak256("moonstreamdao.eth.storage.erc20");
struct ERC20Storage {
string name;
string symbol;
mapping(address => uint256) balances;
mapping(address => mapping(address => uint256)) allowances;
address controller;
uint256 totalSupply;
}
function erc20Storage() internal pure returns (ERC20Storage storage es) {
bytes32 position = ERC20_STORAGE_POSITION;
assembly {
es.slot := position
}
}
event ControlTransferred(
address indexed previousController,
address indexed newController
);
function setController(address newController) internal {
ERC20Storage storage es = erc20Storage();
address previousController = es.controller;
es.controller = newController;
emit ControlTransferred(previousController, newController);
}
function getController()
internal
view
returns (address contractController)
{
contractController = erc20Storage().controller;
}
function enforceIsController() internal view {
ERC20Storage storage es = erc20Storage();
require(msg.sender == es.controller, "LibERC20: Must be controller");
}
}

440
dao/ERC20Facet.py 100644
Wyświetl plik

@ -0,0 +1,440 @@
# 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 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()
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_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)
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)
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)
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()

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 diamond
from . import core, ERC20Facet, ERC20Initializer
def main():
@ -10,8 +10,18 @@ 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)
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)

309
dao/core.py 100644
Wyświetl plik

@ -0,0 +1,309 @@
"""
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
from . import (
abi,
Diamond,
DiamondCutFacet,
DiamondLoupeFacet,
ERC20Facet,
ERC20Initializer,
OwnershipFacet,
)
FACETS: Dict[str, Any] = {
"DiamondCutFacet": DiamondCutFacet,
"DiamondLoupeFacet": DiamondLoupeFacet,
"ERC20Facet": ERC20Facet,
"OwnershipFacet": OwnershipFacet,
}
FACET_PRECEDENCE: List[str] = [
"DiamondCutFacet",
"OwnershipFacet",
"DiamondLoupeFacet",
"ERC20Facet",
]
FACET_ACTIONS: Dict[str, int] = {"add": 0, "replace": 1, "remove": 2}
ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
def facet_cut(
diamond_address: str,
facet_name: str,
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:
"""
Cuts the given facet onto the given Diamond contract.
Resolves selectors in the precedence order defined by FACET_PRECEDENCE (highest precedence first).
"""
assert (
facet_name in FACETS
), f"Invalid facet: {facet_name}. Choices: {','.join(FACETS)}."
assert (
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)
reserved_selectors: Set[str] = set()
for facet in FACET_PRECEDENCE:
if facet == facet_name:
break
facet_abi = abis.get(facet, [])
for item in facet_abi:
if item["type"] == "function":
reserved_selectors.add(abi.encode_function_signature(item))
facet_function_selectors: List[str] = []
facet_abi = abis.get(facet_name, [])
for item in facet_abi:
if item["type"] == "function":
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:
target_address = ZERO_ADDRESS
diamond_cut_action = [
target_address,
FACET_ACTIONS[action],
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], 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
action = args.action
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,
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",
)
parser.set_defaults(func=lambda _: parser.print_help())
subcommands = parser.add_subparsers()
Diamond_parser = Diamond.generate_cli()
subcommands.add_parser("diamond", parents=[Diamond_parser], add_help=False)
facet_cut_parser = subcommands.add_parser("facet-cut")
Diamond.add_default_arguments(facet_cut_parser, transact=True)
facet_cut_parser.add_argument(
"--facet-name",
required=True,
choices=FACETS,
help="Name of facet to cut into or out of diamond",
)
facet_cut_parser.add_argument(
"--facet-address",
required=False,
default=ZERO_ADDRESS,
help=f"Address of deployed facet (default: {ZERO_ADDRESS})",
)
facet_cut_parser.add_argument(
"--action",
required=True,
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="+",
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)
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
)
DiamondLoupeFacet_parser = DiamondLoupeFacet.generate_cli()
subcommands.add_parser(
"diamond-loupe", parents=[DiamondLoupeFacet_parser], add_help=False
)
OwnershipFacet_parser = OwnershipFacet.generate_cli()
subcommands.add_parser("ownership", parents=[OwnershipFacet_parser], add_help=False)
return parser
def main() -> None:
parser = generate_cli()
args = parser.parse_args()
args.func(args)
if __name__ == "__main__":
main()

Wyświetl plik

@ -1,159 +0,0 @@
"""
Generic diamond functionality for Moonstream contracts.
"""
import argparse
import os
from typing import Any, Dict, List, Set
from brownie import network
from . import (
abi,
Diamond,
DiamondCutFacet,
DiamondLoupeFacet,
OwnershipFacet,
)
FACETS: Dict[str, Any] = {
"DiamondCutFacet": DiamondCutFacet,
"DiamondLoupeFacet": DiamondLoupeFacet,
"OwnershipFacet": OwnershipFacet,
}
FACET_PRECEDENCE: List[str] = [
"DiamondCutFacet",
"OwnershipFacet",
"DiamondLoupeFacet",
]
FACET_ACTIONS: Dict[str, int] = {"add": 0, "replace": 1, "remove": 2}
ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
def facet_cut(
diamond_address: str,
facet_name: str,
facet_address: str,
action: str,
transaction_config: Dict[str, Any],
) -> Any:
"""
Cuts the given facet onto the given Diamond contract.
Resolves selectors in the precedence order defined by FACET_PRECEDENCE (highest precedence first).
"""
assert (
facet_name in FACETS
), f"Invalid facet: {facet_name}. Choices: {','.join(FACETS)}."
assert (
action in FACET_ACTIONS
), f"Invalid cut action: {action}. Choices: {','.join(FACET_ACTIONS)}."
project_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
abis = abi.project_abis(project_dir)
reserved_selectors: Set[str] = set()
for facet in FACET_PRECEDENCE:
if facet == facet_name:
break
facet_abi = abis.get(facet, [])
for item in facet_abi:
if item["type"] == "function":
reserved_selectors.add(abi.encode_function_signature(item))
facet_function_selectors: List[str] = []
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)
target_address = facet_address
if FACET_ACTIONS[action] == 2:
target_address = ZERO_ADDRESS
diamond_cut_action = [
target_address,
FACET_ACTIONS[action],
facet_function_selectors,
]
diamond = DiamondCutFacet.DiamondCutFacet(diamond_address)
transaction = diamond.diamond_cut(
[diamond_cut_action], ZERO_ADDRESS, b"", transaction_config
)
return transaction
def handle_facet_cut(args: argparse.Namespace) -> None:
network.connect(args.network)
diamond_address = args.address
action = args.action
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)
def generate_cli() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="CLI to manage Moonstream DAO diamond contracts",
)
parser.set_defaults(func=lambda _: parser.print_help())
subcommands = parser.add_subparsers()
Diamond_parser = Diamond.generate_cli()
subcommands.add_parser("diamond", parents=[Diamond_parser], add_help=False)
facet_cut_parser = subcommands.add_parser("facet-cut")
Diamond.add_default_arguments(facet_cut_parser, transact=True)
facet_cut_parser.add_argument(
"--facet-name",
required=True,
choices=FACETS,
help="Name of facet to cut into or out of diamond",
)
facet_cut_parser.add_argument(
"--facet-address",
required=False,
default=ZERO_ADDRESS,
help=f"Address of deployed facet (default: {ZERO_ADDRESS})",
)
facet_cut_parser.add_argument(
"--action",
required=True,
choices=FACET_ACTIONS,
help="Diamond cut action to take on entire facet",
)
facet_cut_parser.set_defaults(func=handle_facet_cut)
DiamondCutFacet_parser = DiamondCutFacet.generate_cli()
subcommands.add_parser(
"diamond-cut", parents=[DiamondCutFacet_parser], add_help=False
)
DiamondLoupeFacet_parser = DiamondLoupeFacet.generate_cli()
subcommands.add_parser(
"diamond-loupe", parents=[DiamondLoupeFacet_parser], add_help=False
)
OwnershipFacet_parser = OwnershipFacet.generate_cli()
subcommands.add_parser("ownership", parents=[OwnershipFacet_parser], add_help=False)
return parser
def main() -> None:
parser = generate_cli()
args = parser.parse_args()
args.func(args)
if __name__ == "__main__":
main()

61
dao/test_core.py 100644
Wyświetl plik

@ -0,0 +1,61 @@
import unittest
from brownie import accounts, network
from .core import facet_cut, gogogo
from .ERC20Facet import ERC20Facet
from .ERC20Initializer import ERC20Initializer
class MoonstreamDAOTestCase(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
try:
network.connect()
except:
pass
cls.contracts = gogogo(accounts[0], {"from": accounts[0]})
class MoonstreamDAOFullTestCase(MoonstreamDAOTestCase):
@classmethod
def setUpClass(cls) -> None:
super().setUpClass()
# Deploy ERC20
initializer = ERC20Initializer(None)
initializer.deploy({"from": accounts[0]})
erc20_facet = ERC20Facet(None)
erc20_facet.deploy({"from": accounts[0]})
diamond_address = cls.contracts["Diamond"]
facet_cut(
diamond_address,
"ERC20Facet",
erc20_facet.address,
"add",
{"from": accounts[0]},
initializer.address,
)
cls.erc20_initializer = initializer.address
cls.erc20_facet = erc20_facet.address
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,291 @@
import unittest
from brownie import accounts
import brownie
from . import ERC20Facet, ERC20Initializer
from .core import ZERO_ADDRESS, facet_cut
from .test_core import MoonstreamDAOTestCase, MoonstreamDAOFullTestCase
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)
class TestRemoveFacet(MoonstreamDAOTestCase):
def test_remove_facet(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)
facet_cut(
diamond_address,
"ERC20Facet",
ZERO_ADDRESS,
"remove",
{"from": accounts[0]},
)
with self.assertRaises(Exception):
name = diamond_erc20.name()
with self.assertRaises(Exception):
symbol = diamond_erc20.symbol()
class TestERC20(MoonstreamDAOFullTestCase):
def test_mint_fails_if_not_controller(self):
diamond_address = self.contracts["Diamond"]
diamond = ERC20Facet.ERC20Facet(diamond_address)
with self.assertRaises(Exception):
diamond.mint(accounts[1].address, 1000, {"from": accounts[1]})
def test_mint_to_another_address(self):
diamond_address = self.contracts["Diamond"]
diamond = ERC20Facet.ERC20Facet(diamond_address)
initial_balance = diamond.balance_of(accounts[1].address)
diamond.mint(accounts[1].address, 1000, {"from": accounts[0]})
final_balance = diamond.balance_of(accounts[1].address)
self.assertEqual(final_balance, initial_balance + 1000)
def test_transfer(self):
diamond_address = self.contracts["Diamond"]
diamond = ERC20Facet.ERC20Facet(diamond_address)
diamond.mint(accounts[1].address, 1000, {"from": accounts[0]})
initial_sender_balance = diamond.balance_of(accounts[1].address)
initial_receiver_balance = diamond.balance_of(accounts[2].address)
diamond.transfer(accounts[2].address, 500, {"from": accounts[1]})
final_sender_balance = diamond.balance_of(accounts[1].address)
final_receiver_balance = diamond.balance_of(accounts[2].address)
self.assertEqual(final_sender_balance, initial_sender_balance - 500)
self.assertEqual(final_receiver_balance, initial_receiver_balance + 500)
def test_transfer_insufficient_balance(self):
diamond_address = self.contracts["Diamond"]
diamond = ERC20Facet.ERC20Facet(diamond_address)
initial_sender_balance = diamond.balance_of(accounts[1].address)
initial_receiver_balance = diamond.balance_of(accounts[2].address)
with self.assertRaises(Exception):
diamond.transfer(
accounts[2].address, initial_sender_balance + 1, {"from": accounts[1]}
)
final_sender_balance = diamond.balance_of(accounts[1].address)
final_receiver_balance = diamond.balance_of(accounts[2].address)
self.assertEqual(final_sender_balance, initial_sender_balance)
self.assertEqual(final_receiver_balance, initial_receiver_balance)
def test_transfer_from_with_approval(self):
diamond_address = self.contracts["Diamond"]
diamond = ERC20Facet.ERC20Facet(diamond_address)
diamond.mint(accounts[1].address, 1000, {"from": accounts[0]})
initial_sender_balance = diamond.balance_of(accounts[1].address)
initial_receiver_balance = diamond.balance_of(accounts[2].address)
diamond.approve(accounts[2].address, 500, {"from": accounts[1]})
diamond.transfer_from(
accounts[1].address, accounts[2].address, 500, {"from": accounts[2]}
)
final_sender_balance = diamond.balance_of(accounts[1].address)
final_receiver_balance = diamond.balance_of(accounts[2].address)
self.assertEqual(final_sender_balance, initial_sender_balance - 500)
self.assertEqual(final_receiver_balance, initial_receiver_balance + 500)
def test_transfer_with_approval_insufficient_balance(self):
diamond_address = self.contracts["Diamond"]
diamond = ERC20Facet.ERC20Facet(diamond_address)
initial_sender_balance = diamond.balance_of(accounts[1].address)
initial_receiver_balance = diamond.balance_of(accounts[2].address)
diamond.approve(
accounts[2].address, initial_sender_balance + 1, {"from": accounts[1]}
)
with self.assertRaises(Exception):
diamond.transfer_from(
accounts[1].address,
accounts[2].address,
initial_sender_balance + 1,
{"from": accounts[2]},
)
final_sender_balance = diamond.balance_of(accounts[1].address)
final_receiver_balance = diamond.balance_of(accounts[2].address)
self.assertEqual(final_sender_balance, initial_sender_balance)
self.assertEqual(final_receiver_balance, initial_receiver_balance)
def test_transfer_from_with_approval_insufficient_allowance_sufficient_balance(
self,
):
diamond_address = self.contracts["Diamond"]
diamond = ERC20Facet.ERC20Facet(diamond_address)
diamond.mint(accounts[1].address, 1000, {"from": accounts[0]})
diamond.approve(accounts[2].address, 500, {"from": accounts[1]})
initial_sender_balance = diamond.balance_of(accounts[1].address)
initial_receiver_balance = diamond.balance_of(accounts[2].address)
with self.assertRaises(Exception):
diamond.transfer_from(
accounts[1].address,
accounts[2].address,
501,
{"from": accounts[2]},
)
final_sender_balance = diamond.balance_of(accounts[1].address)
final_receiver_balance = diamond.balance_of(accounts[2].address)
self.assertEqual(final_sender_balance, initial_sender_balance)
self.assertEqual(final_receiver_balance, initial_receiver_balance)
def test_not_burnable(self):
diamond_address = self.contracts["Diamond"]
diamond = ERC20Facet.ERC20Facet(diamond_address)
diamond.mint(accounts[1].address, 1000, {"from": accounts[0]})
with self.assertRaises(Exception):
diamond.transfer(brownie.ZERO_ADDRESS, 500, {"from": accounts[1]})
def test_approve_and_allowance(self):
diamond_address = self.contracts["Diamond"]
diamond = ERC20Facet.ERC20Facet(diamond_address)
diamond.approve(accounts[2].address, 500, {"from": accounts[1]})
allowance = diamond.allowance(accounts[1].address, accounts[2].address)
self.assertEqual(allowance, 500)
def test_increase_allowance(self):
diamond_address = self.contracts["Diamond"]
diamond = ERC20Facet.ERC20Facet(diamond_address)
initial_allowance = diamond.allowance(accounts[1].address, accounts[2].address)
diamond.increase_allowance(accounts[2].address, 500, {"from": accounts[1]})
final_allowance = diamond.allowance(accounts[1].address, accounts[2].address)
self.assertEqual(final_allowance, initial_allowance + 500)
def test_decrease_allowance(self):
diamond_address = self.contracts["Diamond"]
diamond = ERC20Facet.ERC20Facet(diamond_address)
initial_allowance = diamond.allowance(accounts[1].address, accounts[2].address)
diamond.decrease_allowance(accounts[2].address, 500, {"from": accounts[1]})
final_allowance = diamond.allowance(accounts[1].address, accounts[2].address)
self.assertEqual(final_allowance, initial_allowance - 500)
def test_mint_total_supply(self):
diamond_address = self.contracts["Diamond"]
diamond = ERC20Facet.ERC20Facet(diamond_address)
initial_total_supply = diamond.total_supply()
diamond.mint(accounts[1].address, 1000, {"from": accounts[0]})
final_total_supply = diamond.total_supply()
self.assertEqual(final_total_supply, initial_total_supply + 1000)
if __name__ == "__main__":
unittest.main()

27
test.sh 100755
Wyświetl plik

@ -0,0 +1,27 @@
#!/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" [TEST_SPEC ...]
echo
echo "Arguments:"
echo "----------"
echo "TEST_SPEC"
echo "\tPython unittest specification of which test to run, following: https://docs.python.org/3/library/unittest.html#command-line-interface"
echo "\tFor example: $0 dao.test_moonstream.TestERC20"
}
if [ "$1" = "-h" ] || [ "$1" = "--help" ]
then
usage
exit 2
fi
TEST_COMMAND=${@:-discover}
brownie compile
set -x
python -m unittest $TEST_COMMAND