diff --git a/.github/workflows/terminus.yml b/.github/workflows/terminus.yml new file mode 100644 index 00000000..a9e648bc --- /dev/null +++ b/.github/workflows/terminus.yml @@ -0,0 +1,41 @@ +name: Terminus protocol tests + +on: + pull_request: + paths: + - "contracts/terminus/**" + - "contracts/mock/**" + - "cli/web3cli/test_terminus.py" + - "cli/web3cli/TerminusFacet.py" + - ".github/workflows/terminus.yml" + branches: + - main +jobs: + build: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: "16" + - uses: actions/setup-python@v2 + with: + python-version: "3.9" + - name: Install ganache + run: npm install -g ganache-cli + - name: Upgrade pip + env: + BROWNIE_LIB: 1 + run: pip install -U pip + - name: Install additional dev dependencies + run: | + pip install black moonworm + - name: Install dependencies for CLI + working-directory: cli/ + env: + BROWNIE_LIB: 1 + run: | + pip install -e . + - name: Run tests + working-directory: cli/ + run: bash test.sh web3cli.test_terminus diff --git a/abi/ERC1155WithTerminusStorage.json b/abi/ERC1155WithTerminusStorage.json new file mode 100644 index 00000000..b1db97a3 --- /dev/null +++ b/abi/ERC1155WithTerminusStorage.json @@ -0,0 +1,379 @@ +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "ApprovalForAll", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "ids", + "type": "uint256[]" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "values", + "type": "uint256[]" + } + ], + "name": "TransferBatch", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "TransferSingle", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "string", + "name": "value", + "type": "string" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "URI", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "poolID", + "type": "uint256" + }, + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "approveForPool", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "accounts", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "ids", + "type": "uint256[]" + } + ], + "name": "balanceOfBatch", + "outputs": [ + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "isApprovedForAll", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "poolID", + "type": "uint256" + }, + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "isApprovedForPool", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256[]", + "name": "ids", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "safeBatchTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "setApprovalForAll", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "poolID", + "type": "uint256" + }, + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "unapproveForPool", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "poolID", + "type": "uint256" + } + ], + "name": "uri", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + } +] \ No newline at end of file diff --git a/abi/LibTerminus.json b/abi/LibTerminus.json new file mode 100644 index 00000000..a25a2753 --- /dev/null +++ b/abi/LibTerminus.json @@ -0,0 +1,46 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousController", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newController", + "type": "address" + } + ], + "name": "ControlTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "poolID", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "previousController", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newController", + "type": "address" + } + ], + "name": "PoolControlTransferred", + "type": "event" + } +] \ No newline at end of file diff --git a/abi/MockTerminus.json b/abi/MockTerminus.json index cb1f6654..e1639763 100644 --- a/abi/MockTerminus.json +++ b/abi/MockTerminus.json @@ -290,6 +290,40 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_capacity", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "_transferable", + "type": "bool" + }, + { + "internalType": "bool", + "name": "_burnable", + "type": "bool" + }, + { + "internalType": "string", + "name": "poolURI", + "type": "string" + } + ], + "name": "createPoolV2", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -810,6 +844,24 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "poolID", + "type": "uint256" + }, + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "unapproveForPool", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { diff --git a/abi/TerminusFacet.json b/abi/TerminusFacet.json new file mode 100644 index 00000000..e1639763 --- /dev/null +++ b/abi/TerminusFacet.json @@ -0,0 +1,902 @@ +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "ApprovalForAll", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": false, + "internalType": "address[]", + "name": "toAddresses", + "type": "address[]" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "name": "PoolMintBatch", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "ids", + "type": "uint256[]" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "values", + "type": "uint256[]" + } + ], + "name": "TransferBatch", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "TransferSingle", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "string", + "name": "value", + "type": "string" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "URI", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "poolID", + "type": "uint256" + }, + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "approveForPool", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "accounts", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "ids", + "type": "uint256[]" + } + ], + "name": "balanceOfBatch", + "outputs": [ + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "uint256", + "name": "poolID", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "burn", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "contractURI", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_capacity", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "_transferable", + "type": "bool" + }, + { + "internalType": "bool", + "name": "_burnable", + "type": "bool" + } + ], + "name": "createPoolV1", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_capacity", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "_transferable", + "type": "bool" + }, + { + "internalType": "bool", + "name": "_burnable", + "type": "bool" + }, + { + "internalType": "string", + "name": "poolURI", + "type": "string" + } + ], + "name": "createPoolV2", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_capacity", + "type": "uint256" + } + ], + "name": "createSimplePool", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "isApprovedForAll", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "poolID", + "type": "uint256" + }, + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "isApprovedForPool", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "poolID", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "mint", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256[]", + "name": "poolIDs", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "mintBatch", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "paymentToken", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "poolBasePrice", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "poolID", + "type": "uint256" + } + ], + "name": "poolIsBurnable", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "poolID", + "type": "uint256" + } + ], + "name": "poolIsTransferable", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "toAddresses", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "name": "poolMintBatch", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256[]", + "name": "ids", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "safeBatchTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "setApprovalForAll", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "_contractURI", + "type": "string" + } + ], + "name": "setContractURI", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newController", + "type": "address" + } + ], + "name": "setController", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newPaymentToken", + "type": "address" + } + ], + "name": "setPaymentToken", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "newBasePrice", + "type": "uint256" + } + ], + "name": "setPoolBasePrice", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "poolID", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "burnable", + "type": "bool" + } + ], + "name": "setPoolBurnable", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "poolID", + "type": "uint256" + }, + { + "internalType": "address", + "name": "newController", + "type": "address" + } + ], + "name": "setPoolController", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "poolID", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "transferable", + "type": "bool" + } + ], + "name": "setPoolTransferable", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "poolID", + "type": "uint256" + }, + { + "internalType": "string", + "name": "poolURI", + "type": "string" + } + ], + "name": "setURI", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "terminusController", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "poolID", + "type": "uint256" + } + ], + "name": "terminusPoolCapacity", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "poolID", + "type": "uint256" + } + ], + "name": "terminusPoolController", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "poolID", + "type": "uint256" + } + ], + "name": "terminusPoolSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalPools", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "poolID", + "type": "uint256" + }, + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "unapproveForPool", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "poolID", + "type": "uint256" + } + ], + "name": "uri", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "toAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "withdrawPayments", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/abi/TerminusInitializer.json b/abi/TerminusInitializer.json new file mode 100644 index 00000000..1a4cdbe8 --- /dev/null +++ b/abi/TerminusInitializer.json @@ -0,0 +1,9 @@ +[ + { + "inputs": [], + "name": "init", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/abi/TerminusPermissions.json b/abi/TerminusPermissions.json new file mode 100644 index 00000000..0637a088 --- /dev/null +++ b/abi/TerminusPermissions.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/brownie-config.yaml b/brownie-config.yaml index 835be82c..b30d8c21 100644 --- a/brownie-config.yaml +++ b/brownie-config.yaml @@ -1,6 +1,5 @@ dependencies: - "OpenZeppelin/openzeppelin-contracts@4.4.0" - - "bugout-dev/dao@0.0.7" - "smartcontractkit/chainlink@1.13.3" compiler: @@ -8,5 +7,4 @@ compiler: remappings: - "@openzeppelin-contracts=OpenZeppelin/openzeppelin-contracts@4.4.0" - "@openzeppelin/contracts=OpenZeppelin/openzeppelin-contracts@4.4.0" - - "@moonstream=bugout-dev/dao@0.0.7" - "@chainlink=smartcontractkit/chainlink@1.13.3" diff --git a/cli/web3cli/TerminusFacet.py b/cli/web3cli/TerminusFacet.py new file mode 100644 index 00000000..f1d4b96e --- /dev/null +++ b/cli/web3cli/TerminusFacet.py @@ -0,0 +1,1250 @@ +# Code generated by moonworm : https://github.com/moonstream-to/moonworm +# Moonworm version : 0.7.1 + +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) -> str: + return raw_value + + +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 TerminusFacet: + def __init__(self, contract_address: Optional[ChecksumAddress]): + self.contract_name = "TerminusFacet" + self.address = contract_address + self.contract = None + self.abi = get_abi_json("TerminusFacet") + 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 + return deployed_contract.tx + + def assert_contract_is_instantiated(self) -> None: + if self.contract is None: + raise Exception("contract has not been instantiated") + + def verify_contract(self): + self.assert_contract_is_instantiated() + contract_class = contract_from_build(self.contract_name) + contract_class.publish_source(self.contract) + + def approve_for_pool( + self, pool_id: int, operator: ChecksumAddress, transaction_config + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.approveForPool(pool_id, operator, transaction_config) + + def balance_of( + self, + account: ChecksumAddress, + id: int, + block_number: Optional[Union[str, int]] = "latest", + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.balanceOf.call(account, id, block_identifier=block_number) + + def balance_of_batch( + self, + accounts: List, + ids: List, + block_number: Optional[Union[str, int]] = "latest", + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.balanceOfBatch.call( + accounts, ids, block_identifier=block_number + ) + + def burn( + self, from_: ChecksumAddress, pool_id: int, amount: int, transaction_config + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.burn(from_, pool_id, amount, transaction_config) + + def contract_uri(self, block_number: Optional[Union[str, int]] = "latest") -> Any: + self.assert_contract_is_instantiated() + return self.contract.contractURI.call(block_identifier=block_number) + + def create_pool_v1( + self, _capacity: int, _transferable: bool, _burnable: bool, transaction_config + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.createPoolV1( + _capacity, _transferable, _burnable, transaction_config + ) + + def create_pool_v2( + self, + _capacity: int, + _transferable: bool, + _burnable: bool, + pool_uri: str, + transaction_config, + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.createPoolV2( + _capacity, _transferable, _burnable, pool_uri, transaction_config + ) + + def create_simple_pool(self, _capacity: int, transaction_config) -> Any: + self.assert_contract_is_instantiated() + return self.contract.createSimplePool(_capacity, transaction_config) + + def is_approved_for_all( + self, + account: ChecksumAddress, + operator: ChecksumAddress, + block_number: Optional[Union[str, int]] = "latest", + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.isApprovedForAll.call( + account, operator, block_identifier=block_number + ) + + def is_approved_for_pool( + self, + pool_id: int, + operator: ChecksumAddress, + block_number: Optional[Union[str, int]] = "latest", + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.isApprovedForPool.call( + pool_id, operator, block_identifier=block_number + ) + + def mint( + self, + to: ChecksumAddress, + pool_id: int, + amount: int, + data: bytes, + transaction_config, + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.mint(to, pool_id, amount, data, transaction_config) + + def mint_batch( + self, + to: ChecksumAddress, + pool_i_ds: List, + amounts: List, + data: bytes, + transaction_config, + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.mintBatch(to, pool_i_ds, amounts, data, transaction_config) + + def payment_token(self, block_number: Optional[Union[str, int]] = "latest") -> Any: + self.assert_contract_is_instantiated() + return self.contract.paymentToken.call(block_identifier=block_number) + + def pool_base_price( + self, block_number: Optional[Union[str, int]] = "latest" + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.poolBasePrice.call(block_identifier=block_number) + + def pool_is_burnable( + self, pool_id: int, block_number: Optional[Union[str, int]] = "latest" + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.poolIsBurnable.call(pool_id, block_identifier=block_number) + + def pool_is_transferable( + self, pool_id: int, block_number: Optional[Union[str, int]] = "latest" + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.poolIsTransferable.call( + pool_id, block_identifier=block_number + ) + + def pool_mint_batch( + self, id: int, to_addresses: List, amounts: List, transaction_config + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.poolMintBatch( + id, to_addresses, amounts, transaction_config + ) + + def safe_batch_transfer_from( + self, + from_: ChecksumAddress, + to: ChecksumAddress, + ids: List, + amounts: List, + data: bytes, + transaction_config, + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.safeBatchTransferFrom( + from_, to, ids, amounts, data, transaction_config + ) + + def safe_transfer_from( + self, + from_: ChecksumAddress, + to: ChecksumAddress, + id: int, + amount: int, + data: bytes, + transaction_config, + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.safeTransferFrom( + from_, to, id, amount, data, transaction_config + ) + + def set_approval_for_all( + self, operator: ChecksumAddress, approved: bool, transaction_config + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.setApprovalForAll(operator, approved, transaction_config) + + def set_contract_uri(self, _contract_uri: str, transaction_config) -> Any: + self.assert_contract_is_instantiated() + return self.contract.setContractURI(_contract_uri, transaction_config) + + def set_controller( + self, new_controller: ChecksumAddress, transaction_config + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.setController(new_controller, transaction_config) + + def set_payment_token( + self, new_payment_token: ChecksumAddress, transaction_config + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.setPaymentToken(new_payment_token, transaction_config) + + def set_pool_base_price(self, new_base_price: int, transaction_config) -> Any: + self.assert_contract_is_instantiated() + return self.contract.setPoolBasePrice(new_base_price, transaction_config) + + def set_pool_burnable( + self, pool_id: int, burnable: bool, transaction_config + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.setPoolBurnable(pool_id, burnable, transaction_config) + + def set_pool_controller( + self, pool_id: int, new_controller: ChecksumAddress, transaction_config + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.setPoolController( + pool_id, new_controller, transaction_config + ) + + def set_pool_transferable( + self, pool_id: int, transferable: bool, transaction_config + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.setPoolTransferable( + pool_id, transferable, transaction_config + ) + + def set_uri(self, pool_id: int, pool_uri: str, transaction_config) -> Any: + self.assert_contract_is_instantiated() + return self.contract.setURI(pool_id, pool_uri, transaction_config) + + def supports_interface( + self, interface_id: bytes, block_number: Optional[Union[str, int]] = "latest" + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.supportsInterface.call( + interface_id, block_identifier=block_number + ) + + def terminus_controller( + self, block_number: Optional[Union[str, int]] = "latest" + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.terminusController.call(block_identifier=block_number) + + def terminus_pool_capacity( + self, pool_id: int, block_number: Optional[Union[str, int]] = "latest" + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.terminusPoolCapacity.call( + pool_id, block_identifier=block_number + ) + + def terminus_pool_controller( + self, pool_id: int, block_number: Optional[Union[str, int]] = "latest" + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.terminusPoolController.call( + pool_id, block_identifier=block_number + ) + + def terminus_pool_supply( + self, pool_id: int, block_number: Optional[Union[str, int]] = "latest" + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.terminusPoolSupply.call( + pool_id, block_identifier=block_number + ) + + def total_pools(self, block_number: Optional[Union[str, int]] = "latest") -> Any: + self.assert_contract_is_instantiated() + return self.contract.totalPools.call(block_identifier=block_number) + + def unapprove_for_pool( + self, pool_id: int, operator: ChecksumAddress, transaction_config + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.unapproveForPool(pool_id, operator, transaction_config) + + def uri( + self, pool_id: int, block_number: Optional[Union[str, int]] = "latest" + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.uri.call(pool_id, block_identifier=block_number) + + def withdraw_payments( + self, to_address: ChecksumAddress, amount: int, transaction_config + ) -> Any: + self.assert_contract_is_instantiated() + return self.contract.withdrawPayments(to_address, 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.max_fee_per_gas is not None: + transaction_config["max_fee"] = args.max_fee_per_gas + if args.max_priority_fee_per_gas is not None: + transaction_config["priority_fee"] = args.max_priority_fee_per_gas + if args.confirmations is not None: + transaction_config["required_confs"] = args.confirmations + if args.nonce is not None: + transaction_config["nonce"] = args.nonce + 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: + parser.add_argument( + "--block-number", + required=False, + type=int, + help="Call at the given block number, defaults to latest", + ) + 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( + "--max-fee-per-gas", + default=None, + help="Max fee per gas for EIP1559 transactions", + ) + parser.add_argument( + "--max-priority-fee-per-gas", + default=None, + help="Max priority fee per gas for EIP1559 transactions", + ) + parser.add_argument( + "--confirmations", + type=int, + default=None, + help="Number of confirmations to await before considering a transaction completed", + ) + parser.add_argument( + "--nonce", type=int, default=None, help="Nonce for the transaction (optional)" + ) + parser.add_argument( + "--value", default=None, help="Value of the transaction in wei(optional)" + ) + parser.add_argument("--verbose", action="store_true", help="Print verbose output") + + +def handle_deploy(args: argparse.Namespace) -> None: + network.connect(args.network) + transaction_config = get_transaction_config(args) + contract = TerminusFacet(None) + result = contract.deploy(transaction_config=transaction_config) + print(result) + if args.verbose: + print(result.info()) + + +def handle_verify_contract(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = TerminusFacet(args.address) + result = contract.verify_contract() + print(result) + + +def handle_approve_for_pool(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = TerminusFacet(args.address) + transaction_config = get_transaction_config(args) + result = contract.approve_for_pool( + pool_id=args.pool_id, + operator=args.operator, + transaction_config=transaction_config, + ) + print(result) + if args.verbose: + print(result.info()) + + +def handle_balance_of(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = TerminusFacet(args.address) + result = contract.balance_of( + account=args.account, id=args.id, block_number=args.block_number + ) + print(result) + + +def handle_balance_of_batch(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = TerminusFacet(args.address) + result = contract.balance_of_batch( + accounts=args.accounts, ids=args.ids, block_number=args.block_number + ) + print(result) + + +def handle_burn(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = TerminusFacet(args.address) + transaction_config = get_transaction_config(args) + result = contract.burn( + from_=args.from_arg, + pool_id=args.pool_id, + amount=args.amount, + transaction_config=transaction_config, + ) + print(result) + if args.verbose: + print(result.info()) + + +def handle_contract_uri(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = TerminusFacet(args.address) + result = contract.contract_uri(block_number=args.block_number) + print(result) + + +def handle_create_pool_v1(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = TerminusFacet(args.address) + transaction_config = get_transaction_config(args) + result = contract.create_pool_v1( + _capacity=args.capacity_arg, + _transferable=args.transferable_arg, + _burnable=args.burnable_arg, + transaction_config=transaction_config, + ) + print(result) + if args.verbose: + print(result.info()) + + +def handle_create_pool_v2(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = TerminusFacet(args.address) + transaction_config = get_transaction_config(args) + result = contract.create_pool_v2( + _capacity=args.capacity_arg, + _transferable=args.transferable_arg, + _burnable=args.burnable_arg, + pool_uri=args.pool_uri, + transaction_config=transaction_config, + ) + print(result) + if args.verbose: + print(result.info()) + + +def handle_create_simple_pool(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = TerminusFacet(args.address) + transaction_config = get_transaction_config(args) + result = contract.create_simple_pool( + _capacity=args.capacity_arg, transaction_config=transaction_config + ) + print(result) + if args.verbose: + print(result.info()) + + +def handle_is_approved_for_all(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = TerminusFacet(args.address) + result = contract.is_approved_for_all( + account=args.account, operator=args.operator, block_number=args.block_number + ) + print(result) + + +def handle_is_approved_for_pool(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = TerminusFacet(args.address) + result = contract.is_approved_for_pool( + pool_id=args.pool_id, operator=args.operator, block_number=args.block_number + ) + print(result) + + +def handle_mint(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = TerminusFacet(args.address) + transaction_config = get_transaction_config(args) + result = contract.mint( + to=args.to, + pool_id=args.pool_id, + amount=args.amount, + data=args.data, + transaction_config=transaction_config, + ) + print(result) + if args.verbose: + print(result.info()) + + +def handle_mint_batch(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = TerminusFacet(args.address) + transaction_config = get_transaction_config(args) + result = contract.mint_batch( + to=args.to, + pool_i_ds=args.pool_i_ds, + amounts=args.amounts, + data=args.data, + transaction_config=transaction_config, + ) + print(result) + if args.verbose: + print(result.info()) + + +def handle_payment_token(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = TerminusFacet(args.address) + result = contract.payment_token(block_number=args.block_number) + print(result) + + +def handle_pool_base_price(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = TerminusFacet(args.address) + result = contract.pool_base_price(block_number=args.block_number) + print(result) + + +def handle_pool_is_burnable(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = TerminusFacet(args.address) + result = contract.pool_is_burnable( + pool_id=args.pool_id, block_number=args.block_number + ) + print(result) + + +def handle_pool_is_transferable(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = TerminusFacet(args.address) + result = contract.pool_is_transferable( + pool_id=args.pool_id, block_number=args.block_number + ) + print(result) + + +def handle_pool_mint_batch(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = TerminusFacet(args.address) + transaction_config = get_transaction_config(args) + result = contract.pool_mint_batch( + id=args.id, + to_addresses=args.to_addresses, + amounts=args.amounts, + transaction_config=transaction_config, + ) + print(result) + if args.verbose: + print(result.info()) + + +def handle_safe_batch_transfer_from(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = TerminusFacet(args.address) + transaction_config = get_transaction_config(args) + result = contract.safe_batch_transfer_from( + from_=args.from_arg, + to=args.to, + ids=args.ids, + amounts=args.amounts, + data=args.data, + transaction_config=transaction_config, + ) + print(result) + if args.verbose: + print(result.info()) + + +def handle_safe_transfer_from(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = TerminusFacet(args.address) + transaction_config = get_transaction_config(args) + result = contract.safe_transfer_from( + from_=args.from_arg, + to=args.to, + id=args.id, + amount=args.amount, + data=args.data, + transaction_config=transaction_config, + ) + print(result) + if args.verbose: + print(result.info()) + + +def handle_set_approval_for_all(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = TerminusFacet(args.address) + transaction_config = get_transaction_config(args) + result = contract.set_approval_for_all( + operator=args.operator, + approved=args.approved, + transaction_config=transaction_config, + ) + print(result) + if args.verbose: + print(result.info()) + + +def handle_set_contract_uri(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = TerminusFacet(args.address) + transaction_config = get_transaction_config(args) + result = contract.set_contract_uri( + _contract_uri=args.contract_uri_arg, transaction_config=transaction_config + ) + print(result) + if args.verbose: + print(result.info()) + + +def handle_set_controller(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = TerminusFacet(args.address) + transaction_config = get_transaction_config(args) + result = contract.set_controller( + new_controller=args.new_controller, transaction_config=transaction_config + ) + print(result) + if args.verbose: + print(result.info()) + + +def handle_set_payment_token(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = TerminusFacet(args.address) + transaction_config = get_transaction_config(args) + result = contract.set_payment_token( + new_payment_token=args.new_payment_token, transaction_config=transaction_config + ) + print(result) + if args.verbose: + print(result.info()) + + +def handle_set_pool_base_price(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = TerminusFacet(args.address) + transaction_config = get_transaction_config(args) + result = contract.set_pool_base_price( + new_base_price=args.new_base_price, transaction_config=transaction_config + ) + print(result) + if args.verbose: + print(result.info()) + + +def handle_set_pool_burnable(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = TerminusFacet(args.address) + transaction_config = get_transaction_config(args) + result = contract.set_pool_burnable( + pool_id=args.pool_id, + burnable=args.burnable, + transaction_config=transaction_config, + ) + print(result) + if args.verbose: + print(result.info()) + + +def handle_set_pool_controller(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = TerminusFacet(args.address) + transaction_config = get_transaction_config(args) + result = contract.set_pool_controller( + pool_id=args.pool_id, + new_controller=args.new_controller, + transaction_config=transaction_config, + ) + print(result) + if args.verbose: + print(result.info()) + + +def handle_set_pool_transferable(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = TerminusFacet(args.address) + transaction_config = get_transaction_config(args) + result = contract.set_pool_transferable( + pool_id=args.pool_id, + transferable=args.transferable, + transaction_config=transaction_config, + ) + print(result) + if args.verbose: + print(result.info()) + + +def handle_set_uri(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = TerminusFacet(args.address) + transaction_config = get_transaction_config(args) + result = contract.set_uri( + pool_id=args.pool_id, + pool_uri=args.pool_uri, + transaction_config=transaction_config, + ) + print(result) + if args.verbose: + print(result.info()) + + +def handle_supports_interface(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = TerminusFacet(args.address) + result = contract.supports_interface( + interface_id=args.interface_id, block_number=args.block_number + ) + print(result) + + +def handle_terminus_controller(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = TerminusFacet(args.address) + result = contract.terminus_controller(block_number=args.block_number) + print(result) + + +def handle_terminus_pool_capacity(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = TerminusFacet(args.address) + result = contract.terminus_pool_capacity( + pool_id=args.pool_id, block_number=args.block_number + ) + print(result) + + +def handle_terminus_pool_controller(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = TerminusFacet(args.address) + result = contract.terminus_pool_controller( + pool_id=args.pool_id, block_number=args.block_number + ) + print(result) + + +def handle_terminus_pool_supply(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = TerminusFacet(args.address) + result = contract.terminus_pool_supply( + pool_id=args.pool_id, block_number=args.block_number + ) + print(result) + + +def handle_total_pools(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = TerminusFacet(args.address) + result = contract.total_pools(block_number=args.block_number) + print(result) + + +def handle_unapprove_for_pool(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = TerminusFacet(args.address) + transaction_config = get_transaction_config(args) + result = contract.unapprove_for_pool( + pool_id=args.pool_id, + operator=args.operator, + transaction_config=transaction_config, + ) + print(result) + if args.verbose: + print(result.info()) + + +def handle_uri(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = TerminusFacet(args.address) + result = contract.uri(pool_id=args.pool_id, block_number=args.block_number) + print(result) + + +def handle_withdraw_payments(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = TerminusFacet(args.address) + transaction_config = get_transaction_config(args) + result = contract.withdraw_payments( + to_address=args.to_address, + amount=args.amount, + transaction_config=transaction_config, + ) + print(result) + if args.verbose: + print(result.info()) + + +def generate_cli() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="CLI for TerminusFacet") + 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) + + verify_contract_parser = subcommands.add_parser("verify-contract") + add_default_arguments(verify_contract_parser, False) + verify_contract_parser.set_defaults(func=handle_verify_contract) + + approve_for_pool_parser = subcommands.add_parser("approve-for-pool") + add_default_arguments(approve_for_pool_parser, True) + approve_for_pool_parser.add_argument( + "--pool-id", required=True, help="Type: uint256", type=int + ) + approve_for_pool_parser.add_argument( + "--operator", required=True, help="Type: address" + ) + approve_for_pool_parser.set_defaults(func=handle_approve_for_pool) + + 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.add_argument( + "--id", required=True, help="Type: uint256", type=int + ) + balance_of_parser.set_defaults(func=handle_balance_of) + + balance_of_batch_parser = subcommands.add_parser("balance-of-batch") + add_default_arguments(balance_of_batch_parser, False) + balance_of_batch_parser.add_argument( + "--accounts", required=True, help="Type: address[]", nargs="+" + ) + balance_of_batch_parser.add_argument( + "--ids", required=True, help="Type: uint256[]", nargs="+" + ) + balance_of_batch_parser.set_defaults(func=handle_balance_of_batch) + + burn_parser = subcommands.add_parser("burn") + add_default_arguments(burn_parser, True) + burn_parser.add_argument("--from-arg", required=True, help="Type: address") + burn_parser.add_argument("--pool-id", required=True, help="Type: uint256", type=int) + burn_parser.add_argument("--amount", required=True, help="Type: uint256", type=int) + burn_parser.set_defaults(func=handle_burn) + + contract_uri_parser = subcommands.add_parser("contract-uri") + add_default_arguments(contract_uri_parser, False) + contract_uri_parser.set_defaults(func=handle_contract_uri) + + create_pool_v1_parser = subcommands.add_parser("create-pool-v1") + add_default_arguments(create_pool_v1_parser, True) + create_pool_v1_parser.add_argument( + "--capacity-arg", required=True, help="Type: uint256", type=int + ) + create_pool_v1_parser.add_argument( + "--transferable-arg", + required=True, + help="Type: bool", + type=boolean_argument_type, + ) + create_pool_v1_parser.add_argument( + "--burnable-arg", required=True, help="Type: bool", type=boolean_argument_type + ) + create_pool_v1_parser.set_defaults(func=handle_create_pool_v1) + + create_pool_v2_parser = subcommands.add_parser("create-pool-v2") + add_default_arguments(create_pool_v2_parser, True) + create_pool_v2_parser.add_argument( + "--capacity-arg", required=True, help="Type: uint256", type=int + ) + create_pool_v2_parser.add_argument( + "--transferable-arg", + required=True, + help="Type: bool", + type=boolean_argument_type, + ) + create_pool_v2_parser.add_argument( + "--burnable-arg", required=True, help="Type: bool", type=boolean_argument_type + ) + create_pool_v2_parser.add_argument( + "--pool-uri", required=True, help="Type: string", type=str + ) + create_pool_v2_parser.set_defaults(func=handle_create_pool_v2) + + create_simple_pool_parser = subcommands.add_parser("create-simple-pool") + add_default_arguments(create_simple_pool_parser, True) + create_simple_pool_parser.add_argument( + "--capacity-arg", required=True, help="Type: uint256", type=int + ) + create_simple_pool_parser.set_defaults(func=handle_create_simple_pool) + + is_approved_for_all_parser = subcommands.add_parser("is-approved-for-all") + add_default_arguments(is_approved_for_all_parser, False) + is_approved_for_all_parser.add_argument( + "--account", required=True, help="Type: address" + ) + is_approved_for_all_parser.add_argument( + "--operator", required=True, help="Type: address" + ) + is_approved_for_all_parser.set_defaults(func=handle_is_approved_for_all) + + is_approved_for_pool_parser = subcommands.add_parser("is-approved-for-pool") + add_default_arguments(is_approved_for_pool_parser, False) + is_approved_for_pool_parser.add_argument( + "--pool-id", required=True, help="Type: uint256", type=int + ) + is_approved_for_pool_parser.add_argument( + "--operator", required=True, help="Type: address" + ) + is_approved_for_pool_parser.set_defaults(func=handle_is_approved_for_pool) + + mint_parser = subcommands.add_parser("mint") + add_default_arguments(mint_parser, True) + mint_parser.add_argument("--to", required=True, help="Type: address") + mint_parser.add_argument("--pool-id", required=True, help="Type: uint256", type=int) + mint_parser.add_argument("--amount", required=True, help="Type: uint256", type=int) + mint_parser.add_argument( + "--data", required=True, help="Type: bytes", type=bytes_argument_type + ) + mint_parser.set_defaults(func=handle_mint) + + mint_batch_parser = subcommands.add_parser("mint-batch") + add_default_arguments(mint_batch_parser, True) + mint_batch_parser.add_argument("--to", required=True, help="Type: address") + mint_batch_parser.add_argument( + "--pool-i-ds", required=True, help="Type: uint256[]", nargs="+" + ) + mint_batch_parser.add_argument( + "--amounts", required=True, help="Type: uint256[]", nargs="+" + ) + mint_batch_parser.add_argument( + "--data", required=True, help="Type: bytes", type=bytes_argument_type + ) + mint_batch_parser.set_defaults(func=handle_mint_batch) + + payment_token_parser = subcommands.add_parser("payment-token") + add_default_arguments(payment_token_parser, False) + payment_token_parser.set_defaults(func=handle_payment_token) + + pool_base_price_parser = subcommands.add_parser("pool-base-price") + add_default_arguments(pool_base_price_parser, False) + pool_base_price_parser.set_defaults(func=handle_pool_base_price) + + pool_is_burnable_parser = subcommands.add_parser("pool-is-burnable") + add_default_arguments(pool_is_burnable_parser, False) + pool_is_burnable_parser.add_argument( + "--pool-id", required=True, help="Type: uint256", type=int + ) + pool_is_burnable_parser.set_defaults(func=handle_pool_is_burnable) + + pool_is_transferable_parser = subcommands.add_parser("pool-is-transferable") + add_default_arguments(pool_is_transferable_parser, False) + pool_is_transferable_parser.add_argument( + "--pool-id", required=True, help="Type: uint256", type=int + ) + pool_is_transferable_parser.set_defaults(func=handle_pool_is_transferable) + + pool_mint_batch_parser = subcommands.add_parser("pool-mint-batch") + add_default_arguments(pool_mint_batch_parser, True) + pool_mint_batch_parser.add_argument( + "--id", required=True, help="Type: uint256", type=int + ) + pool_mint_batch_parser.add_argument( + "--to-addresses", required=True, help="Type: address[]", nargs="+" + ) + pool_mint_batch_parser.add_argument( + "--amounts", required=True, help="Type: uint256[]", nargs="+" + ) + pool_mint_batch_parser.set_defaults(func=handle_pool_mint_batch) + + safe_batch_transfer_from_parser = subcommands.add_parser("safe-batch-transfer-from") + add_default_arguments(safe_batch_transfer_from_parser, True) + safe_batch_transfer_from_parser.add_argument( + "--from-arg", required=True, help="Type: address" + ) + safe_batch_transfer_from_parser.add_argument( + "--to", required=True, help="Type: address" + ) + safe_batch_transfer_from_parser.add_argument( + "--ids", required=True, help="Type: uint256[]", nargs="+" + ) + safe_batch_transfer_from_parser.add_argument( + "--amounts", required=True, help="Type: uint256[]", nargs="+" + ) + safe_batch_transfer_from_parser.add_argument( + "--data", required=True, help="Type: bytes", type=bytes_argument_type + ) + safe_batch_transfer_from_parser.set_defaults(func=handle_safe_batch_transfer_from) + + safe_transfer_from_parser = subcommands.add_parser("safe-transfer-from") + add_default_arguments(safe_transfer_from_parser, True) + safe_transfer_from_parser.add_argument( + "--from-arg", required=True, help="Type: address" + ) + safe_transfer_from_parser.add_argument("--to", required=True, help="Type: address") + safe_transfer_from_parser.add_argument( + "--id", required=True, help="Type: uint256", type=int + ) + safe_transfer_from_parser.add_argument( + "--amount", required=True, help="Type: uint256", type=int + ) + safe_transfer_from_parser.add_argument( + "--data", required=True, help="Type: bytes", type=bytes_argument_type + ) + safe_transfer_from_parser.set_defaults(func=handle_safe_transfer_from) + + set_approval_for_all_parser = subcommands.add_parser("set-approval-for-all") + add_default_arguments(set_approval_for_all_parser, True) + set_approval_for_all_parser.add_argument( + "--operator", required=True, help="Type: address" + ) + set_approval_for_all_parser.add_argument( + "--approved", required=True, help="Type: bool", type=boolean_argument_type + ) + set_approval_for_all_parser.set_defaults(func=handle_set_approval_for_all) + + set_contract_uri_parser = subcommands.add_parser("set-contract-uri") + add_default_arguments(set_contract_uri_parser, True) + set_contract_uri_parser.add_argument( + "--contract-uri-arg", required=True, help="Type: string", type=str + ) + set_contract_uri_parser.set_defaults(func=handle_set_contract_uri) + + set_controller_parser = subcommands.add_parser("set-controller") + add_default_arguments(set_controller_parser, True) + set_controller_parser.add_argument( + "--new-controller", required=True, help="Type: address" + ) + set_controller_parser.set_defaults(func=handle_set_controller) + + set_payment_token_parser = subcommands.add_parser("set-payment-token") + add_default_arguments(set_payment_token_parser, True) + set_payment_token_parser.add_argument( + "--new-payment-token", required=True, help="Type: address" + ) + set_payment_token_parser.set_defaults(func=handle_set_payment_token) + + set_pool_base_price_parser = subcommands.add_parser("set-pool-base-price") + add_default_arguments(set_pool_base_price_parser, True) + set_pool_base_price_parser.add_argument( + "--new-base-price", required=True, help="Type: uint256", type=int + ) + set_pool_base_price_parser.set_defaults(func=handle_set_pool_base_price) + + set_pool_burnable_parser = subcommands.add_parser("set-pool-burnable") + add_default_arguments(set_pool_burnable_parser, True) + set_pool_burnable_parser.add_argument( + "--pool-id", required=True, help="Type: uint256", type=int + ) + set_pool_burnable_parser.add_argument( + "--burnable", required=True, help="Type: bool", type=boolean_argument_type + ) + set_pool_burnable_parser.set_defaults(func=handle_set_pool_burnable) + + set_pool_controller_parser = subcommands.add_parser("set-pool-controller") + add_default_arguments(set_pool_controller_parser, True) + set_pool_controller_parser.add_argument( + "--pool-id", required=True, help="Type: uint256", type=int + ) + set_pool_controller_parser.add_argument( + "--new-controller", required=True, help="Type: address" + ) + set_pool_controller_parser.set_defaults(func=handle_set_pool_controller) + + set_pool_transferable_parser = subcommands.add_parser("set-pool-transferable") + add_default_arguments(set_pool_transferable_parser, True) + set_pool_transferable_parser.add_argument( + "--pool-id", required=True, help="Type: uint256", type=int + ) + set_pool_transferable_parser.add_argument( + "--transferable", required=True, help="Type: bool", type=boolean_argument_type + ) + set_pool_transferable_parser.set_defaults(func=handle_set_pool_transferable) + + set_uri_parser = subcommands.add_parser("set-uri") + add_default_arguments(set_uri_parser, True) + set_uri_parser.add_argument( + "--pool-id", required=True, help="Type: uint256", type=int + ) + set_uri_parser.add_argument( + "--pool-uri", required=True, help="Type: string", type=str + ) + set_uri_parser.set_defaults(func=handle_set_uri) + + supports_interface_parser = subcommands.add_parser("supports-interface") + add_default_arguments(supports_interface_parser, False) + supports_interface_parser.add_argument( + "--interface-id", required=True, help="Type: bytes4", type=bytes_argument_type + ) + supports_interface_parser.set_defaults(func=handle_supports_interface) + + terminus_controller_parser = subcommands.add_parser("terminus-controller") + add_default_arguments(terminus_controller_parser, False) + terminus_controller_parser.set_defaults(func=handle_terminus_controller) + + terminus_pool_capacity_parser = subcommands.add_parser("terminus-pool-capacity") + add_default_arguments(terminus_pool_capacity_parser, False) + terminus_pool_capacity_parser.add_argument( + "--pool-id", required=True, help="Type: uint256", type=int + ) + terminus_pool_capacity_parser.set_defaults(func=handle_terminus_pool_capacity) + + terminus_pool_controller_parser = subcommands.add_parser("terminus-pool-controller") + add_default_arguments(terminus_pool_controller_parser, False) + terminus_pool_controller_parser.add_argument( + "--pool-id", required=True, help="Type: uint256", type=int + ) + terminus_pool_controller_parser.set_defaults(func=handle_terminus_pool_controller) + + terminus_pool_supply_parser = subcommands.add_parser("terminus-pool-supply") + add_default_arguments(terminus_pool_supply_parser, False) + terminus_pool_supply_parser.add_argument( + "--pool-id", required=True, help="Type: uint256", type=int + ) + terminus_pool_supply_parser.set_defaults(func=handle_terminus_pool_supply) + + total_pools_parser = subcommands.add_parser("total-pools") + add_default_arguments(total_pools_parser, False) + total_pools_parser.set_defaults(func=handle_total_pools) + + unapprove_for_pool_parser = subcommands.add_parser("unapprove-for-pool") + add_default_arguments(unapprove_for_pool_parser, True) + unapprove_for_pool_parser.add_argument( + "--pool-id", required=True, help="Type: uint256", type=int + ) + unapprove_for_pool_parser.add_argument( + "--operator", required=True, help="Type: address" + ) + unapprove_for_pool_parser.set_defaults(func=handle_unapprove_for_pool) + + uri_parser = subcommands.add_parser("uri") + add_default_arguments(uri_parser, False) + uri_parser.add_argument("--pool-id", required=True, help="Type: uint256", type=int) + uri_parser.set_defaults(func=handle_uri) + + withdraw_payments_parser = subcommands.add_parser("withdraw-payments") + add_default_arguments(withdraw_payments_parser, True) + withdraw_payments_parser.add_argument( + "--to-address", required=True, help="Type: address" + ) + withdraw_payments_parser.add_argument( + "--amount", required=True, help="Type: uint256", type=int + ) + withdraw_payments_parser.set_defaults(func=handle_withdraw_payments) + + return parser + + +def main() -> None: + parser = generate_cli() + args = parser.parse_args() + args.func(args) + + +if __name__ == "__main__": + main() diff --git a/cli/web3cli/TerminusInitializer.py b/cli/web3cli/TerminusInitializer.py new file mode 100644 index 00000000..635e3be8 --- /dev/null +++ b/cli/web3cli/TerminusInitializer.py @@ -0,0 +1,225 @@ +# Code generated by moonworm : https://github.com/moonstream-to/moonworm +# Moonworm version : 0.7.1 + +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) -> str: + return raw_value + + +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 TerminusInitializer: + def __init__(self, contract_address: Optional[ChecksumAddress]): + self.contract_name = "TerminusInitializer" + self.address = contract_address + self.contract = None + self.abi = get_abi_json("TerminusInitializer") + 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 + return deployed_contract.tx + + def assert_contract_is_instantiated(self) -> None: + if self.contract is None: + raise Exception("contract has not been instantiated") + + def verify_contract(self): + self.assert_contract_is_instantiated() + contract_class = contract_from_build(self.contract_name) + contract_class.publish_source(self.contract) + + 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.max_fee_per_gas is not None: + transaction_config["max_fee"] = args.max_fee_per_gas + if args.max_priority_fee_per_gas is not None: + transaction_config["priority_fee"] = args.max_priority_fee_per_gas + if args.confirmations is not None: + transaction_config["required_confs"] = args.confirmations + if args.nonce is not None: + transaction_config["nonce"] = args.nonce + 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: + parser.add_argument( + "--block-number", + required=False, + type=int, + help="Call at the given block number, defaults to latest", + ) + 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( + "--max-fee-per-gas", + default=None, + help="Max fee per gas for EIP1559 transactions", + ) + parser.add_argument( + "--max-priority-fee-per-gas", + default=None, + help="Max priority fee per gas for EIP1559 transactions", + ) + parser.add_argument( + "--confirmations", + type=int, + default=None, + help="Number of confirmations to await before considering a transaction completed", + ) + parser.add_argument( + "--nonce", type=int, default=None, help="Nonce for the transaction (optional)" + ) + parser.add_argument( + "--value", default=None, help="Value of the transaction in wei(optional)" + ) + parser.add_argument("--verbose", action="store_true", help="Print verbose output") + + +def handle_deploy(args: argparse.Namespace) -> None: + network.connect(args.network) + transaction_config = get_transaction_config(args) + contract = TerminusInitializer(None) + result = contract.deploy(transaction_config=transaction_config) + print(result) + if args.verbose: + print(result.info()) + + +def handle_verify_contract(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = TerminusInitializer(args.address) + result = contract.verify_contract() + print(result) + + +def handle_init(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = TerminusInitializer(args.address) + transaction_config = get_transaction_config(args) + result = contract.init(transaction_config=transaction_config) + print(result) + if args.verbose: + print(result.info()) + + +def generate_cli() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="CLI for TerminusInitializer") + 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) + + verify_contract_parser = subcommands.add_parser("verify-contract") + add_default_arguments(verify_contract_parser, False) + verify_contract_parser.set_defaults(func=handle_verify_contract) + + 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() diff --git a/cli/web3cli/TerminusPermissions.py b/cli/web3cli/TerminusPermissions.py new file mode 100644 index 00000000..8ae25524 --- /dev/null +++ b/cli/web3cli/TerminusPermissions.py @@ -0,0 +1,207 @@ +# Code generated by moonworm : https://github.com/moonstream-to/moonworm +# Moonworm version : 0.7.1 + +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) -> str: + return raw_value + + +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 TerminusPermissions: + def __init__(self, contract_address: Optional[ChecksumAddress]): + self.contract_name = "TerminusPermissions" + self.address = contract_address + self.contract = None + self.abi = get_abi_json("TerminusPermissions") + 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 + return deployed_contract.tx + + def assert_contract_is_instantiated(self) -> None: + if self.contract is None: + raise Exception("contract has not been instantiated") + + def verify_contract(self): + self.assert_contract_is_instantiated() + contract_class = contract_from_build(self.contract_name) + contract_class.publish_source(self.contract) + + +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.max_fee_per_gas is not None: + transaction_config["max_fee"] = args.max_fee_per_gas + if args.max_priority_fee_per_gas is not None: + transaction_config["priority_fee"] = args.max_priority_fee_per_gas + if args.confirmations is not None: + transaction_config["required_confs"] = args.confirmations + if args.nonce is not None: + transaction_config["nonce"] = args.nonce + 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: + parser.add_argument( + "--block-number", + required=False, + type=int, + help="Call at the given block number, defaults to latest", + ) + 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( + "--max-fee-per-gas", + default=None, + help="Max fee per gas for EIP1559 transactions", + ) + parser.add_argument( + "--max-priority-fee-per-gas", + default=None, + help="Max priority fee per gas for EIP1559 transactions", + ) + parser.add_argument( + "--confirmations", + type=int, + default=None, + help="Number of confirmations to await before considering a transaction completed", + ) + parser.add_argument( + "--nonce", type=int, default=None, help="Nonce for the transaction (optional)" + ) + parser.add_argument( + "--value", default=None, help="Value of the transaction in wei(optional)" + ) + parser.add_argument("--verbose", action="store_true", help="Print verbose output") + + +def handle_deploy(args: argparse.Namespace) -> None: + network.connect(args.network) + transaction_config = get_transaction_config(args) + contract = TerminusPermissions(None) + result = contract.deploy(transaction_config=transaction_config) + print(result) + if args.verbose: + print(result.info()) + + +def handle_verify_contract(args: argparse.Namespace) -> None: + network.connect(args.network) + contract = TerminusPermissions(args.address) + result = contract.verify_contract() + print(result) + + +def generate_cli() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="CLI for TerminusPermissions") + 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) + + verify_contract_parser = subcommands.add_parser("verify-contract") + add_default_arguments(verify_contract_parser, False) + verify_contract_parser.set_defaults(func=handle_verify_contract) + + return parser + + +def main() -> None: + parser = generate_cli() + args = parser.parse_args() + args.func(args) + + +if __name__ == "__main__": + main() diff --git a/cli/web3cli/cli.py b/cli/web3cli/cli.py index f957fb08..de14a558 100644 --- a/cli/web3cli/cli.py +++ b/cli/web3cli/cli.py @@ -12,12 +12,12 @@ Lootbox, MockErc20, MockERC721, - ITerminus, setup_drop, CraftingFacet, GOFPFacet, GOFPPredicates, InventoryFacet, + TerminusFacet, StatBlock, ) @@ -57,7 +57,7 @@ def main() -> None: drop_parser = drop.generate_cli() subparsers.add_parser("drop", parents=[drop_parser], add_help=False) - terminus_parser = ITerminus.generate_cli() + terminus_parser = TerminusFacet.generate_cli() subparsers.add_parser("terminus", parents=[terminus_parser], add_help=False) crafting_parser = CraftingFacet.generate_cli() diff --git a/cli/web3cli/core.py b/cli/web3cli/core.py index 4b05a01a..4de58ca6 100644 --- a/cli/web3cli/core.py +++ b/cli/web3cli/core.py @@ -24,6 +24,8 @@ CraftingFacet, GOFPFacet, InventoryFacet, + TerminusFacet, + TerminusInitializer, ) FACETS: Dict[str, Any] = { @@ -35,6 +37,7 @@ "CraftingFacet": CraftingFacet, "GOFPFacet": GOFPFacet, "InventoryFacet": InventoryFacet, + "TerminusFacet": TerminusFacet, } FACET_INIT_CALLDATA: Dict[str, str] = { @@ -47,6 +50,9 @@ "InventoryFacet": lambda address, *args: InventoryFacet.InventoryFacet( address ).contract.init.encode_input(*args), + "TerminusFacet": lambda address, *args: TerminusInitializer.TerminusInitializer( + address + ).contract.init.encode_input(*args), } DIAMOND_FACET_PRECEDENCE: List[str] = [ @@ -66,6 +72,7 @@ class EngineFeatures(Enum): DROPPER = "DropperFacet" GOFP = "GOFPFacet" INVENTORY = "InventoryFacet" + TERMINUS = "TerminusFacet" def feature_from_facet_name(facet_name: str) -> Optional[EngineFeatures]: @@ -79,12 +86,14 @@ def feature_from_facet_name(facet_name: str) -> Optional[EngineFeatures]: EngineFeatures.DROPPER: ["DropperFacet"], EngineFeatures.GOFP: ["GOFPFacet"], EngineFeatures.INVENTORY: ["InventoryFacet"], + EngineFeatures.TERMINUS: ["TerminusFacet"], } FEATURE_IGNORES: Dict[EngineFeatures, List[str]] = { EngineFeatures.DROPPER: {"methods": ["init"], "selectors": []}, EngineFeatures.GOFP: {"methods": ["init"], "selectors": []}, EngineFeatures.INVENTORY: {"methods": ["init"], "selectors": []}, + EngineFeatures.TERMINUS: {"methods": [], "selectors": []}, } FACET_ACTIONS: Dict[str, int] = {"add": 0, "replace": 1, "remove": 2} @@ -710,6 +719,67 @@ def inventory_gogogo( return deployment_info +def terminus_gogogo( + transaction_config: Dict[str, Any], + diamond_cut_address: Optional[str] = None, + diamond_address: Optional[str] = None, + diamond_loupe_address: Optional[str] = None, + ownership_address: Optional[str] = None, + terminus_facet_address: Optional[str] = None, + terminus_initializer_address: Optional[str] = None, + verify_contracts: Optional[bool] = False, +) -> Dict[str, Any]: + """ + Deploys an EIP2535 Diamond contract and a TerminusFacet and mounts the TerminusFacet onto the Diamond contract. + + Returns the addresses and attachments. + """ + deployment_info = diamond_gogogo( + owner_address=transaction_config["from"].address, + transaction_config=transaction_config, + diamond_cut_address=diamond_cut_address, + diamond_address=diamond_address, + diamond_loupe_address=diamond_loupe_address, + ownership_address=ownership_address, + verify_contracts=verify_contracts, + ) + + if terminus_facet_address is None: + terminus_facet = TerminusFacet.TerminusFacet(None) + terminus_facet.deploy(transaction_config=transaction_config) + else: + terminus_facet = TerminusFacet.TerminusFacet(terminus_facet_address) + + if terminus_initializer_address is None: + terminus_initializer = TerminusInitializer.TerminusInitializer(None) + terminus_initializer.deploy(transaction_config=transaction_config) + terminus_initializer_address = terminus_initializer.address + + deployment_info["contracts"]["TerminusFacet"] = terminus_facet.address + deployment_info["contracts"]["TerminusInitializer"] = terminus_initializer_address + + if verify_contracts: + try: + terminus_facet.verify_contract() + deployment_info["verified"].append("InventoryFacet") + except Exception as e: + deployment_info["verification_errors"].append(repr(e)) + + facet_cut( + deployment_info["contracts"]["Diamond"], + "TerminusFacet", + terminus_facet.address, + "add", + transaction_config, + initializer_address=terminus_initializer_address, + feature=EngineFeatures.TERMINUS, + initializer_args=[], + ) + deployment_info["attached"].append("TerminusFacet") + + return deployment_info + + def handle_facet_cut(args: argparse.Namespace) -> None: network.connect(args.network) diamond_address = args.address @@ -805,6 +875,25 @@ def handle_inventory_gogogo(args: argparse.Namespace) -> None: json.dump(result, sys.stdout, indent=4) +def handle_terminus_gogogo(args: argparse.Namespace) -> None: + network.connect(args.network) + transaction_config = TerminusFacet.get_transaction_config(args) + result = terminus_gogogo( + transaction_config=transaction_config, + diamond_cut_address=args.diamond_cut_address, + diamond_address=args.diamond_address, + diamond_loupe_address=args.diamond_loupe_address, + ownership_address=args.ownership_address, + terminus_facet_address=args.terminus_facet_address, + terminus_initializer_address=args.terminus_initializer_address, + verify_contracts=args.verify_contracts, + ) + if args.outfile is not None: + with args.outfile: + json.dump(result, args.outfile) + json.dump(result, sys.stdout, indent=4) + + def handle_crafting_gogogo(args: argparse.Namespace) -> None: network.connect(args.network) @@ -1044,6 +1133,61 @@ def generate_cli(): ) inventory_gogogo_parser.set_defaults(func=handle_inventory_gogogo) + terminus_gogogo_parser = subcommands.add_parser( + "terminus-gogogo", + description="Deploy Terminus diamond contract", + ) + Diamond.add_default_arguments(terminus_gogogo_parser, transact=True) + terminus_gogogo_parser.add_argument( + "--verify-contracts", + action="store_true", + help="Verify contracts", + ) + terminus_gogogo_parser.add_argument( + "--diamond-cut-address", + required=False, + default=None, + help="Address to deployed DiamondCutFacet. If provided, this command skips deployment of a new DiamondCutFacet.", + ) + terminus_gogogo_parser.add_argument( + "--diamond-address", + required=False, + default=None, + help="Address to deployed Diamond contract. If provided, this command skips deployment of a new Diamond contract and simply mounts the required facets onto the existing Diamond contract. Assumes that there is no collision of selectors.", + ) + terminus_gogogo_parser.add_argument( + "--diamond-loupe-address", + required=False, + default=None, + help="Address to deployed DiamondLoupeFacet. If provided, this command skips deployment of a new DiamondLoupeFacet. It mounts the existing DiamondLoupeFacet onto the Diamond.", + ) + terminus_gogogo_parser.add_argument( + "--ownership-address", + required=False, + default=None, + help="Address to deployed OwnershipFacet. If provided, this command skips deployment of a new OwnershipFacet. It mounts the existing OwnershipFacet onto the Diamond.", + ) + terminus_gogogo_parser.add_argument( + "--terminus-facet-address", + required=False, + default=None, + help="Address to deployed TerminusFacet. If provided, this command skips deployment of a new TerminusFacet. It mounts the existing TerminusFacet onto the Diamond.", + ) + terminus_gogogo_parser.add_argument( + "--terminus-initializer-address", + required=False, + default=None, + help="Address to deployed TerminusInitializer. If provided, this command skips deployment of a new TerminusInitializer. It uses the given TerminusInitializer to initialize the diamond upon the mounting of the TerminusFacet.", + ) + terminus_gogogo_parser.add_argument( + "-o", + "--outfile", + type=argparse.FileType("w"), + default=None, + help="(Optional) file to write deployed addresses to", + ) + terminus_gogogo_parser.set_defaults(func=handle_terminus_gogogo) + lootbox_gogogo_parser = subcommands.add_parser( "lootbox-gogogo", help="Deploys Lootbox contract", diff --git a/cli/web3cli/test_terminus.py b/cli/web3cli/test_terminus.py new file mode 100644 index 00000000..74020b15 --- /dev/null +++ b/cli/web3cli/test_terminus.py @@ -0,0 +1,1141 @@ +from typing import List +import unittest + +from brownie import accounts, network +from brownie.exceptions import VirtualMachineError + +from . import MockErc20, TerminusFacet +from .core import terminus_gogogo + + +class TerminusTestCase(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + try: + network.connect() + except: + pass + + cls.deployment_info = terminus_gogogo({"from": accounts[0]}) + cls.terminus = TerminusFacet.TerminusFacet( + cls.deployment_info["contracts"]["Diamond"] + ) + + cls.erc20 = MockErc20.MockErc20(None) + cls.erc20.deploy("test", "test", {"from": accounts[0]}) + + +class TestDeployment(TerminusTestCase): + def test_deployment(self): + controller = self.terminus.terminus_controller() + self.assertEqual(controller, accounts[0].address) + + +class TestController(TerminusTestCase): + def test_set_controller_fails_when_not_called_by_controller(self): + with self.assertRaises(VirtualMachineError): + self.terminus.set_controller(accounts[1].address, {"from": accounts[1]}) + + def test_set_controller_fails_when_not_called_by_controller_even_if_they_change_to_existing_controller( + self, + ): + with self.assertRaises(VirtualMachineError): + self.terminus.set_controller(accounts[0].address, {"from": accounts[1]}) + + def test_set_controller(self): + self.assertEqual(self.terminus.terminus_controller(), accounts[0].address) + self.terminus.set_controller(accounts[3].address, {"from": accounts[0]}) + self.assertEqual(self.terminus.terminus_controller(), accounts[3].address) + self.terminus.set_controller(accounts[0].address, {"from": accounts[3]}) + self.assertEqual(self.terminus.terminus_controller(), accounts[0].address) + + +class TestContractURI(TerminusTestCase): + def test_contract_uri(self): + contract_uri = self.terminus.contract_uri() + self.assertEqual(contract_uri, "") + + self.terminus.set_contract_uri("https://example.com", {"from": accounts[0]}) + + contract_uri = self.terminus.contract_uri() + self.assertEqual(contract_uri, "https://example.com") + + +class TestSimplePoolCreation(TerminusTestCase): + def test_create_simple_pool(self): + self.terminus.set_payment_token(self.erc20.address, {"from": accounts[0]}) + payment_token = self.terminus.payment_token() + self.assertEqual(payment_token, self.erc20.address) + + self.terminus.set_pool_base_price(1000, {"from": accounts[0]}) + pool_base_price = self.terminus.pool_base_price() + self.assertEqual(pool_base_price, 1000) + + self.terminus.set_controller(accounts[1].address, {"from": accounts[0]}) + + self.erc20.mint(accounts[1], 1000, {"from": accounts[0]}) + initial_payer_balance = self.erc20.balance_of(accounts[1].address) + initial_terminus_balance = self.erc20.balance_of(self.terminus.address) + initial_controller_balance = self.erc20.balance_of(accounts[1].address) + + self.erc20.approve(self.terminus.address, 1000, {"from": accounts[1]}) + + initial_total_pools = self.terminus.total_pools() + + self.terminus.create_simple_pool(10, {"from": accounts[1]}) + + final_total_pools = self.terminus.total_pools() + self.assertEqual(final_total_pools, initial_total_pools + 1) + + final_payer_balance = self.erc20.balance_of(accounts[1].address) + intermediate_terminus_balance = self.erc20.balance_of(self.terminus.address) + intermediate_controller_balance = self.erc20.balance_of(accounts[1].address) + self.assertEqual(final_payer_balance, initial_payer_balance - 1000) + self.assertEqual(intermediate_terminus_balance, initial_terminus_balance + 1000) + self.assertEqual( + intermediate_controller_balance, initial_controller_balance - 1000 + ) + + with self.assertRaises(Exception): + self.terminus.withdraw_payments( + accounts[0].address, 1000, {"from": accounts[0]} + ) + + with self.assertRaises(Exception): + self.terminus.withdraw_payments( + accounts[1].address, 1000, {"from": accounts[0]} + ) + + with self.assertRaises(Exception): + self.terminus.withdraw_payments( + accounts[0].address, 1000, {"from": accounts[1]} + ) + + self.terminus.withdraw_payments( + accounts[1].address, 1000, {"from": accounts[1]} + ) + + final_terminus_balance = self.erc20.balance_of(self.terminus.address) + final_controller_balance = self.erc20.balance_of(accounts[1].address) + self.assertEqual(final_terminus_balance, intermediate_terminus_balance - 1000) + self.assertEqual( + final_controller_balance, intermediate_controller_balance + 1000 + ) + + with self.assertRaises(Exception): + self.terminus.withdraw_payments( + accounts[0].address, + final_terminus_balance + 1000, + {"from": accounts[0]}, + ) + + pool_controller = self.terminus.terminus_pool_controller(final_total_pools) + self.assertEqual(pool_controller, accounts[1].address) + + pool_capacity = self.terminus.terminus_pool_capacity(final_total_pools) + self.assertEqual(pool_capacity, 10) + + +class TestPoolOperations(TerminusTestCase): + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + cls.erc20 = MockErc20.MockErc20(None) + cls.erc20.deploy("test", "test", {"from": accounts[0]}) + cls.terminus.set_payment_token(cls.erc20.address, {"from": accounts[0]}) + cls.terminus.set_pool_base_price(1000, {"from": accounts[0]}) + cls.erc20.mint(accounts[1], 1000000, {"from": accounts[0]}) + cls.erc20.approve(cls.terminus.address, 1000000, {"from": accounts[1]}) + + cls.terminus.set_controller(accounts[1].address, {"from": accounts[0]}) + + def setUp(self) -> None: + self.terminus.create_simple_pool(10, {"from": accounts[1]}) + + def test_set_pool_controller(self): + pool_id = self.terminus.total_pools() + old_controller = accounts[1] + new_controller = accounts[2] + + current_controller_address = self.terminus.terminus_pool_controller(pool_id) + self.assertEqual(current_controller_address, old_controller.address) + + with self.assertRaises(Exception): + self.terminus.set_pool_controller( + pool_id, new_controller.address, {"from": new_controller} + ) + current_controller_address = self.terminus.terminus_pool_controller(pool_id) + self.assertEqual(current_controller_address, old_controller.address) + + self.terminus.set_pool_controller( + pool_id, new_controller.address, {"from": old_controller} + ) + current_controller_address = self.terminus.terminus_pool_controller(pool_id) + self.assertEqual(current_controller_address, new_controller.address) + + with self.assertRaises(Exception): + self.terminus.set_pool_controller( + pool_id, old_controller.address, {"from": old_controller} + ) + current_controller_address = self.terminus.terminus_pool_controller(pool_id) + self.assertEqual(current_controller_address, new_controller.address) + + self.terminus.set_pool_controller( + pool_id, old_controller.address, {"from": new_controller} + ) + current_controller_address = self.terminus.terminus_pool_controller(pool_id) + self.assertEqual(current_controller_address, old_controller.address) + + def test_mint(self): + pool_id = self.terminus.total_pools() + self.terminus.mint(accounts[2], pool_id, 1, b"", {"from": accounts[1]}) + + balance = self.terminus.balance_of(accounts[2].address, pool_id) + self.assertEqual(balance, 1) + + supply = self.terminus.terminus_pool_supply(pool_id) + self.assertEqual(supply, 1) + + def test_mint_fails_if_it_exceeds_capacity(self): + pool_id = self.terminus.total_pools() + with self.assertRaises(Exception): + self.terminus.mint(accounts[2], pool_id, 11, b"", {"from": accounts[1]}) + + balance = self.terminus.balance_of(accounts[2].address, pool_id) + self.assertEqual(balance, 0) + + supply = self.terminus.terminus_pool_supply(pool_id) + self.assertEqual(supply, 0) + + def test_mint_batch(self): + pool_id = self.terminus.total_pools() + self.terminus.mint_batch( + accounts[2].address, + pool_i_ds=[pool_id], + amounts=[1], + data=b"", + transaction_config={"from": accounts[1]}, + ) + + balance = self.terminus.balance_of(accounts[2].address, pool_id) + self.assertEqual(balance, 1) + + supply = self.terminus.terminus_pool_supply(pool_id) + self.assertEqual(supply, 1) + + def test_mint_batch_with_approval(self): + pool_id = self.terminus.total_pools() + + self.assertFalse(self.terminus.is_approved_for_pool(pool_id, accounts[3])) + self.assertFalse(self.terminus.is_approved_for_pool(pool_id - 1, accounts[3])) + balances_before = [ + self.terminus.balance_of(accounts[2].address, pool_id), + self.terminus.balance_of(accounts[2].address, pool_id - 1), + ] + supply_before = [ + self.terminus.terminus_pool_supply(pool_id), + self.terminus.terminus_pool_supply(pool_id - 1), + ] + self.terminus.approve_for_pool(pool_id, accounts[3], {"from": accounts[1]}) + with self.assertRaises(Exception): + self.terminus.mint_batch( + accounts[2].address, + pool_i_ds=[pool_id, pool_id - 1], + amounts=[1, 1], + data=b"", + transaction_config={"from": accounts[3]}, + ) + + self.terminus.approve_for_pool(pool_id - 1, accounts[3], {"from": accounts[1]}) + + self.terminus.mint_batch( + accounts[2].address, + pool_i_ds=[pool_id, pool_id - 1], + amounts=[1, 1], + data=b"", + transaction_config={"from": accounts[3]}, + ) + + self.assertEqual( + self.terminus.balance_of(accounts[2].address, pool_id), + balances_before[0] + 1, + ) + self.assertEqual( + self.terminus.balance_of(accounts[2].address, pool_id - 1), + balances_before[1] + 1, + ) + + self.assertEqual( + self.terminus.terminus_pool_supply(pool_id), supply_before[0] + 1 + ) + self.assertEqual( + self.terminus.terminus_pool_supply(pool_id - 1), + supply_before[1] + 1, + ) + + def test_mint_batch_fails_if_it_exceeds_capacity(self): + capacity = 10 + self.terminus.create_pool_v1(capacity, True, True, {"from": accounts[1]}) + pool_id = self.terminus.total_pools() + with self.assertRaises(Exception): + self.terminus.mint_batch( + accounts[2].address, + pool_i_ds=[pool_id, pool_id], + amounts=[int(capacity / 2) + 1, int(capacity / 2) + 1], + data=b"", + transaction_config={"from": accounts[1]}, + ) + + balance = self.terminus.balance_of(accounts[2].address, pool_id) + self.assertEqual(balance, 0) + + supply = self.terminus.terminus_pool_supply(pool_id) + self.assertEqual(supply, 0) + + def test_mint_batch_fails_if_it_exceeds_capacity_one_at_a_time(self): + capacity = 10 + self.terminus.create_pool_v1(capacity, True, True, {"from": accounts[1]}) + pool_id = self.terminus.total_pools() + with self.assertRaises(Exception): + self.terminus.mint_batch( + accounts[2].address, + pool_i_ds=[pool_id for _ in range(capacity + 1)], + amounts=[1 for _ in range(capacity + 1)], + data=b"", + transaction_config={"from": accounts[1]}, + ) + + balance = self.terminus.balance_of(accounts[2].address, pool_id) + self.assertEqual(balance, 0) + + supply = self.terminus.terminus_pool_supply(pool_id) + self.assertEqual(supply, 0) + + def test_pool_mint_batch(self): + pool_id = self.terminus.total_pools() + target_accounts = [account.address for account in accounts[:5]] + target_amounts = [1 for _ in accounts[:5]] + num_accounts = len(accounts[:5]) + initial_pool_supply = self.terminus.terminus_pool_supply(pool_id) + initial_balances: List[int] = [] + for account in accounts[:5]: + initial_balances.append(self.terminus.balance_of(account.address, pool_id)) + self.terminus.pool_mint_batch( + pool_id, target_accounts, target_amounts, {"from": accounts[1]} + ) + final_pool_supply = self.terminus.terminus_pool_supply(pool_id) + self.assertEqual(final_pool_supply, initial_pool_supply + num_accounts) + for i, account in enumerate(accounts[:5]): + final_balance = self.terminus.balance_of(account.address, pool_id) + self.assertEqual(final_balance, initial_balances[i] + 1) + + def test_pool_mint_batch_as_contract_controller_not_pool_controller(self): + pool_id = self.terminus.total_pools() + target_accounts = [account.address for account in accounts[:5]] + target_amounts = [1 for _ in accounts[:5]] + initial_pool_supply = self.terminus.terminus_pool_supply(pool_id) + initial_balances: List[int] = [] + for account in accounts[:5]: + initial_balances.append(self.terminus.balance_of(account.address, pool_id)) + with self.assertRaises(Exception): + self.terminus.pool_mint_batch( + pool_id, target_accounts, target_amounts, {"from": accounts[0]} + ) + final_pool_supply = self.terminus.terminus_pool_supply(pool_id) + self.assertEqual(final_pool_supply, initial_pool_supply) + for i, account in enumerate(accounts[:5]): + final_balance = self.terminus.balance_of(account.address, pool_id) + self.assertEqual(final_balance, initial_balances[i]) + + def test_pool_mint_batch_as_unauthorized_third_party(self): + pool_id = self.terminus.total_pools() + target_accounts = [account.address for account in accounts[:5]] + target_amounts = [1 for _ in accounts[:5]] + initial_pool_supply = self.terminus.terminus_pool_supply(pool_id) + initial_balances: List[int] = [] + for account in accounts[:5]: + initial_balances.append(self.terminus.balance_of(account.address, pool_id)) + with self.assertRaises(Exception): + self.terminus.pool_mint_batch( + pool_id, target_accounts, target_amounts, {"from": accounts[2]} + ) + final_pool_supply = self.terminus.terminus_pool_supply(pool_id) + self.assertEqual(final_pool_supply, initial_pool_supply) + for i, account in enumerate(accounts[:5]): + final_balance = self.terminus.balance_of(account.address, pool_id) + self.assertEqual(final_balance, initial_balances[i]) + + def test_pool_mint_with_pool_approval(self): + self.terminus.create_pool_v1(10, False, False, {"from": accounts[1]}) + pool_id = self.terminus.total_pools() + + self.assertFalse( + self.terminus.is_approved_for_pool(pool_id, accounts[2].address) + ) + with self.assertRaises(Exception): + self.terminus.mint( + accounts[2].address, pool_id, 1, b"", {"from": accounts[2]} + ) + + self.terminus.approve_for_pool( + pool_id, accounts[2].address, {"from": accounts[1]} + ) + supply_0 = self.terminus.terminus_pool_supply(pool_id) + balance_0 = self.terminus.balance_of(accounts[2].address, pool_id) + self.terminus.mint(accounts[2].address, pool_id, 1, b"", {"from": accounts[1]}) + balance_1 = self.terminus.balance_of(accounts[2].address, pool_id) + supply_1 = self.terminus.terminus_pool_supply(pool_id) + + self.assertEqual(balance_1, balance_0 + 1) + self.assertEqual(supply_0 + 1, supply_1) + + def test_pool_mint_batch_with_approval(self): + pool_id = self.terminus.total_pools() + target_accounts = [account.address for account in accounts[:5]] + target_amounts = [1 for _ in accounts[:5]] + num_accounts = len(accounts[:5]) + initial_pool_supply = self.terminus.terminus_pool_supply(pool_id) + initial_balances: List[int] = [] + for account in accounts[:5]: + initial_balances.append(self.terminus.balance_of(account.address, pool_id)) + + self.assertFalse( + self.terminus.is_approved_for_pool(pool_id, accounts[2].address) + ) + with self.assertRaises(Exception): + self.terminus.pool_mint_batch( + pool_id, target_accounts, target_amounts, {"from": accounts[2]} + ) + self.terminus.approve_for_pool( + pool_id, accounts[2].address, {"from": accounts[1]} + ) + self.terminus.pool_mint_batch( + pool_id, target_accounts, target_amounts, {"from": accounts[2]} + ) + + final_pool_supply = self.terminus.terminus_pool_supply(pool_id) + self.assertEqual(final_pool_supply, initial_pool_supply + num_accounts) + for i, account in enumerate(accounts[:5]): + final_balance = self.terminus.balance_of(account.address, pool_id) + self.assertEqual(final_balance, initial_balances[i] + 1) + + def test_transfer(self): + pool_id = self.terminus.total_pools() + self.terminus.mint(accounts[2], pool_id, 1, b"", {"from": accounts[1]}) + + initial_sender_balance = self.terminus.balance_of(accounts[2].address, pool_id) + initial_receiver_balance = self.terminus.balance_of( + accounts[3].address, pool_id + ) + + self.terminus.safe_transfer_from( + accounts[2].address, + accounts[3].address, + pool_id, + 1, + b"", + {"from": accounts[2]}, + ) + + final_sender_balance = self.terminus.balance_of(accounts[2].address, pool_id) + final_receiver_balance = self.terminus.balance_of(accounts[3].address, pool_id) + + self.assertEqual(final_sender_balance, initial_sender_balance - 1) + self.assertEqual(final_receiver_balance, initial_receiver_balance + 1) + + def test_transfer_as_pool_controller(self): + pool_id = self.terminus.total_pools() + self.terminus.mint(accounts[2], pool_id, 1, b"", {"from": accounts[1]}) + + initial_sender_balance = self.terminus.balance_of(accounts[2].address, pool_id) + initial_receiver_balance = self.terminus.balance_of( + accounts[3].address, pool_id + ) + + self.terminus.safe_transfer_from( + accounts[2].address, + accounts[3].address, + pool_id, + 1, + b"", + {"from": accounts[1]}, + ) + + final_sender_balance = self.terminus.balance_of(accounts[2].address, pool_id) + final_receiver_balance = self.terminus.balance_of(accounts[3].address, pool_id) + + self.assertEqual(final_sender_balance, initial_sender_balance - 1) + self.assertEqual(final_receiver_balance, initial_receiver_balance + 1) + + def test_transfer_fails_as_controller_of_another_pool_with_no_approval(self): + """ + Hacken.io auditors claimed that an address that controlled *some* pool could effect transfers + on *any* pool. + + This test shows that this is not true. + + Exercises: + - safeTransferFrom + """ + # Ensure that accounts[1] is pool controller of *some* pool. + self.terminus.create_pool_v1(100, True, True, {"from": accounts[1]}) + controlled_pool_id = self.terminus.total_pools() + self.assertEqual( + self.terminus.terminus_pool_controller(controlled_pool_id), + accounts[1].address, + ) + + self.terminus.create_pool_v1(100, True, True, {"from": accounts[1]}) + pool_id = self.terminus.total_pools() + self.terminus.mint(accounts[2], pool_id, 1, b"", {"from": accounts[1]}) + + # Remove pool control from accounts[1] + self.terminus.set_pool_controller( + pool_id, accounts[4].address, {"from": accounts[1]} + ) + + initial_sender_balance = self.terminus.balance_of(accounts[2].address, pool_id) + initial_receiver_balance = self.terminus.balance_of( + accounts[3].address, pool_id + ) + + with self.assertRaises(VirtualMachineError): + self.terminus.safe_transfer_from( + accounts[2].address, + accounts[3].address, + pool_id, + 1, + b"", + {"from": accounts[1]}, + ) + + final_sender_balance = self.terminus.balance_of(accounts[2].address, pool_id) + final_receiver_balance = self.terminus.balance_of(accounts[3].address, pool_id) + + self.assertEqual(final_sender_balance, initial_sender_balance) + self.assertEqual(final_receiver_balance, initial_receiver_balance) + + def test_transfer_fails_as_terminus_controller_with_no_approval(self): + """ + Tests that neither Terminus controller *nor* pool controller can transfer a token on behalf of + another address without explicit approval (using safeTransferFrom). + + Exercises: + - safeTransferFrom + """ + self.assertEqual(self.terminus.terminus_controller(), accounts[1].address) + self.terminus.create_pool_v1(100, True, True, {"from": accounts[1]}) + pool_id = self.terminus.total_pools() + self.terminus.mint(accounts[2], pool_id, 1, b"", {"from": accounts[1]}) + + # Remove pool control from accounts[1] + self.terminus.set_pool_controller( + pool_id, accounts[4].address, {"from": accounts[1]} + ) + + initial_sender_balance = self.terminus.balance_of(accounts[2].address, pool_id) + initial_receiver_balance = self.terminus.balance_of( + accounts[3].address, pool_id + ) + + with self.assertRaises(VirtualMachineError): + self.terminus.safe_transfer_from( + accounts[2].address, + accounts[3].address, + pool_id, + 1, + b"", + {"from": accounts[1]}, + ) + + final_sender_balance = self.terminus.balance_of(accounts[2].address, pool_id) + final_receiver_balance = self.terminus.balance_of(accounts[3].address, pool_id) + + self.assertEqual(final_sender_balance, initial_sender_balance) + self.assertEqual(final_receiver_balance, initial_receiver_balance) + + def test_transfer_as_unauthorized_recipient(self): + self.terminus.create_pool_v1(2**256 - 1, True, True, {"from": accounts[1]}) + pool_id = self.terminus.total_pools() + self.terminus.mint(accounts[2], pool_id, 1, b"", {"from": accounts[1]}) + + initial_sender_balance = self.terminus.balance_of(accounts[2].address, pool_id) + initial_receiver_balance = self.terminus.balance_of( + accounts[3].address, pool_id + ) + + with self.assertRaises(Exception): + self.terminus.safe_transfer_from( + accounts[2].address, + accounts[3].address, + pool_id, + 1, + b"", + {"from": accounts[3]}, + ) + + final_sender_balance = self.terminus.balance_of(accounts[2].address, pool_id) + final_receiver_balance = self.terminus.balance_of(accounts[3].address, pool_id) + + self.assertEqual(final_sender_balance, initial_sender_balance) + self.assertEqual(final_receiver_balance, initial_receiver_balance) + + def test_transfer_as_authorized_recipient(self): + pool_id = self.terminus.total_pools() + self.terminus.mint(accounts[2], pool_id, 1, b"", {"from": accounts[1]}) + + initial_sender_balance = self.terminus.balance_of(accounts[2].address, pool_id) + initial_receiver_balance = self.terminus.balance_of( + accounts[3].address, pool_id + ) + + self.terminus.approve_for_pool( + pool_id, accounts[3].address, {"from": accounts[1]} + ) + self.terminus.safe_transfer_from( + accounts[2].address, + accounts[3].address, + pool_id, + 1, + b"", + {"from": accounts[3]}, + ) + + final_sender_balance = self.terminus.balance_of(accounts[2].address, pool_id) + final_receiver_balance = self.terminus.balance_of(accounts[3].address, pool_id) + + self.assertEqual(final_sender_balance, initial_sender_balance - 1) + self.assertEqual(final_receiver_balance, initial_receiver_balance + 1) + + def test_transfer_as_approved_operator(self): + pool_id = self.terminus.total_pools() + self.terminus.mint(accounts[2], pool_id, 1, b"", {"from": accounts[1]}) + + initial_sender_balance = self.terminus.balance_of(accounts[2].address, pool_id) + initial_receiver_balance = self.terminus.balance_of( + accounts[3].address, pool_id + ) + + # It is very important to revoke this approval from accounts[3] as accounts[3] tries to initiate + # transfers from accounts[2] in other tests. + self.terminus.set_approval_for_all( + accounts[3].address, True, {"from": accounts[2]} + ) + + try: + self.terminus.safe_transfer_from( + accounts[2].address, + accounts[3].address, + pool_id, + 1, + b"", + {"from": accounts[3]}, + ) + finally: + self.terminus.set_approval_for_all( + accounts[3].address, False, {"from": accounts[2]} + ) + + final_sender_balance = self.terminus.balance_of(accounts[2].address, pool_id) + final_receiver_balance = self.terminus.balance_of(accounts[3].address, pool_id) + + self.assertEqual(final_sender_balance, initial_sender_balance - 1) + self.assertEqual(final_receiver_balance, initial_receiver_balance + 1) + + # Check that accounts[3] is no longer approved for all pools by accounts[2] (for other tests to) + # run correctly. + self.assertFalse( + self.terminus.is_approved_for_all(accounts[2].address, accounts[3].address) + ) + + def test_transfer_as_unauthorized_unrelated_party(self): + pool_id = self.terminus.total_pools() + self.terminus.mint(accounts[2], pool_id, 1, b"", {"from": accounts[1]}) + + initial_sender_balance = self.terminus.balance_of(accounts[2].address, pool_id) + initial_receiver_balance = self.terminus.balance_of( + accounts[3].address, pool_id + ) + + with self.assertRaises(Exception): + self.terminus.safe_transfer_from( + accounts[2].address, + accounts[3].address, + pool_id, + 1, + b"", + {"from": accounts[4]}, + ) + + final_sender_balance = self.terminus.balance_of(accounts[2].address, pool_id) + final_receiver_balance = self.terminus.balance_of(accounts[3].address, pool_id) + + self.assertEqual(final_sender_balance, initial_sender_balance) + self.assertEqual(final_receiver_balance, initial_receiver_balance) + + def test_transfer_as_authorized_unrelated_party(self): + pool_id = self.terminus.total_pools() + self.terminus.mint(accounts[2], pool_id, 1, b"", {"from": accounts[1]}) + + initial_sender_balance = self.terminus.balance_of(accounts[2].address, pool_id) + initial_receiver_balance = self.terminus.balance_of( + accounts[3].address, pool_id + ) + + self.terminus.approve_for_pool( + pool_id, accounts[4].address, {"from": accounts[1]} + ) + self.terminus.safe_transfer_from( + accounts[2].address, + accounts[3].address, + pool_id, + 1, + b"", + {"from": accounts[4]}, + ) + + final_sender_balance = self.terminus.balance_of(accounts[2].address, pool_id) + final_receiver_balance = self.terminus.balance_of(accounts[3].address, pool_id) + + self.assertEqual(final_sender_balance, initial_sender_balance - 1) + self.assertEqual(final_receiver_balance, initial_receiver_balance + 1) + + def test_burn_fails_as_token_owner(self): + pool_id = self.terminus.total_pools() + self.terminus.mint(accounts[2], pool_id, 1, b"", {"from": accounts[1]}) + + initial_pool_supply = self.terminus.terminus_pool_supply(pool_id) + initial_owner_balance = self.terminus.balance_of(accounts[2].address, pool_id) + with self.assertRaises(Exception): + self.terminus.burn(accounts[2].address, pool_id, 1, {"from": accounts[2]}) + + final_pool_supply = self.terminus.terminus_pool_supply(pool_id) + final_owner_balance = self.terminus.balance_of(accounts[2].address, pool_id) + self.assertEqual(final_pool_supply, initial_pool_supply) + self.assertEqual(final_owner_balance, initial_owner_balance) + + def test_burn_fails_as_pool_controller(self): + pool_id = self.terminus.total_pools() + self.terminus.mint(accounts[2], pool_id, 1, b"", {"from": accounts[1]}) + + initial_pool_supply = self.terminus.terminus_pool_supply(pool_id) + initial_owner_balance = self.terminus.balance_of(accounts[2].address, pool_id) + with self.assertRaises(Exception): + self.terminus.burn(accounts[2].address, pool_id, 1, {"from": accounts[1]}) + + final_pool_supply = self.terminus.terminus_pool_supply(pool_id) + final_owner_balance = self.terminus.balance_of(accounts[2].address, pool_id) + self.assertEqual(final_pool_supply, initial_pool_supply) + self.assertEqual(final_owner_balance, initial_owner_balance) + + def test_burn_fails_as_third_party(self): + pool_id = self.terminus.total_pools() + self.terminus.mint(accounts[2], pool_id, 1, b"", {"from": accounts[1]}) + + initial_pool_supply = self.terminus.terminus_pool_supply(pool_id) + initial_owner_balance = self.terminus.balance_of(accounts[2].address, pool_id) + with self.assertRaises(Exception): + self.terminus.burn(accounts[2].address, pool_id, 1, {"from": accounts[3]}) + + final_pool_supply = self.terminus.terminus_pool_supply(pool_id) + final_owner_balance = self.terminus.balance_of(accounts[2].address, pool_id) + self.assertEqual(final_pool_supply, initial_pool_supply) + self.assertEqual(final_owner_balance, initial_owner_balance) + + def test_burn_fails_as_authorized_third_party(self): + pool_id = self.terminus.total_pools() + self.terminus.mint(accounts[2], pool_id, 1, b"", {"from": accounts[1]}) + + initial_pool_supply = self.terminus.terminus_pool_supply(pool_id) + initial_owner_balance = self.terminus.balance_of(accounts[2].address, pool_id) + self.terminus.approve_for_pool( + pool_id, accounts[3].address, {"from": accounts[1]} + ) + with self.assertRaises(Exception): + self.terminus.burn(accounts[2].address, pool_id, 1, {"from": accounts[3]}) + + final_pool_supply = self.terminus.terminus_pool_supply(pool_id) + final_owner_balance = self.terminus.balance_of(accounts[2].address, pool_id) + self.assertEqual(final_pool_supply, initial_pool_supply) + self.assertEqual(final_owner_balance, initial_owner_balance) + + def test_pool_approval(self): + controller = accounts[1] + operator = accounts[2] + user = accounts[3] + + # TODO(zomglings): We should test the Terminus controller permissions on the same contract. + # Currently, controller is both pool controller AND Terminus controller. In a more proper test, + # these would be different accounts. + + # TODO(zomglings): Tested manually that changing burnable below from True to False results in + # the right reversion when we try to burn these tokens on-chain. This should be a separate + # test case that runs *automatically*. + self.terminus.create_pool_v1(100, True, True, {"from": controller}) + pool_id = self.terminus.total_pools() + self.terminus.mint(controller.address, pool_id, 5, "", {"from": controller}) + self.terminus.mint(operator.address, pool_id, 5, "", {"from": controller}) + self.terminus.mint(user.address, pool_id, 5, "", {"from": controller}) + + controller_balance_0 = self.terminus.balance_of(controller.address, pool_id) + operator_balance_0 = self.terminus.balance_of(operator.address, pool_id) + user_balance_0 = self.terminus.balance_of(user.address, pool_id) + + self.assertFalse(self.terminus.is_approved_for_pool(pool_id, operator.address)) + + with self.assertRaises(VirtualMachineError): + self.terminus.mint(controller.address, pool_id, 1, "", {"from": operator}) + + with self.assertRaises(VirtualMachineError): + self.terminus.mint(operator.address, pool_id, 1, "", {"from": operator}) + + with self.assertRaises(VirtualMachineError): + self.terminus.mint(user.address, pool_id, 1, "", {"from": operator}) + + controller_balance_1 = self.terminus.balance_of(controller.address, pool_id) + operator_balance_1 = self.terminus.balance_of(operator.address, pool_id) + user_balance_1 = self.terminus.balance_of(user.address, pool_id) + + self.assertEqual(controller_balance_1, controller_balance_0) + self.assertEqual(operator_balance_1, operator_balance_0) + self.assertEqual(user_balance_1, user_balance_0) + + with self.assertRaises(VirtualMachineError): + self.terminus.burn(controller.address, pool_id, 1, {"from": operator}) + + self.terminus.burn(operator.address, pool_id, 1, {"from": operator}) + + with self.assertRaises(VirtualMachineError): + self.terminus.burn(user.address, pool_id, 1, {"from": operator}) + + controller_balance_2 = self.terminus.balance_of(controller.address, pool_id) + operator_balance_2 = self.terminus.balance_of(operator.address, pool_id) + user_balance_2 = self.terminus.balance_of(user.address, pool_id) + + self.assertEqual(controller_balance_2, controller_balance_1) + self.assertEqual(operator_balance_2, operator_balance_1 - 1) + self.assertEqual(user_balance_2, user_balance_1) + + with self.assertRaises(VirtualMachineError): + self.terminus.approve_for_pool(pool_id, operator, {"from": operator}) + + self.terminus.approve_for_pool(pool_id, operator, {"from": accounts[1]}) + + self.assertTrue(self.terminus.is_approved_for_pool(pool_id, operator.address)) + + self.terminus.mint(controller.address, pool_id, 1, "", {"from": operator}) + self.terminus.mint(operator.address, pool_id, 1, "", {"from": operator}) + self.terminus.mint(user.address, pool_id, 1, "", {"from": operator}) + + controller_balance_3 = self.terminus.balance_of(controller.address, pool_id) + operator_balance_3 = self.terminus.balance_of(operator.address, pool_id) + user_balance_3 = self.terminus.balance_of(user.address, pool_id) + + self.assertEqual(controller_balance_3, controller_balance_2 + 1) + self.assertEqual(operator_balance_3, operator_balance_2 + 1) + self.assertEqual(user_balance_3, user_balance_2 + 1) + + self.terminus.burn(controller.address, pool_id, 1, {"from": operator}) + self.terminus.burn(operator.address, pool_id, 1, {"from": operator}) + self.terminus.burn(user.address, pool_id, 1, {"from": operator}) + + controller_balance_4 = self.terminus.balance_of(controller.address, pool_id) + operator_balance_4 = self.terminus.balance_of(operator.address, pool_id) + user_balance_4 = self.terminus.balance_of(user.address, pool_id) + + self.assertEqual(controller_balance_4, controller_balance_3 - 1) + self.assertEqual(operator_balance_4, operator_balance_3 - 1) + self.assertEqual(user_balance_4, user_balance_3 - 1) + + with self.assertRaises(VirtualMachineError): + self.terminus.unapprove_for_pool(pool_id, operator, {"from": operator}) + + self.assertTrue(self.terminus.is_approved_for_pool(pool_id, operator.address)) + + self.terminus.unapprove_for_pool(pool_id, operator, {"from": controller}) + + self.assertFalse(self.terminus.is_approved_for_pool(pool_id, operator.address)) + + with self.assertRaises(VirtualMachineError): + self.terminus.mint(controller.address, pool_id, 1, "", {"from": operator}) + + with self.assertRaises(VirtualMachineError): + self.terminus.mint(operator.address, pool_id, 1, "", {"from": operator}) + + with self.assertRaises(VirtualMachineError): + self.terminus.mint(user.address, pool_id, 1, "", {"from": operator}) + + controller_balance_5 = self.terminus.balance_of(controller.address, pool_id) + operator_balance_5 = self.terminus.balance_of(operator.address, pool_id) + user_balance_5 = self.terminus.balance_of(user.address, pool_id) + + self.assertEqual(controller_balance_5, controller_balance_4) + self.assertEqual(operator_balance_5, operator_balance_4) + self.assertEqual(user_balance_5, user_balance_4) + + with self.assertRaises(VirtualMachineError): + self.terminus.burn(controller.address, pool_id, 1, {"from": operator}) + + self.terminus.burn(operator.address, pool_id, 1, {"from": operator}) + + with self.assertRaises(VirtualMachineError): + self.terminus.burn(user.address, pool_id, 1, {"from": operator}) + + controller_balance_6 = self.terminus.balance_of(controller.address, pool_id) + operator_balance_6 = self.terminus.balance_of(operator.address, pool_id) + user_balance_6 = self.terminus.balance_of(user.address, pool_id) + + self.assertEqual(controller_balance_6, controller_balance_5) + self.assertEqual(operator_balance_6, operator_balance_5 - 1) + self.assertEqual(user_balance_6, user_balance_5) + + +class TestPoolCreation(TestPoolOperations): + def setUp(self): + self.terminus.create_pool_v1(10, True, False, {"from": accounts[1]}) + + def test_nontransferable_pool(self): + self.terminus.create_pool_v1(10, False, False, {"from": accounts[1]}) + pool_id = self.terminus.total_pools() + self.terminus.mint(accounts[2], pool_id, 1, b"", {"from": accounts[1]}) + + initial_sender_balance = self.terminus.balance_of(accounts[2].address, pool_id) + initial_receiver_balance = self.terminus.balance_of( + accounts[3].address, pool_id + ) + + with self.assertRaises(Exception): + self.terminus.safe_transfer_from( + accounts[2].address, + accounts[3].address, + pool_id, + 1, + b"", + {"from": accounts[2]}, + ) + + final_sender_balance = self.terminus.balance_of(accounts[2].address, pool_id) + final_receiver_balance = self.terminus.balance_of(accounts[3].address, pool_id) + + self.assertEqual(final_sender_balance, initial_sender_balance) + self.assertEqual(final_receiver_balance, initial_receiver_balance) + + def test_pool_state_view_methods(self): + nontransferable_nonburnable_pool_uri = "https://example.com/ff.json" + self.terminus.create_pool_v2( + 10, + False, + False, + nontransferable_nonburnable_pool_uri, + {"from": accounts[1]}, + ) + nontransferable_nonburnable_pool_id = self.terminus.total_pools() + self.assertFalse( + self.terminus.pool_is_transferable(nontransferable_nonburnable_pool_id) + ) + self.assertFalse( + self.terminus.pool_is_burnable(nontransferable_nonburnable_pool_id) + ) + self.assertEqual( + self.terminus.uri(nontransferable_nonburnable_pool_id), + nontransferable_nonburnable_pool_uri, + ) + + transferable_nonburnable_pool_uri = "https://example.com/tf.json" + self.terminus.create_pool_v2( + 10, True, False, transferable_nonburnable_pool_uri, {"from": accounts[1]} + ) + transferable_nonburnable_pool_id = self.terminus.total_pools() + self.assertTrue( + self.terminus.pool_is_transferable(transferable_nonburnable_pool_id) + ) + self.assertFalse( + self.terminus.pool_is_burnable(transferable_nonburnable_pool_id) + ) + self.assertEqual( + self.terminus.uri(transferable_nonburnable_pool_id), + transferable_nonburnable_pool_uri, + ) + + transferable_burnable_pool_uri = "https://example.com/tt.json" + self.terminus.create_pool_v2( + 10, True, True, transferable_burnable_pool_uri, {"from": accounts[1]} + ) + transferable_burnable_pool_id = self.terminus.total_pools() + self.assertTrue( + self.terminus.pool_is_transferable(transferable_burnable_pool_id) + ) + self.assertTrue(self.terminus.pool_is_burnable(transferable_burnable_pool_id)) + self.assertEqual( + self.terminus.uri(transferable_burnable_pool_id), + transferable_burnable_pool_uri, + ) + + nontransferable_burnable_pool_uri = "https://example.com/ft.json" + self.terminus.create_pool_v2( + 10, False, True, nontransferable_burnable_pool_uri, {"from": accounts[1]} + ) + nontransferable_burnable_pool_id = self.terminus.total_pools() + self.assertFalse( + self.terminus.pool_is_transferable(nontransferable_burnable_pool_id) + ) + self.assertTrue( + self.terminus.pool_is_burnable(nontransferable_burnable_pool_id) + ) + self.assertEqual( + self.terminus.uri(nontransferable_burnable_pool_id), + nontransferable_burnable_pool_uri, + ) + + def test_pool_state_setters(self): + self.terminus.create_pool_v1(10, False, False, {"from": accounts[1]}) + pool_id = self.terminus.total_pools() + self.assertEqual( + self.terminus.terminus_pool_controller(pool_id), accounts[1].address + ) + + self.assertFalse(self.terminus.pool_is_transferable(pool_id)) + self.assertFalse(self.terminus.pool_is_burnable(pool_id)) + + self.terminus.set_pool_transferable(pool_id, True, {"from": accounts[1]}) + self.assertTrue(self.terminus.pool_is_transferable(pool_id)) + self.assertFalse(self.terminus.pool_is_burnable(pool_id)) + + self.terminus.set_pool_burnable(pool_id, True, {"from": accounts[1]}) + self.assertTrue(self.terminus.pool_is_transferable(pool_id)) + self.assertTrue(self.terminus.pool_is_burnable(pool_id)) + + self.terminus.set_pool_transferable(pool_id, False, {"from": accounts[1]}) + self.assertFalse(self.terminus.pool_is_transferable(pool_id)) + self.assertTrue(self.terminus.pool_is_burnable(pool_id)) + + self.terminus.set_pool_burnable(pool_id, False, {"from": accounts[1]}) + self.assertFalse(self.terminus.pool_is_transferable(pool_id)) + self.assertFalse(self.terminus.pool_is_burnable(pool_id)) + + def test_pool_state_setters_do_not_allow_noncontroller_to_set_parameters(self): + self.terminus.create_pool_v1(10, False, False, {"from": accounts[1]}) + pool_id = self.terminus.total_pools() + self.assertEqual( + self.terminus.terminus_pool_controller(pool_id), accounts[1].address + ) + + self.assertFalse(self.terminus.pool_is_transferable(pool_id)) + self.assertFalse(self.terminus.pool_is_burnable(pool_id)) + + with self.assertRaises(VirtualMachineError): + self.terminus.set_pool_transferable(pool_id, True, {"from": accounts[2]}) + + with self.assertRaises(VirtualMachineError): + self.terminus.set_pool_burnable(pool_id, True, {"from": accounts[2]}) + + self.assertFalse(self.terminus.pool_is_transferable(pool_id)) + self.assertFalse(self.terminus.pool_is_burnable(pool_id)) + + def test_burnable_pool_burn_as_token_owner(self): + self.terminus.create_pool_v1(10, True, True, {"from": accounts[1]}) + pool_id = self.terminus.total_pools() + self.terminus.mint(accounts[2], pool_id, 1, b"", {"from": accounts[1]}) + + initial_pool_supply = self.terminus.terminus_pool_supply(pool_id) + initial_owner_balance = self.terminus.balance_of(accounts[2].address, pool_id) + self.terminus.burn(accounts[2].address, pool_id, 1, {"from": accounts[2]}) + + final_pool_supply = self.terminus.terminus_pool_supply(pool_id) + final_owner_balance = self.terminus.balance_of(accounts[2].address, pool_id) + self.assertEqual(final_pool_supply, initial_pool_supply - 1) + self.assertEqual(final_owner_balance, initial_owner_balance - 1) + + def test_burnable_pool_burn_as_pool_controller(self): + self.terminus.create_pool_v1(10, True, True, {"from": accounts[1]}) + pool_id = self.terminus.total_pools() + self.terminus.mint(accounts[2], pool_id, 1, b"", {"from": accounts[1]}) + + initial_pool_supply = self.terminus.terminus_pool_supply(pool_id) + initial_owner_balance = self.terminus.balance_of(accounts[2].address, pool_id) + self.terminus.burn(accounts[2].address, pool_id, 1, {"from": accounts[1]}) + + final_pool_supply = self.terminus.terminus_pool_supply(pool_id) + final_owner_balance = self.terminus.balance_of(accounts[2].address, pool_id) + self.assertEqual(final_pool_supply, initial_pool_supply - 1) + self.assertEqual(final_owner_balance, initial_owner_balance - 1) + + def test_burnable_pool_burn_as_authorized_third_party(self): + self.terminus.create_pool_v1(10, True, True, {"from": accounts[1]}) + pool_id = self.terminus.total_pools() + self.terminus.mint(accounts[2], pool_id, 1, b"", {"from": accounts[1]}) + + initial_pool_supply = self.terminus.terminus_pool_supply(pool_id) + initial_owner_balance = self.terminus.balance_of(accounts[2].address, pool_id) + self.terminus.approve_for_pool( + pool_id, accounts[3].address, {"from": accounts[1]} + ) + self.terminus.burn(accounts[2].address, pool_id, 1, {"from": accounts[3]}) + + final_pool_supply = self.terminus.terminus_pool_supply(pool_id) + final_owner_balance = self.terminus.balance_of(accounts[2].address, pool_id) + self.assertEqual(final_pool_supply, initial_pool_supply - 1) + self.assertEqual(final_owner_balance, initial_owner_balance - 1) + + def test_burnable_pool_burn_as_unauthorized_third_party(self): + self.terminus.create_pool_v1(10, True, True, {"from": accounts[1]}) + pool_id = self.terminus.total_pools() + self.terminus.mint(accounts[2], pool_id, 1, b"", {"from": accounts[1]}) + + initial_pool_supply = self.terminus.terminus_pool_supply(pool_id) + initial_owner_balance = self.terminus.balance_of(accounts[2].address, pool_id) + with self.assertRaises(Exception): + self.terminus.burn(accounts[2].address, pool_id, 1, {"from": accounts[3]}) + + final_pool_supply = self.terminus.terminus_pool_supply(pool_id) + final_owner_balance = self.terminus.balance_of(accounts[2].address, pool_id) + self.assertEqual(final_pool_supply, initial_pool_supply) + self.assertEqual(final_owner_balance, initial_owner_balance) + + def test_nontransferable_pool_safe_transfer_from(self): + self.terminus.create_pool_v1(10, False, False, {"from": accounts[1]}) + pool_id = self.terminus.total_pools() + self.terminus.mint(accounts[2], pool_id, 1, b"", {"from": accounts[1]}) + + initial_pool_supply = self.terminus.terminus_pool_supply(pool_id) + initial_owner_balance = self.terminus.balance_of(accounts[2].address, pool_id) + with self.assertRaises(Exception): + self.terminus.safe_transfer_from( + accounts[2].address, + accounts[3].address, + pool_id, + 1, + b"", + {"from": accounts[2]}, + ) + + final_pool_supply = self.terminus.terminus_pool_supply(pool_id) + final_owner_balance = self.terminus.balance_of(accounts[2].address, pool_id) + self.assertEqual(final_pool_supply, initial_pool_supply) + self.assertEqual(final_owner_balance, initial_owner_balance) + + def test_nontransferable_pool_safe_batch_transfer_from(self): + self.terminus.create_pool_v1(10, False, False, {"from": accounts[1]}) + pool_id = self.terminus.total_pools() + self.terminus.mint(accounts[2], pool_id, 1, b"", {"from": accounts[1]}) + + initial_pool_supply = self.terminus.terminus_pool_supply(pool_id) + initial_owner_balance = self.terminus.balance_of(accounts[2].address, pool_id) + with self.assertRaises(Exception): + self.terminus.safe_batch_transfer_from( + accounts[2].address, + accounts[3].address, + [pool_id], + [1], + b"", + {"from": accounts[2]}, + ) + + final_pool_supply = self.terminus.terminus_pool_supply(pool_id) + final_owner_balance = self.terminus.balance_of(accounts[2].address, pool_id) + self.assertEqual(final_pool_supply, initial_pool_supply) + self.assertEqual(final_owner_balance, initial_owner_balance) + + +if __name__ == "__main__": + unittest.main() diff --git a/contracts/ControllableWithTerminus.sol b/contracts/ControllableWithTerminus.sol index 07d6229a..11ae261f 100644 --- a/contracts/ControllableWithTerminus.sol +++ b/contracts/ControllableWithTerminus.sol @@ -6,15 +6,17 @@ */ pragma solidity ^0.8.9; -import "@moonstream/contracts/terminus/TerminusFacet.sol"; +import {TerminusFacet} from "./terminus/TerminusFacet.sol"; import "@openzeppelin-contracts/contracts/access/Ownable.sol"; abstract contract ControllableWithTerminus is Ownable { TerminusFacet private terminus; uint256 public administratorPoolId; - constructor(address _terminusContractAddress, uint256 _administratorPoolId) - { + constructor( + address _terminusContractAddress, + uint256 _administratorPoolId + ) { terminus = TerminusFacet(_terminusContractAddress); administratorPoolId = _administratorPoolId; } @@ -32,10 +34,9 @@ abstract contract ControllableWithTerminus is Ownable { _; } - function changeAdministratorPoolId(uint256 _administratorPoolId) - public - onlyOwner - { + function changeAdministratorPoolId( + uint256 _administratorPoolId + ) public onlyOwner { administratorPoolId = _administratorPoolId; } diff --git a/contracts/Dropper.sol b/contracts/Dropper.sol index 8edac9d0..9f0fa3a1 100644 --- a/contracts/Dropper.sol +++ b/contracts/Dropper.sol @@ -7,7 +7,6 @@ pragma solidity ^0.8.9; -import "@moonstream/contracts/terminus/TerminusFacet.sol"; import "@openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin-contracts/contracts/token/ERC721/IERC721.sol"; import "@openzeppelin-contracts/contracts/token/ERC1155/IERC1155.sol"; @@ -19,6 +18,8 @@ import "@openzeppelin-contracts/contracts/token/ERC1155/utils/ERC1155Holder.sol" import "@openzeppelin-contracts/contracts/utils/cryptography/draft-EIP712.sol"; import "@openzeppelin-contracts/contracts/utils/cryptography/SignatureChecker.sol"; +import {TerminusFacet} from "./terminus/TerminusFacet.sol"; + /** * @title Moonstream Dropper * @author Moonstream Engineering (engineering@moonstream.to) @@ -143,11 +144,9 @@ contract Dropper is return NumClaims; } - function getClaim(uint256 claimId) - external - view - returns (ClaimableToken memory) - { + function getClaim( + uint256 claimId + ) external view returns (ClaimableToken memory) { return ClaimToken[claimId]; } @@ -160,27 +159,24 @@ contract Dropper is return IsClaimActive[claimId]; } - function setSignerForClaim(uint256 claimId, address signer) - public - onlyOwner - { + function setSignerForClaim( + uint256 claimId, + address signer + ) public onlyOwner { ClaimSigner[claimId] = signer; emit ClaimSignerChanged(claimId, signer); } - function getSignerForClaim(uint256 claimId) - external - view - returns (address) - { + function getSignerForClaim( + uint256 claimId + ) external view returns (address) { return ClaimSigner[claimId]; } - function getClaimStatus(uint256 claimId, address claimant) - external - view - returns (uint256) - { + function getClaimStatus( + uint256 claimId, + address claimant + ) external view returns (uint256) { return ClaimCompleted[claimId][claimant]; } @@ -280,19 +276,19 @@ contract Dropper is emit Claimed(claimId, msg.sender); } - function withdrawERC20(address tokenAddress, uint256 amount) - public - onlyOwner - { + function withdrawERC20( + address tokenAddress, + uint256 amount + ) public onlyOwner { IERC20 erc20Contract = IERC20(tokenAddress); erc20Contract.transfer(_msgSender(), amount); emit Withdrawal(msg.sender, ERC20_TYPE, tokenAddress, 0, amount); } - function withdrawERC721(address tokenAddress, uint256 tokenId) - public - onlyOwner - { + function withdrawERC721( + address tokenAddress, + uint256 tokenId + ) public onlyOwner { address _owner = owner(); IERC721 erc721Contract = IERC721(tokenAddress); erc721Contract.safeTransferFrom(address(this), _owner, tokenId, ""); @@ -335,10 +331,10 @@ contract Dropper is return ClaimURI[claimId]; } - function setClaimUri(uint256 claimId, string memory uri) - external - onlyOwner - { + function setClaimUri( + uint256 claimId, + string memory uri + ) external onlyOwner { ClaimURI[claimId] = uri; } } diff --git a/contracts/Dropper/DropperFacet.sol b/contracts/Dropper/DropperFacet.sol index c088fc5b..c6c846c4 100644 --- a/contracts/Dropper/DropperFacet.sol +++ b/contracts/Dropper/DropperFacet.sol @@ -7,7 +7,6 @@ pragma solidity ^0.8.9; -import {TerminusPermissions} from "@moonstream/contracts/terminus/TerminusPermissions.sol"; import "@openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin-contracts/contracts/token/ERC721/IERC721.sol"; import "@openzeppelin-contracts/contracts/token/ERC1155/IERC1155.sol"; @@ -21,6 +20,7 @@ import "../interfaces/ITerminus.sol"; import "../diamond/security/DiamondReentrancyGuard.sol"; import "../diamond/libraries/LibDiamond.sol"; import "../diamond/libraries/LibSignatures.sol"; +import {TerminusPermissions} from "../terminus/TerminusPermissions.sol"; /** * @title Moonstream Dropper diff --git a/contracts/Lootbox.sol b/contracts/Lootbox.sol index f67f68b0..0ba4b072 100644 --- a/contracts/Lootbox.sol +++ b/contracts/Lootbox.sol @@ -7,7 +7,6 @@ pragma solidity ^0.8.9; -import "@moonstream/contracts/terminus/TerminusFacet.sol"; import "@openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin-contracts/contracts/token/ERC1155/IERC1155.sol"; import "@openzeppelin-contracts/contracts/access/Ownable.sol"; @@ -17,6 +16,7 @@ import "@openzeppelin-contracts/contracts/token/ERC1155/utils/ERC1155Holder.sol" import "./ControllableWithTerminus.sol"; import "./LootboxRandomness.sol"; +import {TerminusFacet} from "./terminus/TerminusFacet.sol"; /** * @title Moonstream Lootbox managing contract diff --git a/contracts/crafting/facets/CraftingFacet.sol b/contracts/crafting/facets/CraftingFacet.sol index 3a961c9d..1d439686 100644 --- a/contracts/crafting/facets/CraftingFacet.sol +++ b/contracts/crafting/facets/CraftingFacet.sol @@ -12,12 +12,12 @@ import "@openzeppelin-contracts/contracts/token/ERC1155/extensions/ERC1155Burnab import "@openzeppelin-contracts/contracts/token/ERC721/extensions/ERC721Burnable.sol"; import "@openzeppelin-contracts/contracts/token/ERC1155/utils/ERC1155Holder.sol"; import "@openzeppelin-contracts/contracts/token/ERC721/utils/ERC721Holder.sol"; -import {TerminusPermissions} from "@moonstream/contracts/terminus/TerminusPermissions.sol"; import {MockTerminus} from "../../mock/MockTerminus.sol"; import {MockErc20} from "../../mock/MockErc20.sol"; import "../libraries/LibCrafting.sol"; import "../../diamond/libraries/LibDiamond.sol"; import "../../diamond/security/DiamondReentrancyGuard.sol"; +import {TerminusPermissions} from "../../terminus/TerminusPermissions.sol"; contract CraftingFacet is ERC1155Holder, @@ -49,9 +49,10 @@ contract CraftingFacet is _; } - function setTerminusAuth(address terminusAddress, uint256 tokenId) - external - { + function setTerminusAuth( + address terminusAddress, + uint256 tokenId + ) external { LibDiamond.enforceIsContractOwner(); LibCrafting.CraftingStorage storage cs = LibCrafting.craftingStorage(); cs.authTerminusAddress = terminusAddress; @@ -66,10 +67,10 @@ contract CraftingFacet is emit RecipeCreated(0, recipe, msg.sender); } - function updateRecipe(uint256 recipeId, Recipe calldata recipe) - external - onlyAuthorizedAddress - { + function updateRecipe( + uint256 recipeId, + Recipe calldata recipe + ) external onlyAuthorizedAddress { LibCrafting.CraftingStorage storage cs = LibCrafting.craftingStorage(); cs.recipes[recipeId] = recipe; emit RecipeUpdated(recipeId, recipe, msg.sender); diff --git a/contracts/inventory/InventoryFacet.sol b/contracts/inventory/InventoryFacet.sol index 39c1ae15..14308f91 100644 --- a/contracts/inventory/InventoryFacet.sol +++ b/contracts/inventory/InventoryFacet.sol @@ -7,7 +7,6 @@ pragma solidity ^0.8.0; -import {TerminusPermissions} from "@moonstream/contracts/terminus/TerminusPermissions.sol"; import "@openzeppelin-contracts/contracts/token/ERC1155/utils/ERC1155Holder.sol"; import "@openzeppelin-contracts/contracts/token/ERC721/utils/ERC721Holder.sol"; import "@openzeppelin-contracts/contracts/token/ERC721/IERC721.sol"; @@ -16,6 +15,7 @@ import "@openzeppelin-contracts/contracts/token/ERC1155/IERC1155.sol"; import "../diamond/libraries/LibDiamond.sol"; import {DiamondReentrancyGuard} from "../diamond/security/DiamondReentrancyGuard.sol"; import {Slot, EquippedItem, IInventory} from "./IInventory.sol"; +import {TerminusPermissions} from "../terminus/TerminusPermissions.sol"; /** LibInventory defines the storage structure used by the Inventory contract as a facet for an EIP-2535 Diamond diff --git a/contracts/mechanics/garden-of-forking-paths/GardenOfForkingPaths.sol b/contracts/mechanics/garden-of-forking-paths/GardenOfForkingPaths.sol index 83910477..4de6c596 100644 --- a/contracts/mechanics/garden-of-forking-paths/GardenOfForkingPaths.sol +++ b/contracts/mechanics/garden-of-forking-paths/GardenOfForkingPaths.sol @@ -7,14 +7,14 @@ pragma solidity ^0.8.0; -import {TerminusFacet} from "@moonstream/contracts/terminus/TerminusFacet.sol"; -import {TerminusPermissions} from "@moonstream/contracts/terminus/TerminusPermissions.sol"; import "@openzeppelin-contracts/contracts/token/ERC1155/utils/ERC1155Holder.sol"; import "@openzeppelin-contracts/contracts/token/ERC721/utils/ERC721Holder.sol"; import "@openzeppelin-contracts/contracts/token/ERC721/IERC721.sol"; import "@openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import "../../diamond/libraries/LibDiamond.sol"; import "../../diamond/security/DiamondReentrancyGuard.sol"; +import {TerminusFacet} from "../../terminus/TerminusFacet.sol"; +import {TerminusPermissions} from "../../terminus/TerminusPermissions.sol"; struct Session { address playerTokenAddress; diff --git a/contracts/mock/MockTerminus.sol b/contracts/mock/MockTerminus.sol index c014b205..eab907e1 100644 --- a/contracts/mock/MockTerminus.sol +++ b/contracts/mock/MockTerminus.sol @@ -2,7 +2,7 @@ ///@notice This contract is for mock for terminus token (used only for cli generation). pragma solidity ^0.8.0; -import "@moonstream/contracts/terminus/TerminusFacet.sol"; +import "../terminus/TerminusFacet.sol"; contract MockTerminus is TerminusFacet { constructor() {} diff --git a/contracts/terminus/ERC1155WithTerminusStorage.sol b/contracts/terminus/ERC1155WithTerminusStorage.sol new file mode 100644 index 00000000..b01dc7a2 --- /dev/null +++ b/contracts/terminus/ERC1155WithTerminusStorage.sol @@ -0,0 +1,638 @@ +// SPDX-License-Identifier: Apache-2.0 + +/** + * Authors: Moonstream Engineering (engineering@moonstream.to) + * GitHub: https://github.com/bugout-dev/dao + * + * An ERC1155 implementation which uses the Moonstream DAO common storage structure for proxies. + * EIP1155: https://eips.ethereum.org/EIPS/eip-1155 + * + * The Moonstream contract is used to delegate calls from an EIP2535 Diamond proxy. + * + * This implementation is adapted from the OpenZeppelin ERC1155 implementation: + * https://github.com/OpenZeppelin/openzeppelin-contracts/tree/6bd6b76d1156e20e45d1016f355d154141c7e5b9/contracts/token/ERC1155 + */ + +pragma solidity ^0.8.9; + +import "@openzeppelin-contracts/contracts/token/ERC1155/IERC1155.sol"; +import "@openzeppelin-contracts/contracts/token/ERC1155/IERC1155Receiver.sol"; +import "@openzeppelin-contracts/contracts/token/ERC1155/extensions/IERC1155MetadataURI.sol"; +import "@openzeppelin-contracts/contracts/utils/Address.sol"; +import "@openzeppelin-contracts/contracts/utils/Context.sol"; +import "@openzeppelin-contracts/contracts/utils/introspection/ERC165.sol"; +import "./LibTerminus.sol"; + +contract ERC1155WithTerminusStorage is + Context, + ERC165, + IERC1155, + IERC1155MetadataURI +{ + using Address for address; + + constructor() {} + + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(ERC165, IERC165) + returns (bool) + { + return + interfaceId == type(IERC1155).interfaceId || + interfaceId == type(IERC1155MetadataURI).interfaceId || + super.supportsInterface(interfaceId); + } + + function uri(uint256 poolID) + public + view + virtual + override + returns (string memory) + { + return LibTerminus.terminusStorage().poolURI[poolID]; + } + + /** + * @dev See {IERC1155-balanceOf}. + * + * Requirements: + * + * - `account` cannot be the zero address. + */ + function balanceOf(address account, uint256 id) + public + view + virtual + override + returns (uint256) + { + require( + account != address(0), + "ERC1155WithTerminusStorage: balance query for the zero address" + ); + return LibTerminus.terminusStorage().poolBalances[id][account]; + } + + /** + * @dev See {IERC1155-balanceOfBatch}. + * + * Requirements: + * + * - `accounts` and `ids` must have the same length. + */ + function balanceOfBatch(address[] memory accounts, uint256[] memory ids) + public + view + virtual + override + returns (uint256[] memory) + { + require( + accounts.length == ids.length, + "ERC1155WithTerminusStorage: accounts and ids length mismatch" + ); + + uint256[] memory batchBalances = new uint256[](accounts.length); + + for (uint256 i = 0; i < accounts.length; ++i) { + batchBalances[i] = balanceOf(accounts[i], ids[i]); + } + + return batchBalances; + } + + /** + * @dev See {IERC1155-setApprovalForAll}. + */ + function setApprovalForAll(address operator, bool approved) + public + virtual + override + { + _setApprovalForAll(_msgSender(), operator, approved); + } + + /** + * @dev See {IERC1155-isApprovedForAll}. + */ + function isApprovedForAll(address account, address operator) + public + view + virtual + override + returns (bool) + { + return + LibTerminus.terminusStorage().globalOperatorApprovals[account][ + operator + ]; + } + + function isApprovedForPool(uint256 poolID, address operator) + public + view + returns (bool) + { + return LibTerminus._isApprovedForPool(poolID, operator); + } + + function approveForPool(uint256 poolID, address operator) external { + LibTerminus.enforcePoolIsController(poolID, _msgSender()); + LibTerminus._approveForPool(poolID, operator); + } + + function unapproveForPool(uint256 poolID, address operator) external { + LibTerminus.enforcePoolIsController(poolID, _msgSender()); + LibTerminus._unapproveForPool(poolID, operator); + } + + /** + * @dev See {IERC1155-safeTransferFrom}. + */ + function safeTransferFrom( + address from, + address to, + uint256 id, + uint256 amount, + bytes memory data + ) public virtual override { + require( + from == _msgSender() || + isApprovedForAll(from, _msgSender()) || + isApprovedForPool(id, _msgSender()), + "ERC1155WithTerminusStorage: caller is not owner nor approved" + ); + _safeTransferFrom(from, to, id, amount, data); + } + + /** + * @dev See {IERC1155-safeBatchTransferFrom}. + */ + function safeBatchTransferFrom( + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) public virtual override { + require( + from == _msgSender() || isApprovedForAll(from, _msgSender()), + "ERC1155WithTerminusStorage: transfer caller is not owner nor approved" + ); + _safeBatchTransferFrom(from, to, ids, amounts, data); + } + + /** + * @dev Transfers `amount` tokens of token type `id` from `from` to `to`. + * + * Emits a {TransferSingle} event. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - `from` must have a balance of tokens of type `id` of at least `amount`. + * - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155Received} and return the + * acceptance magic value. + */ + function _safeTransferFrom( + address from, + address to, + uint256 id, + uint256 amount, + bytes memory data + ) internal virtual { + require( + to != address(0), + "ERC1155WithTerminusStorage: transfer to the zero address" + ); + LibTerminus.TerminusStorage storage ts = LibTerminus.terminusStorage(); + require( + !ts.poolNotTransferable[id], + "ERC1155WithTerminusStorage: _safeTransferFrom -- pool is not transferable" + ); + + address operator = _msgSender(); + + _beforeTokenTransfer( + operator, + from, + to, + _asSingletonArray(id), + _asSingletonArray(amount), + data + ); + + uint256 fromBalance = ts.poolBalances[id][from]; + require( + fromBalance >= amount, + "ERC1155WithTerminusStorage: insufficient balance for transfer" + ); + unchecked { + ts.poolBalances[id][from] = fromBalance - amount; + } + ts.poolBalances[id][to] += amount; + + emit TransferSingle(operator, from, to, id, amount); + + _doSafeTransferAcceptanceCheck(operator, from, to, id, amount, data); + } + + /** + * @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {_safeTransferFrom}. + * + * Emits a {TransferBatch} event. + * + * Requirements: + * + * - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155BatchReceived} and return the + * acceptance magic value. + */ + function _safeBatchTransferFrom( + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) internal virtual { + require( + ids.length == amounts.length, + "ERC1155WithTerminusStorage: ids and amounts length mismatch" + ); + require( + to != address(0), + "ERC1155WithTerminusStorage: transfer to the zero address" + ); + + address operator = _msgSender(); + + _beforeTokenTransfer(operator, from, to, ids, amounts, data); + + LibTerminus.TerminusStorage storage ts = LibTerminus.terminusStorage(); + + for (uint256 i = 0; i < ids.length; ++i) { + uint256 id = ids[i]; + uint256 amount = amounts[i]; + + require( + !ts.poolNotTransferable[id], + "ERC1155WithTerminusStorage: _safeBatchTransferFrom -- pool is not transferable" + ); + + uint256 fromBalance = ts.poolBalances[id][from]; + require( + fromBalance >= amount, + "ERC1155WithTerminusStorage: insufficient balance for transfer" + ); + unchecked { + ts.poolBalances[id][from] = fromBalance - amount; + } + ts.poolBalances[id][to] += amount; + } + + emit TransferBatch(operator, from, to, ids, amounts); + + _doSafeBatchTransferAcceptanceCheck( + operator, + from, + to, + ids, + amounts, + data + ); + } + + /** + * @dev Creates `amount` tokens of token type `id`, and assigns them to `to`. + * + * Emits a {TransferSingle} event. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155Received} and return the + * acceptance magic value. + */ + function _mint( + address to, + uint256 id, + uint256 amount, + bytes memory data + ) internal virtual { + require( + to != address(0), + "ERC1155WithTerminusStorage: mint to the zero address" + ); + LibTerminus.TerminusStorage storage ts = LibTerminus.terminusStorage(); + require( + ts.poolSupply[id] + amount <= ts.poolCapacity[id], + "ERC1155WithTerminusStorage: _mint -- Minted tokens would exceed pool capacity" + ); + + address operator = _msgSender(); + + _beforeTokenTransfer( + operator, + address(0), + to, + _asSingletonArray(id), + _asSingletonArray(amount), + data + ); + + ts.poolSupply[id] += amount; + ts.poolBalances[id][to] += amount; + emit TransferSingle(operator, address(0), to, id, amount); + + _doSafeTransferAcceptanceCheck( + operator, + address(0), + to, + id, + amount, + data + ); + } + + /** + * @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {_mint}. + * + * Requirements: + * + * - `ids` and `amounts` must have the same length. + * - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155BatchReceived} and return the + * acceptance magic value. + */ + function _mintBatch( + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) internal virtual { + require( + to != address(0), + "ERC1155WithTerminusStorage: mint to the zero address" + ); + require( + ids.length == amounts.length, + "ERC1155WithTerminusStorage: ids and amounts length mismatch" + ); + + LibTerminus.TerminusStorage storage ts = LibTerminus.terminusStorage(); + + address operator = _msgSender(); + + _beforeTokenTransfer(operator, address(0), to, ids, amounts, data); + + for (uint256 i = 0; i < ids.length; i++) { + require( + ts.poolSupply[ids[i]] + amounts[i] <= ts.poolCapacity[ids[i]], + "ERC1155WithTerminusStorage: _mintBatch -- Minted tokens would exceed pool capacity" + ); + ts.poolSupply[ids[i]] += amounts[i]; + ts.poolBalances[ids[i]][to] += amounts[i]; + } + + emit TransferBatch(operator, address(0), to, ids, amounts); + + _doSafeBatchTransferAcceptanceCheck( + operator, + address(0), + to, + ids, + amounts, + data + ); + } + + /** + * @dev Destroys `amount` tokens of token type `id` from `from` + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `from` must have at least `amount` tokens of token type `id`. + */ + function _burn( + address from, + uint256 id, + uint256 amount + ) internal virtual { + require( + from != address(0), + "ERC1155WithTerminusStorage: burn from the zero address" + ); + LibTerminus.TerminusStorage storage ts = LibTerminus.terminusStorage(); + require( + ts.poolBurnable[id], + "ERC1155WithTerminusStorage: _burn -- pool is not burnable" + ); + + address operator = _msgSender(); + + _beforeTokenTransfer( + operator, + from, + address(0), + _asSingletonArray(id), + _asSingletonArray(amount), + "" + ); + + uint256 fromBalance = ts.poolBalances[id][from]; + require( + fromBalance >= amount, + "ERC1155WithTerminusStorage: burn amount exceeds balance" + ); + unchecked { + ts.poolBalances[id][from] = fromBalance - amount; + ts.poolSupply[id] -= amount; + } + + emit TransferSingle(operator, from, address(0), id, amount); + } + + /** + * @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {_burn}. + * + * Requirements: + * + * - `ids` and `amounts` must have the same length. + */ + function _burnBatch( + address from, + uint256[] memory ids, + uint256[] memory amounts + ) internal virtual { + require( + from != address(0), + "ERC1155WithTerminusStorage: burn from the zero address" + ); + require( + ids.length == amounts.length, + "ERC1155WithTerminusStorage: ids and amounts length mismatch" + ); + + address operator = _msgSender(); + + LibTerminus.TerminusStorage storage ts = LibTerminus.terminusStorage(); + for (uint256 i = 0; i < ids.length; i++) { + require( + ts.poolBurnable[ids[i]], + "ERC1155WithTerminusStorage: _burnBatch -- pool is not burnable" + ); + } + + _beforeTokenTransfer(operator, from, address(0), ids, amounts, ""); + + for (uint256 i = 0; i < ids.length; i++) { + uint256 id = ids[i]; + uint256 amount = amounts[i]; + + uint256 fromBalance = ts.poolBalances[id][from]; + require( + fromBalance >= amount, + "ERC1155WithTerminusStorage: burn amount exceeds balance" + ); + unchecked { + ts.poolBalances[id][from] = fromBalance - amount; + ts.poolSupply[id] -= amount; + } + } + + emit TransferBatch(operator, from, address(0), ids, amounts); + } + + /** + * @dev Approve `operator` to operate on all of `owner` tokens + * + * Emits a {ApprovalForAll} event. + */ + function _setApprovalForAll( + address owner, + address operator, + bool approved + ) internal virtual { + require( + owner != operator, + "ERC1155WithTerminusStorage: setting approval status for self" + ); + LibTerminus.TerminusStorage storage ts = LibTerminus.terminusStorage(); + ts.globalOperatorApprovals[owner][operator] = approved; + emit ApprovalForAll(owner, operator, approved); + } + + /** + * @dev Hook that is called before any token transfer. This includes minting + * and burning, as well as batched variants. + * + * The same hook is called on both single and batched variants. For single + * transfers, the length of the `id` and `amount` arrays will be 1. + * + * Calling conditions (for each `id` and `amount` pair): + * + * - When `from` and `to` are both non-zero, `amount` of ``from``'s tokens + * of token type `id` will be transferred to `to`. + * - When `from` is zero, `amount` tokens of token type `id` will be minted + * for `to`. + * - when `to` is zero, `amount` of ``from``'s tokens of token type `id` + * will be burned. + * - `from` and `to` are never both zero. + * - `ids` and `amounts` have the same, non-zero length. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _beforeTokenTransfer( + address operator, + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) internal virtual {} + + function _doSafeTransferAcceptanceCheck( + address operator, + address from, + address to, + uint256 id, + uint256 amount, + bytes memory data + ) private { + if (to.isContract()) { + try + IERC1155Receiver(to).onERC1155Received( + operator, + from, + id, + amount, + data + ) + returns (bytes4 response) { + if (response != IERC1155Receiver.onERC1155Received.selector) { + revert( + "ERC1155WithTerminusStorage: ERC1155Receiver rejected tokens" + ); + } + } catch Error(string memory reason) { + revert(reason); + } catch { + revert( + "ERC1155WithTerminusStorage: transfer to non ERC1155Receiver implementer" + ); + } + } + } + + function _doSafeBatchTransferAcceptanceCheck( + address operator, + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) private { + if (to.isContract()) { + try + IERC1155Receiver(to).onERC1155BatchReceived( + operator, + from, + ids, + amounts, + data + ) + returns (bytes4 response) { + if ( + response != IERC1155Receiver.onERC1155BatchReceived.selector + ) { + revert( + "ERC1155WithTerminusStorage: ERC1155Receiver rejected tokens" + ); + } + } catch Error(string memory reason) { + revert(reason); + } catch { + revert( + "ERC1155WithTerminusStorage: transfer to non ERC1155Receiver implementer" + ); + } + } + } + + function _asSingletonArray(uint256 element) + private + pure + returns (uint256[] memory) + { + uint256[] memory array = new uint256[](1); + array[0] = element; + + return array; + } +} diff --git a/contracts/terminus/LibTerminus.sol b/contracts/terminus/LibTerminus.sol new file mode 100644 index 00000000..29bd8497 --- /dev/null +++ b/contracts/terminus/LibTerminus.sol @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: Apache-2.0 + +/** + * Authors: Moonstream Engineering (engineering@moonstream.to) + * GitHub: https://github.com/bugout-dev/dao + * + * Common storage structure and internal methods for Moonstream DAO Terminus contracts. + * As Terminus is an extension of ERC1155, this library can also be used to implement bare ERC1155 contracts + * using the common storage pattern (e.g. for use in diamond proxies). + */ + +// TODO(zomglings): Should we support EIP1761 in addition to ERC1155 or roll our own scopes and feature flags? +// https://eips.ethereum.org/EIPS/eip-1761 + +pragma solidity ^0.8.9; + +library LibTerminus { + bytes32 constant TERMINUS_STORAGE_POSITION = + keccak256("moonstreamdao.eth.storage.terminus"); + + struct TerminusStorage { + // Terminus administration + address controller; + bool isTerminusActive; + uint256 currentPoolID; + address paymentToken; + uint256 poolBasePrice; + // Terminus pools + mapping(uint256 => address) poolController; + mapping(uint256 => string) poolURI; + mapping(uint256 => uint256) poolCapacity; + mapping(uint256 => uint256) poolSupply; + mapping(uint256 => mapping(address => uint256)) poolBalances; + mapping(uint256 => bool) poolNotTransferable; + mapping(uint256 => bool) poolBurnable; + mapping(address => mapping(address => bool)) globalOperatorApprovals; + mapping(uint256 => mapping(address => bool)) globalPoolOperatorApprovals; + // Contract metadata + string contractURI; + } + + function terminusStorage() + internal + pure + returns (TerminusStorage storage es) + { + bytes32 position = TERMINUS_STORAGE_POSITION; + assembly { + es.slot := position + } + } + + event ControlTransferred( + address indexed previousController, + address indexed newController + ); + + event PoolControlTransferred( + uint256 indexed poolID, + address indexed previousController, + address indexed newController + ); + + function setController(address newController) internal { + TerminusStorage storage ts = terminusStorage(); + address previousController = ts.controller; + ts.controller = newController; + emit ControlTransferred(previousController, newController); + } + + function enforceIsController() internal view { + TerminusStorage storage ts = terminusStorage(); + require(msg.sender == ts.controller, "LibTerminus: Must be controller"); + } + + function setTerminusActive(bool active) internal { + TerminusStorage storage ts = terminusStorage(); + ts.isTerminusActive = active; + } + + function setPoolController(uint256 poolID, address newController) internal { + TerminusStorage storage ts = terminusStorage(); + address previousController = ts.poolController[poolID]; + ts.poolController[poolID] = newController; + emit PoolControlTransferred(poolID, previousController, newController); + } + + function createSimplePool(uint256 _capacity) internal returns (uint256) { + TerminusStorage storage ts = terminusStorage(); + uint256 poolID = ts.currentPoolID + 1; + setPoolController(poolID, msg.sender); + ts.poolCapacity[poolID] = _capacity; + ts.currentPoolID++; + return poolID; + } + + function enforcePoolIsController(uint256 poolID, address maybeController) + internal + view + { + TerminusStorage storage ts = terminusStorage(); + require( + ts.poolController[poolID] == maybeController, + "LibTerminus: Must be pool controller" + ); + } + + function _isApprovedForPool(uint256 poolID, address operator) + internal + view + returns (bool) + { + LibTerminus.TerminusStorage storage ts = LibTerminus.terminusStorage(); + if (operator == ts.poolController[poolID]) { + return true; + } else if (ts.globalPoolOperatorApprovals[poolID][operator]) { + return true; + } + return false; + } + + function _approveForPool(uint256 poolID, address operator) internal { + LibTerminus.TerminusStorage storage ts = LibTerminus.terminusStorage(); + ts.globalPoolOperatorApprovals[poolID][operator] = true; + } + + function _unapproveForPool(uint256 poolID, address operator) internal { + LibTerminus.TerminusStorage storage ts = LibTerminus.terminusStorage(); + ts.globalPoolOperatorApprovals[poolID][operator] = false; + } +} diff --git a/contracts/terminus/TerminusFacet.sol b/contracts/terminus/TerminusFacet.sol new file mode 100644 index 00000000..b0c49c5f --- /dev/null +++ b/contracts/terminus/TerminusFacet.sol @@ -0,0 +1,336 @@ +// 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 Terminus decentralized authorization contract. + * + * Terminus users can create authorization pools. Each authorization pool has the following properties: + * 1. Controller: The address that controls the pool. Initially set to be the address of the pool creator. + * 2. Pool URI: Metadata URI for the authorization pool. + * 3. Pool capacity: The total number of tokens that can be minted in that authorization pool. + * 4. Pool supply: The number of tokens that have actually been minted in that authorization pool. + * 5. Transferable: A boolean value which denotes whether or not tokens from that pool can be transfered + * between addresses. (Note: Implemented by TerminusStorage.poolNotTransferable since we expect most + * pools to be transferable. This negation is better for storage + gas since false is default value + * in map to bool.) + * 6. Burnable: A boolean value which denotes whether or not tokens from that pool can be burned. + */ + +pragma solidity ^0.8.0; + +import "@openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import "./ERC1155WithTerminusStorage.sol"; +import "./LibTerminus.sol"; +import "../diamond/libraries/LibDiamond.sol"; + +contract TerminusFacet is ERC1155WithTerminusStorage { + constructor() { + LibTerminus.TerminusStorage storage ts = LibTerminus.terminusStorage(); + ts.controller = msg.sender; + } + + event PoolMintBatch( + uint256 indexed id, + address indexed operator, + address from, + address[] toAddresses, + uint256[] amounts + ); + + function setController(address newController) external { + LibTerminus.enforceIsController(); + LibTerminus.setController(newController); + } + + function poolMintBatch( + uint256 id, + address[] memory toAddresses, + uint256[] memory amounts + ) public { + require( + toAddresses.length == amounts.length, + "TerminusFacet: _poolMintBatch -- toAddresses and amounts length mismatch" + ); + address operator = _msgSender(); + require( + isApprovedForPool(id, operator), + "TerminusFacet: poolMintBatch -- caller is neither owner nor approved" + ); + + LibTerminus.TerminusStorage storage ts = LibTerminus.terminusStorage(); + + uint256 i = 0; + uint256 totalAmount = 0; + + for (i = 0; i < toAddresses.length; i++) { + address to = toAddresses[i]; + uint256 amount = amounts[i]; + require( + to != address(0), + "TerminusFacet: _poolMintBatch -- cannot mint to zero address" + ); + totalAmount += amount; + ts.poolBalances[id][to] += amount; + emit TransferSingle(operator, address(0), to, id, amount); + } + + require( + ts.poolSupply[id] + totalAmount <= ts.poolCapacity[id], + "TerminusFacet: _poolMintBatch -- Minted tokens would exceed pool capacity" + ); + ts.poolSupply[id] += totalAmount; + + emit PoolMintBatch(id, operator, address(0), toAddresses, amounts); + } + + function terminusController() external view returns (address) { + return LibTerminus.terminusStorage().controller; + } + + function paymentToken() external view returns (address) { + return LibTerminus.terminusStorage().paymentToken; + } + + function setPaymentToken(address newPaymentToken) external { + LibTerminus.enforceIsController(); + LibTerminus.TerminusStorage storage ts = LibTerminus.terminusStorage(); + ts.paymentToken = newPaymentToken; + } + + function poolBasePrice() external view returns (uint256) { + return LibTerminus.terminusStorage().poolBasePrice; + } + + function setPoolBasePrice(uint256 newBasePrice) external { + LibTerminus.enforceIsController(); + LibTerminus.TerminusStorage storage ts = LibTerminus.terminusStorage(); + ts.poolBasePrice = newBasePrice; + } + + function _paymentTokenContract() internal view returns (IERC20) { + address paymentTokenAddress = LibTerminus + .terminusStorage() + .paymentToken; + require( + paymentTokenAddress != address(0), + "TerminusFacet: Payment token has not been set" + ); + return IERC20(paymentTokenAddress); + } + + function withdrawPayments(address toAddress, uint256 amount) external { + LibTerminus.enforceIsController(); + require( + _msgSender() == toAddress, + "TerminusFacet: withdrawPayments -- Controller can only withdraw to self" + ); + IERC20 paymentTokenContract = _paymentTokenContract(); + paymentTokenContract.transfer(toAddress, amount); + } + + function contractURI() public view returns (string memory) { + return LibTerminus.terminusStorage().contractURI; + } + + function setContractURI(string memory _contractURI) external { + LibTerminus.enforceIsController(); + LibTerminus.TerminusStorage storage ts = LibTerminus.terminusStorage(); + ts.contractURI = _contractURI; + } + + function setURI(uint256 poolID, string memory poolURI) external { + LibTerminus.enforcePoolIsController(poolID, _msgSender()); + LibTerminus.TerminusStorage storage ts = LibTerminus.terminusStorage(); + ts.poolURI[poolID] = poolURI; + } + + function totalPools() external view returns (uint256) { + return LibTerminus.terminusStorage().currentPoolID; + } + + function setPoolController(uint256 poolID, address newController) external { + LibTerminus.enforcePoolIsController(poolID, msg.sender); + LibTerminus.setPoolController(poolID, newController); + } + + function terminusPoolController(uint256 poolID) + external + view + returns (address) + { + return LibTerminus.terminusStorage().poolController[poolID]; + } + + function terminusPoolCapacity(uint256 poolID) + external + view + returns (uint256) + { + return LibTerminus.terminusStorage().poolCapacity[poolID]; + } + + function terminusPoolSupply(uint256 poolID) + external + view + returns (uint256) + { + return LibTerminus.terminusStorage().poolSupply[poolID]; + } + + function poolIsTransferable(uint256 poolID) external view returns (bool) { + return !LibTerminus.terminusStorage().poolNotTransferable[poolID]; + } + + function poolIsBurnable(uint256 poolID) external view returns (bool) { + return LibTerminus.terminusStorage().poolBurnable[poolID]; + } + + function setPoolTransferable(uint256 poolID, bool transferable) external { + LibTerminus.enforcePoolIsController(poolID, msg.sender); + LibTerminus.TerminusStorage storage ts = LibTerminus.terminusStorage(); + ts.poolNotTransferable[poolID] = !transferable; + } + + function setPoolBurnable(uint256 poolID, bool burnable) external { + LibTerminus.enforcePoolIsController(poolID, msg.sender); + LibTerminus.TerminusStorage storage ts = LibTerminus.terminusStorage(); + ts.poolBurnable[poolID] = burnable; + } + + function createSimplePool(uint256 _capacity) external returns (uint256) { + LibTerminus.enforceIsController(); + LibTerminus.TerminusStorage storage ts = LibTerminus.terminusStorage(); + uint256 requiredPayment = ts.poolBasePrice; + + if (requiredPayment > 0) { + IERC20 paymentTokenContract = _paymentTokenContract(); + require( + paymentTokenContract.allowance(_msgSender(), address(this)) >= + requiredPayment, + "TerminusFacet: createSimplePool -- Insufficient allowance on payment token" + ); + require( + paymentTokenContract.transferFrom( + msg.sender, + address(this), + requiredPayment + ), + "TerminusFacet: createSimplePool -- Transfer of payment token was unsuccessful" + ); + } + return LibTerminus.createSimplePool(_capacity); + } + + function createPoolV1( + uint256 _capacity, + bool _transferable, + bool _burnable + ) external returns (uint256) { + LibTerminus.enforceIsController(); + LibTerminus.TerminusStorage storage ts = LibTerminus.terminusStorage(); + uint256 requiredPayment = ts.poolBasePrice; + if (requiredPayment > 0) { + IERC20 paymentTokenContract = _paymentTokenContract(); + require( + paymentTokenContract.allowance(_msgSender(), address(this)) >= + requiredPayment, + "TerminusFacet: createPoolV1 -- Insufficient allowance on payment token" + ); + require( + paymentTokenContract.transferFrom( + msg.sender, + address(this), + requiredPayment + ), + "TerminusFacet: createPoolV1 -- Transfer of payment token was unsuccessful" + ); + } + uint256 poolID = LibTerminus.createSimplePool(_capacity); + if (!_transferable) { + ts.poolNotTransferable[poolID] = true; + } + if (_burnable) { + ts.poolBurnable[poolID] = true; + } + return poolID; + } + + function createPoolV2( + uint256 _capacity, + bool _transferable, + bool _burnable, + string memory poolURI + ) external returns (uint256) { + LibTerminus.enforceIsController(); + LibTerminus.TerminusStorage storage ts = LibTerminus.terminusStorage(); + uint256 requiredPayment = ts.poolBasePrice; + if (requiredPayment > 0) { + IERC20 paymentTokenContract = _paymentTokenContract(); + require( + paymentTokenContract.allowance(_msgSender(), address(this)) >= + requiredPayment, + "TerminusFacet: createPoolV2 -- Insufficient allowance on payment token" + ); + require( + paymentTokenContract.transferFrom( + msg.sender, + address(this), + requiredPayment + ), + "TerminusFacet: createPoolV2 -- Transfer of payment token was unsuccessful" + ); + } + uint256 poolID = LibTerminus.createSimplePool(_capacity); + if (!_transferable) { + ts.poolNotTransferable[poolID] = true; + } + if (_burnable) { + ts.poolBurnable[poolID] = true; + } + ts.poolURI[poolID] = poolURI; + return poolID; + } + + function mint( + address to, + uint256 poolID, + uint256 amount, + bytes memory data + ) external { + require( + isApprovedForPool(poolID, msg.sender), + "TerminusFacet: mint -- caller is neither owner nor approved" + ); + _mint(to, poolID, amount, data); + } + + function mintBatch( + address to, + uint256[] memory poolIDs, + uint256[] memory amounts, + bytes memory data + ) external { + for (uint256 i = 0; i < poolIDs.length; i++) { + require( + isApprovedForPool(poolIDs[i], msg.sender), + "TerminusFacet: mintBatch -- caller is neither owner nor approved" + ); + } + _mintBatch(to, poolIDs, amounts, data); + } + + function burn( + address from, + uint256 poolID, + uint256 amount + ) external { + address operator = _msgSender(); + require( + operator == from || isApprovedForPool(poolID, operator), + "TerminusFacet: burn -- caller is neither owner nor approved" + ); + _burn(from, poolID, amount); + } +} diff --git a/contracts/terminus/TerminusInitializer.sol b/contracts/terminus/TerminusInitializer.sol new file mode 100644 index 00000000..c7066832 --- /dev/null +++ b/contracts/terminus/TerminusInitializer.sol @@ -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 Terminus contract. Used when mounting a new TerminusFacet onto its diamond proxy. + */ + +pragma solidity ^0.8.9; + +import "@openzeppelin-contracts/contracts/token/ERC1155/IERC1155.sol"; +import "@openzeppelin-contracts/contracts/token/ERC1155/extensions/IERC1155MetadataURI.sol"; +import "../diamond/libraries/LibDiamond.sol"; +import "./LibTerminus.sol"; + +contract TerminusInitializer { + function init() external { + LibDiamond.enforceIsContractOwner(); + LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage(); + ds.supportedInterfaces[type(IERC1155).interfaceId] = true; + ds.supportedInterfaces[type(IERC1155MetadataURI).interfaceId] = true; + + LibTerminus.TerminusStorage storage ts = LibTerminus.terminusStorage(); + ts.controller = msg.sender; + } +} diff --git a/contracts/terminus/TerminusPermissions.sol b/contracts/terminus/TerminusPermissions.sol new file mode 100644 index 00000000..b58f0610 --- /dev/null +++ b/contracts/terminus/TerminusPermissions.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: Apache-2.0 + +/** + * Authors: Moonstream Engineering (engineering@moonstream.to) + * GitHub: https://github.com/bugout-dev/dao + * + */ + +import "@openzeppelin-contracts/contracts/access/Ownable.sol"; +import "./TerminusFacet.sol"; + +pragma solidity ^0.8.9; + +abstract contract TerminusPermissions { + function _holdsPoolToken( + address terminusAddress, + uint256 poolId, + uint256 _amount + ) internal view returns (bool) { + TerminusFacet terminus = TerminusFacet(terminusAddress); + return terminus.balanceOf(msg.sender, poolId) >= _amount; + } + + modifier holdsPoolToken(address terminusAddress, uint256 poolId) { + require( + _holdsPoolToken(terminusAddress, poolId, 1), + "TerminusPermissions.holdsPoolToken: Sender doens't hold pool tokens" + ); + _; + } + + modifier spendsPoolToken(address terminusAddress, uint256 poolId) { + require( + _holdsPoolToken(terminusAddress, poolId, 1), + "TerminusPermissions.spendsPoolToken: Sender doens't hold pool tokens" + ); + TerminusFacet terminusContract = TerminusFacet(terminusAddress); + terminusContract.burn(msg.sender, poolId, 1); + _; + } +}