From 5f208331c92497f3e93be276180931385e6ff090 Mon Sep 17 00:00:00 2001 From: David Roon Date: Fri, 21 Oct 2022 15:16:30 +0200 Subject: [PATCH] Resume deployment and other improvements (#576) * making it possible to resume deployment. adding ERC20 function too * fix test * fix test for erc20 and fix slither warning * fix logging & speed up verify --- .../token/erc20/ERC20TokenExtension.sol | 57 +++-- hardhat.config.js | 2 - tasks/verify.ts | 5 +- test/extensions/erc20.test.js | 11 + utils/contract-util.js | 5 +- utils/deployment-util.js | 241 +++++++++++------- utils/hardhat-util.js | 15 ++ 7 files changed, 219 insertions(+), 117 deletions(-) diff --git a/contracts/extensions/token/erc20/ERC20TokenExtension.sol b/contracts/extensions/token/erc20/ERC20TokenExtension.sol index 04c847d73..3786c4e88 100644 --- a/contracts/extensions/token/erc20/ERC20TokenExtension.sol +++ b/contracts/extensions/token/erc20/ERC20TokenExtension.sol @@ -75,22 +75,6 @@ contract ERC20Extension is AdapterGuard, IExtension, IERC20 { dao = _dao; } - function bytes32ToString(bytes32 _bytes32) - internal - pure - returns (string memory) - { - uint8 i = 0; - while (i < 32 && _bytes32[i] != 0) { - i++; - } - bytes memory bytesArray = new bytes(i); - for (i = 0; i < 32 && _bytes32[i] != 0; i++) { - bytesArray[i] = _bytes32[i]; - } - return string(bytesArray); - } - /** * @dev Returns the token address managed by the DAO that tracks the * internal transfers. @@ -156,6 +140,18 @@ contract ERC20Extension is AdapterGuard, IExtension, IERC20 { return bank.balanceOf(DaoHelper.TOTAL, tokenAddress); } + /** + * @dev Returns the amount of tokens assigned to all the members. + */ + function totalAssignedTokens() external view returns (uint256) { + BankExtension bank = BankExtension( + dao.getExtensionAddress(DaoHelper.BANK) + ); + return + bank.balanceOf(DaoHelper.TOTAL, tokenAddress) - + bank.balanceOf(DaoHelper.GUILD, tokenAddress); + } + /** * @dev Returns the amount of tokens owned by `account`. */ @@ -214,16 +210,13 @@ contract ERC20Extension is AdapterGuard, IExtension, IERC20 { address senderAddr = dao.getAddressIfDelegated(msg.sender); require( DaoHelper.isNotZeroAddress(senderAddr), - "ERC20: approve from the zero address" - ); - require( - DaoHelper.isNotZeroAddress(spender), - "ERC20: approve to the zero address" + "ERC20: approve from 0x0" ); + require(DaoHelper.isNotZeroAddress(spender), "ERC20: approve to 0x0"); require(dao.isMember(senderAddr), "sender is not a member"); require( DaoHelper.isNotReservedAddress(spender), - "spender can not be a reserved address" + "spender is reserved address" ); _allowances[senderAddr][spender] = amount; @@ -285,7 +278,7 @@ contract ERC20Extension is AdapterGuard, IExtension, IERC20 { ) public override returns (bool) { require( DaoHelper.isNotZeroAddress(recipient), - "ERC20: transfer to the zero address" + "ERC20: transfer to 0x0" ); IERC20TransferStrategy strategy = IERC20TransferStrategy( @@ -323,7 +316,7 @@ contract ERC20Extension is AdapterGuard, IExtension, IERC20 { //check if sender has approved msg.sender to spend amount require( currentAllowance >= amount, - "ERC20: transfer amount exceeds allowance" + "ERC20: amount exceeds allowance" ); if (allowedAmount >= amount) { @@ -340,4 +333,20 @@ contract ERC20Extension is AdapterGuard, IExtension, IERC20 { return false; } + + function bytes32ToString(bytes32 _bytes32) + internal + pure + returns (string memory) + { + uint8 i = 0; + while (i < 32 && _bytes32[i] != 0) { + i++; + } + bytes memory bytesArray = new bytes(i); + for (i = 0; i < 32 && _bytes32[i] != 0; i++) { + bytesArray[i] = _bytes32[i]; + } + return string(bytesArray); + } } diff --git a/hardhat.config.js b/hardhat.config.js index 90aa9e819..73f282762 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -48,8 +48,6 @@ module.exports = { network_id: 5, chainId: 5, skipDryRun: true, - gas: 2100000, - gasPrice: 4000000000, accounts: { mnemonic: process.env.WALLET_MNEMONIC || "", count: 10, diff --git a/tasks/verify.ts b/tasks/verify.ts index edfd0fd9c..c2e78958b 100755 --- a/tasks/verify.ts +++ b/tasks/verify.ts @@ -125,7 +125,10 @@ const main = async () => { p.then(async () => { const r = await verify(c); log(`[${count++}/${verifyContracts.length}]`); - await sleep(1500); // avoid rate-limit errors + + if(!r || !r.stderr) { + await sleep(1500); // avoid rate-limit errors + } return r; }), Promise.resolve(0) diff --git a/test/extensions/erc20.test.js b/test/extensions/erc20.test.js index 502106966..4fce2fad0 100644 --- a/test/extensions/erc20.test.js +++ b/test/extensions/erc20.test.js @@ -34,6 +34,7 @@ const { ZERO_ADDRESS, numberOfUnits, DAI_TOKEN, + GUILD, } = require("../../utils/contract-util"); const { @@ -447,6 +448,7 @@ describe("Extension - ERC20", () => { let externalAddressAUnits = await erc20Ext.balanceOf( externalAddressA.address ); + expect(externalAddressAUnits.toString()).equal( numberOfUnits.mul(toBN("0")).toString() ); @@ -463,6 +465,15 @@ describe("Extension - ERC20", () => { expect(externalAddressAUnits.toString()).equal( numberOfUnits.mul(toBN("0")).toString() ); + + const guildBalance = await erc20Ext.balanceOf(GUILD); + const totalSupply = await erc20Ext.totalSupply(); + + const assignedBalance = await erc20Ext.totalAssignedTokens(); + + expect(totalSupply.sub(guildBalance).toString()).equal( + assignedBalance.toString() + ); }); it("should not be possible to approve a transferFrom units from a member to an external account when the transfer type is equals 0 (member transfer only)", async () => { diff --git a/utils/contract-util.js b/utils/contract-util.js index d43b7be35..d7f552203 100644 --- a/utils/contract-util.js +++ b/utils/contract-util.js @@ -40,6 +40,7 @@ const toBNWeb3 = Web3Utils.toBN; // Ethers.js utils const { ethers } = require("ethers"); const { error } = require("./log-util"); +const { info } = require("console"); const toUtf8 = ethers.utils.toUtf8String; const toBytes32 = ethers.utils.formatBytes32String; const toHex = ethers.utils.hexValue; @@ -72,11 +73,13 @@ const maxUnits = toBN("10000000000000000000"); const waitTx = async (p) => { try { + info("waiting for transaction to be submitted / mined"); let res = await p; if (res && res.wait) { + info(`submitted! tx hash:${res.hash} - nonce:${res.nonce}`); res = await res.wait(); + info("transaction mined"); } - return res; } catch (err) { error(err); diff --git a/utils/deployment-util.js b/utils/deployment-util.js index 42b631e8f..e5845a0e0 100644 --- a/utils/deployment-util.js +++ b/utils/deployment-util.js @@ -119,9 +119,9 @@ const createFactories = async ({ options }) => { */ const createExtensions = async ({ dao, factories, options }) => { const extensions = {}; - debug("create extensions ..."); + const createExtension = async ({ dao, factory, options }) => { - debug("create extension ", factory.configs.alias); + info("create extension " + factory.configs.alias); const factoryConfigs = factory.configs; const extensionConfigs = options.contractConfigs.find( (c) => c.id === factoryConfigs.generatesExtensionId @@ -131,78 +131,113 @@ const createExtensions = async ({ dao, factories, options }) => { `Missing extension configuration for in ${factoryConfigs.name} configs` ); - let tx; - if ( - factoryConfigs.deploymentArgs && - factoryConfigs.deploymentArgs.length > 0 - ) { - const args = factoryConfigs.deploymentArgs.map((argName) => { - const arg = options[argName]; - if (arg !== null && arg !== undefined) return arg; - throw new Error( - `Missing deployment argument <${argName}> in ${factoryConfigs.name}.create` - ); - }); - tx = await factory.create(...args); - } else { - tx = await factory.create(); - } - /** - * The tx event is the safest way to read the new extension address. - * 1. Find the event that contains the `args` field which indicates the factory event - * 2. Take the data at index 1 which represents the new extension address - */ - let extensionAddress; - - if (tx.wait) { - const res = await tx.wait(); - const factoryEvent = res.events.find( - (e) => e.address === factory.address + const extensionId = sha3(extensionConfigs.id); + let extensionAddress = await dao.extensions(extensionId); + + if (extensionAddress === ZERO_ADDRESS) { + let tx; + info( + `extension ${extensionConfigs.name} not found in the DAO, deploying it ` ); - if (!factoryEvent) throw new Error("Missing factory event."); + if ( + factoryConfigs.deploymentArgs && + factoryConfigs.deploymentArgs.length > 0 + ) { + const args = factoryConfigs.deploymentArgs.map((argName) => { + const arg = options[argName]; + if (arg !== null && arg !== undefined) return arg; + throw new Error( + `Missing deployment argument <${argName}> in ${factoryConfigs.name}.create` + ); + }); + tx = await factory.create(...args); + } else { + tx = await factory.create(); + } - extensionAddress = factoryEvent.args[1]; - } else { - const { logs } = tx; - const factoryLog = logs.find((l) => l.address === factory.address); - if (!factoryLog) { - throw new Error("no event emitted by the factory"); + /** + * The tx event is the safest way to read the new extension address. + * 1. Find the event that contains the `args` field which indicates the factory event + * 2. Take the data at index 1 which represents the new extension address + */ + let extensionAddress; + if (tx.wait) { + info( + `waiting for transaction ${tx.hash} to be mined... nonce: ${tx.nonce}` + ); + const res = await tx.wait(); + const factoryEvent = res.events.find( + (e) => e.address === factory.address + ); + if (!factoryEvent) throw new Error("Missing factory event."); + + extensionAddress = factoryEvent.args[1]; + } else { + const { logs } = tx; + const factoryLog = logs.find((l) => l.address === factory.address); + if (!factoryLog) { + throw new Error("no event emitted by the factory"); + } + extensionAddress = factoryLog.args[1]; } - extensionAddress = factoryLog.args[1]; - } - const extensionInterface = options[extensionConfigs.name]; - if (!extensionInterface) - throw new Error( - `Extension contract not found for ${extensionConfigs.name}` + + const extensionInterface = options[extensionConfigs.name]; + if (!extensionInterface) + throw new Error( + `Extension contract not found for ${extensionConfigs.name}` + ); + + const newExtension = embedConfigs( + await options.attachFunction(extensionInterface, extensionAddress), + extensionInterface.contractName, + options.contractConfigs ); - const newExtension = embedConfigs( - await options.attachFunction(extensionInterface, extensionAddress), - extensionInterface.contractName, - options.contractConfigs - ); + if (!newExtension || !newExtension.configs) + throw new Error( + `Unable to embed extension configs for ${extensionConfigs.name}` + ); - if (!newExtension || !newExtension.configs) - throw new Error( - `Unable to embed extension configs for ${extensionConfigs.name}` + await waitTx( + dao["addExtension(bytes32,address)"](extensionId, newExtension.address) ); - await waitTx( - dao["addExtension(bytes32,address)"]( - sha3(newExtension.configs.id), - newExtension.address - ) - ); + info(` + Extension enabled '${newExtension.configs.name}' + ------------------------------------------------- + contract address: ${newExtension.address} + creator address: ${options.owner} + identity address: ${factory.identity.address} + `); + return { ...newExtension, identity: factory.identity }; + } else { + const extensionInterface = options[extensionConfigs.name]; + if (!extensionInterface) + throw new Error( + `Extension contract not found for ${extensionConfigs.name}` + ); - info(` - Extension enabled '${newExtension.configs.name}' - ------------------------------------------------- - contract address: ${newExtension.address} - creator address: ${options.owner} - identity address: ${factory.identity.address} - `); + const newExtension = embedConfigs( + await options.attachFunction(extensionInterface, extensionAddress), + extensionInterface.contractName, + options.contractConfigs + ); - return { ...newExtension, identity: factory.identity }; + if (!newExtension || !newExtension.configs) + throw new Error( + `Unable to embed extension configs for ${extensionConfigs.name}` + ); + + info(` + Extension enabled '${newExtension.configs.name}' + ------------------------------------------------- + contract address: ${newExtension.address} + creator address: ${options.owner} + identity address: ${factory.identity.address} + `); + + return { ...newExtension, identity: factory.identity }; + } }; await Object.values(factories).reduce( @@ -407,8 +442,10 @@ const validateContractConfigs = (contractConfigs) => { * configs/networks/*.config.ts. */ const deployDao = async (options) => { + info("validate contract configs"); validateContractConfigs(options.contractConfigs); + info("create DaoRegistry"); const { dao, daoFactory } = await cloneDao({ ...options, name: options.daoName || "test-dao", @@ -420,9 +457,11 @@ const deployDao = async (options) => { unitTokenToMint: UNITS, lootTokenToMint: LOOT, }; - + info("create factories"); const factories = await createFactories({ options }); + info("create extensions"); const extensions = await createExtensions({ dao, factories, options }); + info("create adapters"); const adapters = await createAdapters({ dao, daoFactory, @@ -490,12 +529,21 @@ const cloneDao = async ({ DaoFactory, name, }) => { + info("deploy or load Dao factory"); const daoFactory = await deployFunction(DaoFactory, [DaoRegistry]); - await waitTx(daoFactory.createDao(name, creator ? creator : owner)); + let daoAddress = await daoFactory.getDaoAddress(name); + if (daoAddress === ZERO_ADDRESS) { + info(`create a DaoRegistry ${name} ${creator ? creator : owner}`); + await waitTx(daoFactory.createDao(name, creator ? creator : owner)); + + daoAddress = await daoFactory.getDaoAddress(name); + } - const daoAddress = await daoFactory.getDaoAddress(name); if (daoAddress === ZERO_ADDRESS) throw Error("Invalid dao address"); const daoInstance = await attachFunction(DaoRegistry, daoAddress); + const daoState = await daoInstance.state(); + //if Dao is already finialized + if (daoState === 1) throw Error("Dao is already finalized"); info(` Cloned 'DaoRegistry' ------------------------------------------------- @@ -536,22 +584,33 @@ const configureDao = async ({ .filter((a) => !a.configs.skipAutoDeploy) .filter((a) => a.configs.acls.dao); - await adaptersWithAccess.reduce((p, a) => { + const txs = await adaptersWithAccess.reduce((p, a) => { info(` - Adapter configured '${a.configs.name}' + Configuring Adapter '${a.configs.name}' ------------------------------------------------- contract address: ${a.address} contract acls: ${JSON.stringify(a.configs.acls)}`); - return p.then( - async () => - await waitTx( - daoFactory.addAdapters(dao.address, [ - entryDao(a.configs.id, a.address, a.configs.acls), - ]) - ) - ); - }, Promise.resolve()); + return p.then(async (previous) => { + const entry = entryDao(a.configs.id, a.address, a.configs.acls); + const adapterAddress = await dao.adapters(entry.id); + + //add the adapter again if the address is different + if (adapterAddress !== a.address) { + const addedTx = await daoFactory.addAdapters(dao.address, [entry]); + info( + `waiting for tx ${addedTx.hash} that adds adapter ${a.configs.name} nonce ${addedTx.nonce}` + ); + return previous.concat([addedTx]); + } else { + info(`Adapter ${a.configs.name} already added to the DAO`); + return previous; + } + }); + }, Promise.resolve([])); + info("waiting for all adapter txs to be mined"); + //once they have all been added, time to wait for each of them to be mined + await Promise.all(txs.filter((tx) => !!tx.wait).map((tx) => tx.wait())); // If an extension needs access to other extension, // that extension needs to be added to the DAO as an adapter contract, @@ -561,22 +620,26 @@ const configureDao = async ({ .filter((a) => !a.configs.skipAutoDeploy) .filter((e) => Object.keys(e.configs.acls.extensions).length > 0); - await extensionsWithAccess.reduce((p, e) => { + const extTxs = await extensionsWithAccess.reduce((p, e) => { info(` - Extension configured '${e.configs.name}' + Configuring Extension '${e.configs.name}' ------------------------------------------------- contract address: ${e.address} contract acls: ${JSON.stringify(e.configs.acls)}`); - return p.then( - async () => - await waitTx( - daoFactory.addAdapters(dao.address, [ - entryDao(e.configs.id, e.address, e.configs.acls), - ]) - ) - ); - }, Promise.resolve()); + return p.then(async (previous) => { + const tx = await daoFactory.addAdapters(dao.address, [ + entryDao(e.configs.id, e.address, e.configs.acls), + ]); + + info( + `waiting for tx ${tx.hash} that configures extension ${e.configs.name} nonce ${tx.nonce}` + ); + return previous.concat([tx]); + }); + }, Promise.resolve([])); + info(`waiting for all extension configutation txs to be mined`); + await Promise.all(extTxs.filter((tx) => !!tx.wait).map((tx) => tx.wait())); }; const configureAdaptersWithDAOParameters = async () => { diff --git a/utils/hardhat-util.js b/utils/hardhat-util.js index c6da56e54..7cce98a3f 100644 --- a/utils/hardhat-util.js +++ b/utils/hardhat-util.js @@ -59,6 +59,9 @@ const deployFunction = async ({ allConfigs, network, daoArtifacts }) => { res = await contractFactory.deploy(); } + info( + `Deploying contract ${contractInterface.contractName} with transaction id ${res.deployTransaction.hash} nonce: ${res.deployTransaction.nonce}` + ); const tx = await res.deployTransaction.wait(); const contract = await res.deployed(); info(` @@ -116,6 +119,18 @@ const deployFunction = async ({ allConfigs, network, daoArtifacts }) => { ------------------------------------------------- contract address: ${contractAddress}`); const instance = await attach(contractInterface, contractAddress); + if (contractConfig.type === ContractType.Factory && args) { + const identityInterface = args.flat()[0]; + const identity = await instance.identityAddress(); + return { + ...instance, + configs: contractConfig, + identity: { + name: identityInterface.contractName, + address: identity, + }, + }; + } return { ...instance, configs: contractConfig }; }