diff --git a/.travis.yml b/.travis.yml index f5f910f50..29ddc4a89 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ cache: jobs: include: - stage: Test - install: + install: - echo -ne '\n' | sudo add-apt-repository ppa:ethereum/ethereum - sudo apt-get update - sudo apt-get install -y dpkg diff --git a/0 b/0 new file mode 100644 index 000000000..e69de29bb diff --git a/CLI/commands/ST20Generator.js b/CLI/commands/ST20Generator.js index 689b2b27f..0863c727e 100644 --- a/CLI/commands/ST20Generator.js +++ b/CLI/commands/ST20Generator.js @@ -32,7 +32,9 @@ async function executeApp(_ticker, _transferOwnership, _name, _details, _divisib await step_transfer_ticker_ownership(_transferOwnership); await step_token_deploy(_name, _details, _divisible); } - await tokenManager.executeApp(tokenSymbol); + if (typeof _divisible === 'undefined') { + await tokenManager.executeApp(tokenSymbol); + } } catch (err) { console.log(err); return; diff --git a/CLI/commands/TickerRollForward.js b/CLI/commands/TickerRollForward.js index 69d36575d..266749da1 100644 --- a/CLI/commands/TickerRollForward.js +++ b/CLI/commands/TickerRollForward.js @@ -20,9 +20,9 @@ let securityTokenRegistry; let securityTokenRegistryAddress; function Ticker(_owner, _symbol, _name) { - this.owner = _owner; - this.symbol = _symbol; - this.name = _name; + this.owner = _owner; + this.symbol = _symbol; + this.name = _name; } function FailedRegistration(_ticker, _error) { @@ -58,11 +58,11 @@ async function startScript() { } async function readFile() { - var stream = fs.createReadStream("./CLI/data/ticker_data.csv"); + var stream = fs.createReadStream(`${__dirname}/../data/ticker_data.csv`); var csvStream = csv() .on("data", function (data) { - ticker_data.push(new Ticker(data[0],data[1],data[2],data[3])); + ticker_data.push(new Ticker(data[0], data[1], data[2], data[3])); }) .on("end", async function () { await registerTickers(); @@ -73,12 +73,12 @@ async function readFile() { async function registerTickers() { // Poly approval for registration fees let polyBalance = BigNumber(await polyToken.methods.balanceOf(Issuer.address).call()); - let fee = web3.utils.fromWei(await securityTokenRegistry.methods.getTickerRegistrationFee().call()); + let fee = web3.utils.fromWei(await securityTokenRegistry.methods.getTickerRegistrationFee().call()); let totalFee = BigNumber(ticker_data.length).mul(fee); if (totalFee.gt(polyBalance)) { console.log(chalk.red(`\n*******************************************************************************`)); - console.log(chalk.red(`Not enough POLY to pay registration fee. Require ${totalFee.div(10**18).toNumber()} POLY but have ${polyBalance.div(10**18).toNumber()} POLY.`)); + console.log(chalk.red(`Not enough POLY to pay registration fee. Require ${totalFee.div(10 ** 18).toNumber()} POLY but have ${polyBalance.div(10 ** 18).toNumber()} POLY.`)); console.log(chalk.red(`*******************************************************************************\n`)); process.exit(0); } else { @@ -100,7 +100,7 @@ async function registerTickers() { } // validate ticker - await securityTokenRegistry.methods.getTickerDetails(ticker_data[i].symbol).call({}, function(error, result){ + await securityTokenRegistry.methods.getTickerDetails(ticker_data[i].symbol).call({}, function (error, result) { if (result[1] != 0) { failed_tickers.push(` ${i} is already registered`); valid = false; @@ -131,7 +131,7 @@ async function logResults() { Successful registrations: ${registered_tickers.length} Failed registrations: ${failed_tickers.length} Total gas consumed: ${totalGas} - Total gas cost: ${defaultGasPrice.mul(totalGas).div(10**18)} ETH + Total gas cost: ${defaultGasPrice.mul(totalGas).div(10 ** 18)} ETH List of failed registrations: ${failed_tickers} diff --git a/CLI/commands/common/common_functions.js b/CLI/commands/common/common_functions.js index fb3db68b7..7b21e555f 100644 --- a/CLI/commands/common/common_functions.js +++ b/CLI/commands/common/common_functions.js @@ -3,7 +3,7 @@ const Tx = require('ethereumjs-tx'); const permissionsList = require('./permissions_list'); const abis = require('../helpers/contract_abis'); -async function connect(abi, address) { +function connect(abi, address) { contractRegistry = new web3.eth.Contract(abi, address); contractRegistry.setProvider(web3.currentProvider); return contractRegistry @@ -15,7 +15,7 @@ async function checkPermission(contractName, functionName, contractRegistry) { return true } else { let stAddress = await contractRegistry.methods.securityToken().call(); - let securityToken = await connect(abis.securityToken(), stAddress); + let securityToken = connect(abis.securityToken(), stAddress); let stOwner = await securityToken.methods.owner().call(); if (stOwner == Issuer.address) { return true @@ -49,7 +49,7 @@ async function getGasLimit(options, action) { async function checkPermissions(action) { let contractRegistry = await connect(action._parent.options.jsonInterface, action._parent._address); - //NOTE this is a condition to verify if the transaction comes from a module or not. + //NOTE this is a condition to verify if the transaction comes from a module or not. if (contractRegistry.methods.hasOwnProperty('factory')) { let moduleAddress = await contractRegistry.methods.factory().call(); let moduleRegistry = await connect(abis.moduleFactory(), moduleAddress); @@ -153,10 +153,13 @@ module.exports = { let filteredLogs = logs.filter(l => l.topics.includes(eventJsonInterface.signature)); return filteredLogs.map(l => web3.eth.abi.decodeLog(eventJsonInterface.inputs, l.data, l.topics.slice(1))); }, + connect: function (abi, address) { + return connect(abi, address) + }, splitIntoBatches: function (data, batchSize) { let allBatches = []; - for (let index = 0; index < data.length; index += batchSize) { - allBatches.push(data.slice(index, index + batchSize)); + for (let index = 0; index < data.length; index += parseInt(batchSize)) { + allBatches.push(data.slice(index, index + parseInt(batchSize))); } return allBatches; }, diff --git a/CLI/commands/common/constants.js b/CLI/commands/common/constants.js index 30a3bf699..d121fd2da 100644 --- a/CLI/commands/common/constants.js +++ b/CLI/commands/common/constants.js @@ -29,7 +29,7 @@ module.exports = Object.freeze({ FUND_RAISE_TYPES: { ETH: 0, POLY: 1, - DAI: 2 + STABLE: 2 }, DEFAULT_BATCH_SIZE: 75, ADDRESS_ZERO: '0x0000000000000000000000000000000000000000' diff --git a/CLI/commands/common/permissions_list.js b/CLI/commands/common/permissions_list.js index 4d2b58cf7..8f74c15e7 100644 --- a/CLI/commands/common/permissions_list.js +++ b/CLI/commands/common/permissions_list.js @@ -60,9 +60,7 @@ function getPermissionList() { }, ManualApprovalTransferManager: { addManualApproval: "TRANSFER_APPROVAL", - addManualBlocking: "TRANSFER_APPROVAL", revokeManualApproval: "TRANSFER_APPROVAL", - revokeManualBlocking: "TRANSFER_APPROVAL" }, PercentageTransferManager: { modifyWhitelist: "WHITELIST", @@ -70,36 +68,60 @@ function getPermissionList() { setAllowPrimaryIssuance: "ADMIN", changeHolderPercentage: "ADMIN" }, - LockupVolumeRestrictionTM: { - addLockup: "ADMIN", - addLockUpMulti: "ADMIN", - removeLockUp: "ADMIN", - modifyLockUp: "ADMIN" + VolumeRestrictionTM: { + changeExemptWalletList: "ADMIN", + addIndividualRestriction: "ADMIN", + addIndividualRestrictionMulti: "ADMIN", + addGlobalRestriction: "ADMIN", + addDailyGlobalRestriction: "ADMIN", + removeIndividualRestriction: "ADMIN", + removeIndividualRestrictionMulti: "ADMIN", + removeGlobalRestriction: "ADMIN", + removeDailyGlobalRestriction: "ADMIN", + modifyIndividualRestriction: "ADMIN", + modifyIndividualRestrictionMulti: "ADMIN", + modifyGlobalRestriction: "ADMIN", + modifyDailyGlobalRestriction: "ADMIN" }, - SingleTradeVolumeRestrictionTM: { - setAllowPrimaryIssuance: "ADMIN", - changeTransferLimitToPercentage: "ADMIN", - changeTransferLimitToTokens: "ADMIN", - changeGlobalLimitInTokens: "ADMIN", - changeGlobalLimitInPercentage: "ADMIN", - addExemptWallet: "ADMIN", - removeExemptWallet: "ADMIN", - addExemptWalletMulti: "ADMIN", - removeExemptWalletMulti: "ADMIN", - setTransferLimitInTokens: "ADMIN", - setTransferLimitInPercentage: "ADMIN", - removeTransferLimitInPercentage: "ADMIN", - removeTransferLimitInTokens: "ADMIN", - setTransferLimitInTokensMulti: "ADMIN", - setTransferLimitInPercentageMulti: "ADMIN", - removeTransferLimitInTokensMulti: "ADMIN", - removeTransferLimitInPercentageMulti: "ADMIN" + BlacklistTransferManager: { + addBlacklistType: "ADMIN", + addBlacklistTypeMulti: "ADMIN", + modifyBlacklistType: "ADMIN", + modifyBlacklistTypeMulti: "ADMIN", + deleteBlacklistType: "ADMIN", + deleteBlacklistTypeMulti: "ADMIN", + addInvestorToBlacklist: "ADMIN", + addInvestorToBlacklistMulti: "ADMIN", + addMultiInvestorToBlacklistMulti: "ADMIN", + addInvestorToNewBlacklist: "ADMIN", + deleteInvestorFromAllBlacklist: "ADMIN", + deleteInvestorFromAllBlacklistMulti: "ADMIN", + deleteInvestorFromBlacklist: "ADMIN", + deleteMultiInvestorsFromBlacklistMulti: "ADMIN", + }, + VestingEscrowWallet: { + changeTreasuryWallet: "ONLY_OWNER", + depositTokens: "ADMIN", + sendToTreasury: "ADMIN", + pushAvailableTokens: "ADMIN", + addTemplate: "ADMIN", + removeTemplate: "ADMIN", + addSchedule: "ADMIN", + addScheduleFromTemplate: "ADMIN", + modifySchedule: "ADMIN", + revokeSchedule: "ADMIN", + revokeAllSchedules: "ADMIN", + pushAvailableTokensMulti: "ADMIN", + addScheduleMulti: "ADMIN", + addScheduleFromTemplateMulti: "ADMIN", + revokeSchedulesMulti: "ADMIN", + modifyScheduleMulti: "ADMIN" } } } module.exports = { - verifyPermission: function(contractName, functionName) { + verifyPermission: function (contractName, functionName) { let list = getPermissionList(); try { return list[contractName][functionName] diff --git a/CLI/commands/dividends_manager.js b/CLI/commands/dividends_manager.js index 0f423bf5d..0b4dd8f39 100644 --- a/CLI/commands/dividends_manager.js +++ b/CLI/commands/dividends_manager.js @@ -1,492 +1,667 @@ -var readlineSync = require('readline-sync'); -var chalk = require('chalk'); -var moment = require('moment'); -var common = require('./common/common_functions'); -var gbl = require('./common/global'); -var contracts = require('./helpers/contract_addresses'); -var abis = require('./helpers/contract_abis'); +const readlineSync = require('readline-sync'); +const chalk = require('chalk'); +const moment = require('moment'); +const common = require('./common/common_functions'); +const gbl = require('./common/global'); +const contracts = require('./helpers/contract_addresses'); +const abis = require('./helpers/contract_abis'); +const csvParse = require('./helpers/csv'); +const { table } = require('table') + +const EXCLUSIONS_DATA_CSV = `${__dirname}/../data/Checkpoint/exclusions_data.csv`; +const TAX_WITHHOLDING_DATA_CSV = `${__dirname}/../data/Checkpoint/tax_withholding_data.csv`; // App flow let tokenSymbol; let securityToken; let polyToken; let securityTokenRegistry; -let generalTransferManager; +let moduleRegistry; let currentDividendsModule; -async function executeApp(type) { - dividendsType = type; +let dividendsType; - common.logAsciiBull(); - console.log("**********************************************"); - console.log("Welcome to the Command-Line Dividends Manager."); - console.log("**********************************************"); - console.log("Issuer Account: " + Issuer.address + "\n"); +async function executeApp() { + console.log('\n', chalk.blue('Dividends Manager - Main Menu', '\n')); - await setup(); - try { - await start_explorer(); - } catch (err) { - console.log(err); - return; + let tmModules = await getAllModulesByType(gbl.constants.MODULES_TYPES.DIVIDENDS); + let nonArchivedModules = tmModules.filter(m => !m.archived); + if (nonArchivedModules.length > 0) { + console.log(`Dividends modules attached:`); + nonArchivedModules.map(m => console.log(`- ${m.name} at ${m.address}`)) + } else { + console.log(`There are no dividends modules attached`); } -}; -async function setup(){ - try { - let securityTokenRegistryAddress = await contracts.securityTokenRegistry(); - let securityTokenRegistryABI = abis.securityTokenRegistry(); - securityTokenRegistry = new web3.eth.Contract(securityTokenRegistryABI, securityTokenRegistryAddress); - securityTokenRegistry.setProvider(web3.currentProvider); + let currentCheckpoint = await securityToken.methods.currentCheckpointId().call(); + if (currentCheckpoint > 0) { + console.log(`\nCurrent checkpoint: ${currentCheckpoint}`); + } - let polyTokenAddress = await contracts.polyToken(); - let polyTokenABI = abis.polyToken(); - polyToken = new web3.eth.Contract(polyTokenABI, polyTokenAddress); - polyToken.setProvider(web3.currentProvider); - } catch (err) { - console.log(err) - console.log('\x1b[31m%s\x1b[0m',"There was a problem getting the contracts. Make sure they are deployed to the selected network."); - process.exit(0); + let options = ['Create checkpoint', 'Explore address balances']; + if (nonArchivedModules.length > 0) { + options.push('Config existing modules'); + } + options.push('Add new dividends module'); + + let index = readlineSync.keyInSelect(options, 'What do you want to do?', { cancel: 'EXIT' }); + let optionSelected = index != -1 ? options[index] : 'EXIT'; + console.log('Selected:', optionSelected, '\n'); + switch (optionSelected) { + case 'Create checkpoint': + await createCheckpointFromST(); + break; + case 'Explore address balances': + await exploreAddress(currentCheckpoint); + break; + case 'Config existing modules': + await configExistingModules(nonArchivedModules); + break; + case 'Add new dividends module': + await addDividendsModule(); + break; + case 'EXIT': + return; } -} -async function start_explorer(){ - console.log('\n\x1b[34m%s\x1b[0m',"Dividends Manager - Main Menu"); + await executeApp(); +} - if (!tokenSymbol) - tokenSymbol = readlineSync.question('Enter the token symbol: '); +async function createCheckpointFromST() { + let createCheckpointAction = securityToken.methods.createCheckpoint(); + let receipt = await common.sendTransaction(createCheckpointAction); + let event = common.getEventFromLogs(securityToken._jsonInterface, receipt.logs, 'CheckpointCreated'); + console.log(chalk.green(`Checkpoint ${event._checkpointId} has been created successfully!`)); +} - let result = await securityTokenRegistry.methods.getSecurityTokenAddress(tokenSymbol).call(); - if (result == "0x0000000000000000000000000000000000000000") { - tokenSymbol = undefined; - console.log(chalk.red(`Token symbol provided is not a registered Security Token.`)); - } else { - let securityTokenABI = abis.securityToken(); - securityToken = new web3.eth.Contract(securityTokenABI,result); +async function exploreAddress(currentCheckpoint) { + let address = readlineSync.question('Enter address to explore: ', { + limit: function (input) { + return web3.utils.isAddress(input); + }, + limitMessage: "Must be a valid address", + }); + let checkpoint = null; + if (currentCheckpoint > 0) { + checkpoint = await selectCheckpoint(false); + } - // Get the GTM - result = await securityToken.methods.getModulesByName(web3.utils.toHex('GeneralTransferManager')).call(); - if (result.length == 0) { - console.log(chalk.red(`General Transfer Manager is not attached.`)); - } else { - generalTransferManagerAddress = result[0]; - let generalTransferManagerABI = abis.generalTransferManager(); - generalTransferManager = new web3.eth.Contract(generalTransferManagerABI, generalTransferManagerAddress); - generalTransferManager.setProvider(web3.currentProvider); - - let typeOptions = ['POLY', 'ETH']; - if (!typeOptions.includes(dividendsType)) { - let index = readlineSync.keyInSelect(typeOptions, 'What type of dividends do you want work with?', {cancel: false}); - dividendsType = typeOptions[index]; - console.log(`Selected: ${dividendsType}`) - } + let balance = web3.utils.fromWei(await securityToken.methods.balanceOf(address).call()); + let totalSupply = web3.utils.fromWei(await securityToken.methods.totalSupply().call()); + console.log(`Balance of ${address} is: ${balance} ${tokenSymbol}`); + console.log(`TotalSupply is: ${totalSupply} ${tokenSymbol}`); - let currentCheckpoint = await securityToken.methods.currentCheckpointId().call(); - console.log(chalk.yellow(`\nToken is at checkpoint: ${currentCheckpoint}`)); + if (checkpoint) { + let balanceAt = web3.utils.fromWei(await securityToken.methods.balanceOfAt(address, checkpoint).call()); + let totalSupplyAt = web3.utils.fromWei(await securityToken.methods.totalSupplyAt(checkpoint).call()); + console.log(`Balance of ${address} at checkpoint ${checkpoint}: ${balanceAt} ${tokenSymbol}`); + console.log(`TotalSupply at checkpoint ${checkpoint} is: ${totalSupplyAt} ${tokenSymbol}`); + } +} - let options = ['Mint tokens', 'Transfer tokens', 'Create checkpoint', 'Set default exclusions for dividends', 'Tax holding settings', 'Create dividends'] +async function configExistingModules(dividendModules) { + let options = dividendModules.map(m => `${m.name} at ${m.address}`); + let index = readlineSync.keyInSelect(options, 'Which module do you want to config? ', { cancel: 'RETURN' }); + console.log('Selected:', index != -1 ? options[index] : 'RETURN', '\n'); + let moduleNameSelected = index != -1 ? dividendModules[index].name : 'RETURN'; + switch (moduleNameSelected) { + case 'ERC20DividendCheckpoint': + currentDividendsModule = new web3.eth.Contract(abis.erc20DividendCheckpoint(), dividendModules[index].address); + currentDividendsModule.setProvider(web3.currentProvider); + dividendsType = 'ERC20'; + break; + case 'EtherDividendCheckpoint': + currentDividendsModule = new web3.eth.Contract(abis.etherDividendCheckpoint(), dividendModules[index].address); + currentDividendsModule.setProvider(web3.currentProvider); + dividendsType = 'ETH'; + break; + } - if (currentCheckpoint > 0) { - options.push('Explore account at checkpoint', 'Explore total supply at checkpoint') - } + await dividendsManager(); +} - // Only show dividend options if divididenModule is already attached - if (await isDividendsModuleAttached()) { - options.push('Push dividends to accounts', - `Explore ${dividendsType} balance`, 'Reclaim expired dividends') - } +async function dividendsManager() { + console.log(chalk.blue(`Dividends module at ${currentDividendsModule.options.address}`), '\n'); - let index = readlineSync.keyInSelect(options, 'What do you want to do?'); - let selected = index != -1 ? options[index] : 'Cancel'; - console.log('Selected:', selected, '\n'); - switch (selected) { - case 'Mint tokens': - let _to = readlineSync.question('Enter beneficiary of minting: '); - let _amount = readlineSync.question('Enter amount of tokens to mint: '); - await mintTokens(_to,_amount); - break; - case 'Transfer tokens': - let _to2 = readlineSync.question('Enter beneficiary of tranfer: '); - let _amount2 = readlineSync.question('Enter amount of tokens to transfer: '); - await transferTokens(_to2,_amount2); - break; - case 'Create checkpoint': - let createCheckpointAction = securityToken.methods.createCheckpoint(); - await common.sendTransaction(createCheckpointAction); - break; - case 'Set default exclusions for dividends': - await setDefaultExclusions(); - break; - case 'Tax holding settings': - await taxHoldingMenu(); - break; - case 'Create dividends': - let divName = readlineSync.question(`Enter a name or title to indetify this dividend: `); - let dividend = readlineSync.question(`How much ${dividendsType} would you like to distribute to token holders?: `); - await checkBalance(dividend); - let checkpointId = currentCheckpoint == 0 ? 0 : await selectCheckpoint(true); // If there are no checkpoints, it must create a new one - await createDividends(divName, dividend, checkpointId); - break; - case 'Explore account at checkpoint': - let _address = readlineSync.question('Enter address to explore: '); - let _checkpoint = await selectCheckpoint(false); - await exploreAddress(_address, _checkpoint); - break; - case 'Explore total supply at checkpoint': - let _checkpoint2 = await selectCheckpoint(false); - await exploreTotalSupply(_checkpoint2); - break; - case 'Push dividends to accounts': - let _dividend = await selectDividend({valid: true, expired: false, reclaimed: false, withRemaining: true}); - if (_dividend !== null) { - let _addresses = readlineSync.question('Enter addresses to push dividends to (ex- add1,add2,add3,...): '); - await pushDividends(_dividend, _addresses); - } - break; - case `Explore ${dividendsType} balance`: - let _address3 = readlineSync.question('Enter address to explore: '); - let _dividend3 = await selectDividend(); - if (_dividend3 !== null) { - let dividendAmounts = await currentDividendsModule.methods.calculateDividend(_dividend3.index, _address3).call(); - let dividendBalance = dividendAmounts[0]; - let dividendTax = dividendAmounts[1]; - let balance = await getBalance(_address3); - console.log(` - ${dividendsType} Balance: ${web3.utils.fromWei(balance)} ${dividendsType} - Dividends owned: ${web3.utils.fromWei(dividendBalance)} ${dividendsType} - Tax withheld: ${web3.utils.fromWei(dividendTax)} ${dividendsType} - `); - } - break; - case 'Reclaim expired dividends': - let _dividend4 = await selectDividend({expired: true, reclaimed: false}); - if (_dividend4 !== null) { - await reclaimedDividend(_dividend4); - } - break; - case 'Cancel': - process.exit(0); - break; - } - } - } - //Restart - await start_explorer(); -} + let wallet = await currentDividendsModule.methods.wallet().call(); + let currentDividends = await getDividends(); + let defaultExcluded = await currentDividendsModule.methods.getDefaultExcluded().call(); + let currentCheckpointId = await securityToken.methods.currentCheckpointId().call(); -async function mintTokens(address, amount){ - if (await securityToken.methods.mintingFrozen().call()) { - console.log(chalk.red("Minting is not possible - Minting has been permanently frozen by issuer")); - } else { - await whitelistAddress(address); + console.log(`- Wallet: ${wallet}`); + console.log(`- Current dividends: ${currentDividends.length}`); + console.log(`- Default exclusions: ${defaultExcluded.length}`); - try { - let mintAction = securityToken.methods.mint(address,web3.utils.toWei(amount)); - let receipt = await common.sendTransaction(mintAction); - let event = common.getEventFromLogs(securityToken._jsonInterface, receipt.logs, 'Transfer'); - console.log(` - Minted ${web3.utils.fromWei(event.value)} tokens - to account ${event.to}` - ); - } catch (err) { - console.log(err); - console.log(chalk.red("There was an error processing the transfer transaction. \n The most probable cause for this error is one of the involved accounts not being in the whitelist or under a lockup period.")); - } + let options = ['Change wallet', 'Create checkpoint']; + if (currentCheckpointId > 0) { + options.push('Explore checkpoint'); } -} - -async function transferTokens(address, amount){ - await whitelistAddress(address); + if (defaultExcluded.length > 0) { + options.push('Show current default exclusions'); + } + options.push( + 'Set default exclusions', + 'Set tax withholding' + ); + if (currentDividends.length > 0) { + options.push('Manage existing dividends'); + } + options.push('Create new dividends'); - try{ - let transferAction = securityToken.methods.transfer(address,web3.utils.toWei(amount)); - let receipt = await common.sendTransaction(transferAction, {factor: 1.5}); - let event = common.getEventFromLogs(securityToken._jsonInterface, receipt.logs, 'Transfer'); - console.log(` - Account ${event.from} - transferred ${web3.utils.fromWei(event.value)} tokens - to account ${event.to}` - ); - } catch (err) { - console.log(err); - console.log(chalk.red("There was an error processing the transfer transaction. \n The most probable cause for this error is one of the involved accounts not being in the whitelist or under a lockup period.")); + let index = readlineSync.keyInSelect(options, 'What do you want to do?', { cancel: 'RETURN' }); + let selected = index != -1 ? options[index] : 'RETURN'; + console.log('Selected:', selected, '\n'); + switch (selected) { + case 'Change wallet': + await changeWallet(); + break; + case 'Create checkpoint': + await createCheckpointFromDividendModule(); + break; + case 'Explore checkpoint': + await exploreCheckpoint(); + case 'Show current default exclusions': + showExcluded(defaultExcluded); + break; + case 'Set default exclusions': + await setDefaultExclusions(); + break; + case 'Set tax withholding': + await taxWithholding(); + break; + case 'Manage existing dividends': + let selectedDividend = await selectDividend(currentDividends); + if (selectedDividend) { + await manageExistingDividend(selectedDividend.index); + } + break; + case 'Create new dividends': + await createDividends(); + break; + case 'RETURN': + return; } -} -async function exploreAddress(address, checkpoint){ - let balance = await securityToken.methods.balanceOf(address).call(); - balance = web3.utils.fromWei(balance); - console.log(`Balance of ${address} is: ${balance} (Using balanceOf)`); + await dividendsManager(); +} - let balanceAt = await securityToken.methods.balanceOfAt(address,checkpoint).call(); - balanceAt = web3.utils.fromWei(balanceAt); - console.log(`Balance of ${address} is: ${balanceAt} (Using balanceOfAt - checkpoint ${checkpoint})`); +async function changeWallet() { + let newWallet = readlineSync.question('Enter the new account address to receive reclaimed dividends and tax: ', { + limit: function (input) { + return web3.utils.isAddress(input); + }, + limitMessage: "Must be a valid address", + }); + let action = currentDividendsModule.methods.changeWallet(newWallet); + let receipt = await common.sendTransaction(action); + let event = common.getEventFromLogs(currentDividendsModule._jsonInterface, receipt.logs, 'SetWallet'); + console.log(chalk.green(`The wallet has been changed successfully!`)); } -async function exploreTotalSupply(checkpoint){ - let totalSupply = await securityToken.methods.totalSupply().call(); - totalSupply = web3.utils.fromWei(totalSupply); - console.log(`TotalSupply is: ${totalSupply} (Using totalSupply)`); +async function createCheckpointFromDividendModule() { + let createCheckpointAction = securityToken.methods.createCheckpoint(); + await common.sendTransaction(createCheckpointAction); + console.log(chalk.green(`Checkpoint have been created successfully!`)); +} - let totalSupplyAt = await securityToken.methods.totalSupplyAt(checkpoint).call(); - totalSupplyAt = web3.utils.fromWei(totalSupplyAt); - console.log(`TotalSupply is: ${totalSupplyAt} (Using totalSupplyAt - checkpoint ${checkpoint})`); +async function exploreCheckpoint() { + let checkpoint = await selectCheckpoint(false); + + let checkpointData = await currentDividendsModule.methods.getCheckpointData(checkpoint).call(); + let dataTable = [['Investor', `Balance at checkpoint (${tokenSymbol})`, 'Tax withholding set (%)']]; + for (let i = 0; i < checkpointData.investors.length; i++) { + dataTable.push([ + checkpointData.investors[i], + web3.utils.fromWei(checkpointData.balances[i]), + parseFloat(web3.utils.fromWei(checkpointData.withholdings[i])) * 100 + ]); + } + console.log(); + console.log(table(dataTable)); } async function setDefaultExclusions() { - await addDividendsModule(); - - let excluded = await currentDividendsModule.methods.getDefaultExcluded().call(); - showExcluded(excluded); - - console.log(chalk.yellow(`Excluded addresses will be loaded from 'dividendsExclusions_data.csv'. Please check your data before continue.`)); + console.log(chalk.yellow(`Excluded addresses will be loaded from 'exclusions_data.csv'. Please check your data before continue.`)); if (readlineSync.keyInYNStrict(`Do you want to continue?`)) { let excluded = getExcludedFromDataFile(); - let setDefaultExclusionsActions = currentDividendsModule.methods.setDefaultExcluded(excluded); + let setDefaultExclusionsActions = currentDividendsModule.methods.setDefaultExcluded(excluded[0]); let receipt = await common.sendTransaction(setDefaultExclusionsActions); let event = common.getEventFromLogs(currentDividendsModule._jsonInterface, receipt.logs, 'SetDefaultExcludedAddresses'); - console.log(chalk.green(`Exclusions were successfully set.`)); - showExcluded(event._excluded); + console.log(chalk.green(`Exclusions have been set successfully!`)); + showExcluded(event._excluded); } } -async function taxHoldingMenu() { - await addDividendsModule(); +async function manageExistingDividend(dividendIndex) { + // Show current data - let options = ['Set a % to withhold from dividends sent to an address', 'Withdraw withholding for dividend', 'Return to main menu']; - let index = readlineSync.keyInSelect(options, 'What do you want to do?', {cancel: false}); - let selected = options[index]; - console.log("Selected:", selected); - switch (selected) { - case 'Set a % to withhold from dividends sent to an address': - let address = readlineSync.question('Enter the address of the investor: ', { - limit: function(input) { - return web3.utils.isAddress(input); - }, - limitMessage: "Must be a valid address", - }); - let percentage = readlineSync.question('Enter the percentage of dividends to withhold (number between 0-100): ', { - limit: function(input) { - return (parseInt(input) >= 0 && parseInt(input) <= 100); - }, - limitMessage: "Must be a value between 0 and 100", - }); - let percentageWei = web3.utils.toWei((percentage / 100).toString()); - let setWithHoldingFixedAction = currentDividendsModule.methods.setWithholdingFixed([address], percentageWei); - let receipt = await common.sendTransaction(setWithHoldingFixedAction); - console.log(chalk.green(`Successfully set tax withholding of ${percentage}% for ${address}.`)); + let dividend = await currentDividendsModule.methods.dividends(dividendIndex).call(); + let dividendTokenAddress = gbl.constants.ADDRESS_ZERO; + let dividendTokenSymbol = 'ETH'; + if (dividendsType === 'ERC20') { + dividendTokenAddress = await currentDividendsModule.methods.dividendTokens(dividendIndex).call(); + let erc20token = new web3.eth.Contract(abis.erc20(), dividendTokenAddress); + dividendTokenSymbol = await erc20token.methods.symbol().call(); + } + let progress = await currentDividendsModule.methods.getDividendProgress(dividendIndex).call(); + let investorArray = progress[0]; + let claimedArray = progress[1]; + let excludedArray = progress[2]; + let withheldArray = progress[3]; + let amountArray = progress[4]; + let balanceArray = progress[5]; + + // function for adding two numbers. Easy! + const add = (a, b) => { + const bnA = new web3.utils.BN(a); + const bnB = new web3.utils.BN(b); + return bnA.add(bnB).toString(); + }; + // use reduce to sum our array + let taxesToWithHeld = withheldArray.reduce(add, 0); + let claimedInvestors = claimedArray.filter(c => c).length; + let excludedInvestors = excludedArray.filter(e => e).length; + + console.log(`- Name: ${web3.utils.hexToUtf8(dividend.name)}`); + console.log(`- Created: ${moment.unix(dividend.created).format('MMMM Do YYYY, HH:mm:ss')}`); + console.log(`- Maturity: ${moment.unix(dividend.maturity).format('MMMM Do YYYY, HH:mm:ss')}`); + console.log(`- Expiry: ${moment.unix(dividend.expiry).format('MMMM Do YYYY, HH:mm:ss')}`); + console.log(`- At checkpoint: ${dividend.checkpointId}`); + console.log(`- Amount: ${web3.utils.fromWei(dividend.amount)} ${dividendTokenSymbol}`); + console.log(`- Claimed amount: ${web3.utils.fromWei(dividend.claimedAmount)} ${dividendTokenSymbol}`); + console.log(`- Taxes:`); + console.log(` To withhold: ${web3.utils.fromWei(taxesToWithHeld)} ${dividendTokenSymbol}`); + console.log(` Withheld to-date: ${web3.utils.fromWei(dividend.totalWithheld)} ${dividendTokenSymbol}`); + console.log(` Withdrawn to-date: ${web3.utils.fromWei(dividend.totalWithheldWithdrawn)} ${dividendTokenSymbol}`); + console.log(`- Total investors: ${investorArray.length}`); + console.log(` Have already claimed: ${claimedInvestors} (${investorArray.length - excludedInvestors !== 0 ? claimedInvestors / (investorArray.length - excludedInvestors) * 100 : 0}%)`); + console.log(` Excluded: ${excludedInvestors} `); + // ------------------ + + + let options = ['Show investors', 'Show report', 'Explore account']; + if (isValidDividend(dividend) && hasRemaining(dividend) && !isExpiredDividend(dividend) && !dividend.reclaimed) { + options.push('Push dividends to accounts'); + } + if (hasRemainingWithheld(dividend)) { + options.push('Withdraw withholding'); + } + if (isExpiredDividend(dividend) && !dividend.reclaimed) { + options.push('Reclaim expired dividends'); + } + + let index = readlineSync.keyInSelect(options, 'What do you want to do?', { cancel: 'RETURN' }); + let optionSelected = index !== -1 ? options[index] : 'RETURN'; + console.log('Selected:', optionSelected, '\n'); + switch (optionSelected) { + case 'Show investors': + showInvestors(investorArray, claimedArray, excludedArray); break; - case 'Withdraw withholding for dividend': - let _dividend = await selectDividend({withRemainingWithheld: true}); - if (_dividend !== null) { - let withdrawWithholdingAction = currentDividendsModule.methods.withdrawWithholding(_dividend.index); - let receipt = await common.sendTransaction(withdrawWithholdingAction); - let eventName; - if (dividendsType == 'POLY') { - eventName = 'ERC20DividendWithholdingWithdrawn'; - } else if (dividendsType == 'ETH') { - eventName = 'EtherDividendWithholdingWithdrawn'; - } - let event = common.getEventFromLogs(currentDividendsModule._jsonInterface, receipt.logs, eventName); - console.log(chalk.green(`Successfully withdrew ${web3.utils.fromWei(event._withheldAmount)} ${dividendsType} from dividend ${_dividend.index} tax withholding.`)); - } + case 'Show report': + showReport( + web3.utils.hexToUtf8(dividend.name), + dividendTokenSymbol, + dividend.amount, // Total amount of dividends sent + dividend.totalWithheld, // Total amount of taxes withheld + dividend.claimedAmount, // Total amount of dividends distributed + investorArray, // Per Address(Amount sent, Taxes withheld (%), Taxes withheld ($/ETH/# Tokens), Amount received, Withdrawn (TRUE/FALSE) + claimedArray, + excludedArray, + withheldArray, + amountArray + ); + break; + case 'Push dividends to accounts': + await pushDividends(dividendIndex, dividend.checkpointId); break; - case 'Return to main menu': + case 'Explore account': + await exploreAccount(dividendIndex, dividendTokenAddress, dividendTokenSymbol); break; + case 'Withdraw withholding': + await withdrawWithholding(dividendIndex, dividendTokenSymbol); + break; + case 'Reclaim expired dividends': + await reclaimedDividend(dividendIndex, dividendTokenSymbol); + return; + case 'RETURN': + return; } -} -async function createDividends(name, dividend, checkpointId) { - await addDividendsModule(); + await manageExistingDividend(dividendIndex); +} - let time = Math.floor(Date.now()/1000); - let maturityTime = readlineSync.questionInt('Enter the dividend maturity time from which dividend can be paid (Unix Epoch time)\n(Now = ' + time + ' ): ', {defaultInput: time}); - let defaultTime = time + gbl.constants.DURATION.minutes(10); - let expiryTime = readlineSync.questionInt('Enter the dividend expiry time (Unix Epoch time)\n(10 minutes from now = ' + defaultTime + ' ): ', {defaultInput: defaultTime}); - - let useDefaultExcluded = readlineSync.keyInYNStrict(`Do you want to use the default excluded addresses for this dividend? If not, data from 'dividendsExclusions_data.csv' will be used instead.`); +async function taxWithholding() { + let addresses = readlineSync.question(`Enter addresses to set tax withholding to(ex - add1, add2, add3, ...) or leave empty to read from 'tax_withholding_data.csv': `, { + limit: function (input) { + return input === '' || (input.split(',').every(a => web3.utils.isAddress(a))); + }, + limitMessage: `All addresses must be valid` + }).split(','); + if (addresses[0] !== '') { + let percentage = readlineSync.question('Enter the percentage of dividends to withhold (number between 0-100): ', { + limit: function (input) { + return (parseFloat(input) >= 0 && parseFloat(input) <= 100); + }, + limitMessage: 'Must be a value between 0 and 100', + }); + let percentageWei = web3.utils.toWei((percentage / 100).toString()); + let setWithHoldingFixedAction = currentDividendsModule.methods.setWithholdingFixed(addresses, percentageWei); + let receipt = await common.sendTransaction(setWithHoldingFixedAction); + let event = common.getEventFromLogs(currentDividendsModule._jsonInterface, receipt.logs, 'SetWithholdingFixed'); + console.log(chalk.green(`Successfully set tax rate of ${web3.utils.fromWei(event._withholding)}% for: `)); + console.log(chalk.green(event._investors)); + } else { + let parsedData = csvParse(TAX_WITHHOLDING_DATA_CSV); + let validData = parsedData.filter(row => + web3.utils.isAddress(row[0]) && + !isNaN(row[1]) + ); + let invalidRows = parsedData.filter(row => !validData.includes(row)); + if (invalidRows.length > 0) { + console.log(chalk.red(`The following lines from csv file are not valid: ${invalidRows.map(r => parsedData.indexOf(r) + 1).join(',')} `)); + } + let batches = common.splitIntoBatches(validData, 100); + let [investorArray, taxArray] = common.transposeBatches(batches); + for (let batch = 0; batch < batches.length; batch++) { + taxArray[batch] = taxArray[batch].map(t => web3.utils.toWei((t / 100).toString())); + console.log(`Batch ${batch + 1} - Attempting to set multiple tax rates to accounts: \n\n`, investorArray[batch], '\n'); + let action = await currentDividendsModule.methods.setWithholding(investorArray[batch], taxArray[batch]); + let receipt = await common.sendTransaction(action); + console.log(chalk.green('Multiple tax rates have benn set successfully!')); + console.log(`${receipt.gasUsed} gas used.Spent: ${web3.utils.fromWei((new web3.utils.BN(receipt.gasUsed)).mul(new web3.utils.BN(defaultGasPrice)))} ETH`); + } + } +} - let createDividendAction; - if (dividendsType == 'POLY') { - let approveAction = polyToken.methods.approve(currentDividendsModule._address, web3.utils.toWei(dividend)); - await common.sendTransaction(approveAction); - if (checkpointId > 0) { - if (useDefaultExcluded) { - createDividendAction = currentDividendsModule.methods.createDividendWithCheckpoint(maturityTime, expiryTime, polyToken._address, web3.utils.toWei(dividend), checkpointId, web3.utils.toHex(name)); - } else { - let excluded = getExcludedFromDataFile(); - createDividendAction = currentDividendsModule.methods.createDividendWithCheckpointAndExclusions(maturityTime, expiryTime, polyToken._address, web3.utils.toWei(dividend), checkpointId, excluded, web3.utils.toHex(name)); - } - } else { - if (useDefaultExcluded) { - createDividendAction = currentDividendsModule.methods.createDividend(maturityTime, expiryTime, polyToken._address, web3.utils.toWei(dividend), web3.utils.toHex(name)); - } else { - let excluded = getExcludedFromDataFile(); - createDividendAction = currentDividendsModule.methods.createDividendWithExclusions(maturityTime, expiryTime, polyToken._address, web3.utils.toWei(dividend), excluded, web3.utils.toHex(name)); +async function createDividends() { + let dividendName = readlineSync.question(`Enter a name or title to indetify this dividend: `); + let dividendToken = gbl.constants.ADDRESS_ZERO; + let dividendSymbol = 'ETH'; + let token; + if (dividendsType === 'ERC20') { + do { + dividendToken = readlineSync.question(`Enter the address of ERC20 token in which dividend will be denominated(POLY = ${polyToken.options.address}): `, { + limit: function (input) { + return web3.utils.isAddress(input); + }, + limitMessage: "Must be a valid ERC20 address", + defaultInput: polyToken.options.address + }); + token = new web3.eth.Contract(abis.erc20(), dividendToken); + try { + dividendSymbol = await token.methods.symbol().call(); + } catch (err) { + console.log(chalk.red(`${dividendToken} is not a valid ERC20 token address!!`)); } - } - let receipt = await common.sendTransaction(createDividendAction); - let event = common.getEventFromLogs(currentDividendsModule._jsonInterface, receipt.logs, 'ERC20DividendDeposited'); - console.log(chalk.green(`Dividend ${event._dividendIndex} deposited`)); - } else if (dividendsType == 'ETH') { - if (checkpointId > 0) { - if (useDefaultExcluded) { - createDividendAction = currentDividendsModule.methods.createDividendWithCheckpoint(maturityTime, expiryTime, checkpointId, web3.utils.toHex(name)); + } while (dividendSymbol === 'ETH'); + } + let dividendAmount = readlineSync.question(`How much ${dividendSymbol} would you like to distribute to token holders? `); + + let dividendAmountBN = new web3.utils.BN(dividendAmount); + let issuerBalance = new web3.utils.BN(web3.utils.fromWei(await getBalance(Issuer.address, dividendToken))); + if (issuerBalance.lt(dividendAmountBN)) { + console.log(chalk.red(`You have ${issuerBalance} ${dividendSymbol}.You need ${dividendAmountBN.sub(issuerBalance)} ${dividendSymbol} more!`)); + } else { + let checkpointId = await selectCheckpoint(true); // If there are no checkpoints, it must create a new one + let now = Math.floor(Date.now() / 1000); + let maturityTime = readlineSync.questionInt('Enter the dividend maturity time from which dividend can be paid (Unix Epoch time)\n(Now = ' + now + ' ): ', { defaultInput: now }); + let defaultTime = now + gbl.constants.DURATION.minutes(10); + let expiryTime = readlineSync.questionInt('Enter the dividend expiry time (Unix Epoch time)\n(10 minutes from now = ' + defaultTime + ' ): ', { defaultInput: defaultTime }); + + let useDefaultExcluded = !readlineSync.keyInYNStrict(`Do you want to use data from 'dividends_exclusions_data.csv' for this dividend? If not, default exclusions will apply.`); + + let createDividendAction; + if (dividendsType == 'ERC20') { + let approveAction = token.methods.approve(currentDividendsModule._address, web3.utils.toWei(dividendAmountBN)); + await common.sendTransaction(approveAction); + if (checkpointId > 0) { + if (useDefaultExcluded) { + createDividendAction = currentDividendsModule.methods.createDividendWithCheckpoint(maturityTime, expiryTime, token.options.address, web3.utils.toWei(dividendAmountBN), checkpointId, web3.utils.toHex(dividendName)); + } else { + let excluded = getExcludedFromDataFile(); + createDividendAction = currentDividendsModule.methods.createDividendWithCheckpointAndExclusions(maturityTime, expiryTime, token.options.address, web3.utils.toWei(dividendAmountBN), checkpointId, excluded[0], web3.utils.toHex(dividendName)); + } } else { - let excluded = getExcludedFromDataFile(); - createDividendAction = currentDividendsModule.methods.createDividendWithCheckpointAndExclusions(maturityTime, expiryTime, checkpointId, excluded, web3.utils.toHex(name)); + if (useDefaultExcluded) { + createDividendAction = currentDividendsModule.methods.createDividend(maturityTime, expiryTime, token.options.address, web3.utils.toWei(dividendAmountBN), web3.utils.toHex(dividendName)); + } else { + let excluded = getExcludedFromDataFile(); + createDividendAction = currentDividendsModule.methods.createDividendWithExclusions(maturityTime, expiryTime, token.options.address, web3.utils.toWei(dividendAmountBN), excluded[0], web3.utils.toHex(dividendName)); + } } + let receipt = await common.sendTransaction(createDividendAction); + let event = common.getEventFromLogs(currentDividendsModule._jsonInterface, receipt.logs, 'ERC20DividendDeposited'); + console.log(chalk.green(`Dividend ${event._dividendIndex} deposited`)); } else { - if (useDefaultExcluded) { - createDividendAction = currentDividendsModule.methods.createDividend(maturityTime, expiryTime, web3.utils.toHex(name)); + if (checkpointId > 0) { + if (useDefaultExcluded) { + createDividendAction = currentDividendsModule.methods.createDividendWithCheckpoint(maturityTime, expiryTime, checkpointId, web3.utils.toHex(dividendName)); + } else { + let excluded = getExcludedFromDataFile(); + createDividendAction = currentDividendsModule.methods.createDividendWithCheckpointAndExclusions(maturityTime, expiryTime, checkpointId, excluded, web3.utils.toHex(dividendName)); + } } else { - let excluded = getExcludedFromDataFile(); - createDividendAction = currentDividendsModule.methods.createDividendWithExclusions(maturityTime, expiryTime, excluded, web3.utils.toHex(name)); + if (useDefaultExcluded) { + createDividendAction = currentDividendsModule.methods.createDividend(maturityTime, expiryTime, web3.utils.toHex(dividendName)); + } else { + let excluded = getExcludedFromDataFile(); + createDividendAction = currentDividendsModule.methods.createDividendWithExclusions(maturityTime, expiryTime, excluded, web3.utils.toHex(dividendName)); + } } - } - let receipt = await common.sendTransaction(createDividendAction, {value: web3.utils.toWei(dividend)}); - let event = common.getEventFromLogs(currentDividendsModule._jsonInterface, receipt.logs, 'EtherDividendDeposited'); - console.log(` - Dividend ${event._dividendIndex} deposited` - ); - } -} - -async function pushDividends(dividend, account){ - let accs = account.split(','); - let pushDividendPaymentToAddressesAction = currentDividendsModule.methods.pushDividendPaymentToAddresses(dividend.index, accs); - let receipt = await common.sendTransaction(pushDividendPaymentToAddressesAction); - let successEventName; - if (dividendsType == 'POLY') { - successEventName = 'ERC20DividendClaimed'; - } else if (dividendsType == 'ETH') { - successEventName = 'EtherDividendClaimed'; - let failedEventName = 'EtherDividendClaimFailed'; - let failedEvents = common.getMultipleEventsFromLogs(currentDividendsModule._jsonInterface, receipt.logs, failedEventName); - for (const event of failedEvents) { + let receipt = await common.sendTransaction(createDividendAction, { value: web3.utils.toWei(dividendAmountBN) }); + let event = common.getEventFromLogs(currentDividendsModule._jsonInterface, receipt.logs, 'EtherDividendDeposited'); console.log(` - Failed to claim ${web3.utils.fromWei(event._amount)} ${dividendsType} - to account ${event._payee}` +Dividend ${ event._dividendIndex} deposited` ); } } - - let successEvents = common.getMultipleEventsFromLogs(currentDividendsModule._jsonInterface, receipt.logs, successEventName); - for (const event of successEvents) { - console.log(` - Claimed ${web3.utils.fromWei(event._amount)} ${dividendsType} - to account ${event._payee} - ${web3.utils.fromWei(event._withheld)} ${dividendsType} of tax withheld` - ); - } } -async function reclaimedDividend(dividend) { - let reclaimDividendAction = currentDividendsModule.methods.reclaimDividend(dividend.index); - let receipt = await common.sendTransaction(reclaimDividendAction); - let eventName; - if (dividendsType == 'POLY') { - eventName = 'ERC20DividendReclaimed'; - } else if (dividendsType == 'ETH') { - eventName = 'EtherDividendReclaimed'; +function showInvestors(investorsArray, claimedArray, excludedArray) { + let dataTable = [['Investor', 'Has claimed', 'Is excluded']]; + for (let i = 0; i < investorsArray.length; i++) { + dataTable.push([ + investorsArray[i], + claimedArray[i] ? 'YES' : 'NO', + excludedArray[i] ? 'YES' : 'NO' + ]); } - let event = common.getEventFromLogs(currentDividendsModule._jsonInterface, receipt.logs, eventName); - console.log(` - Reclaimed Amount ${web3.utils.fromWei(event._claimedAmount)} ${dividendsType} - to account ${event._claimer}` - ); + console.log(); + console.log(table(dataTable)); } -async function whitelistAddress(address) { - let now = Math.floor(Date.now() / 1000); - let modifyWhitelistAction = generalTransferManager.methods.modifyWhitelist(address, now, now, now + 31536000, true); - await common.sendTransaction(modifyWhitelistAction); - console.log(chalk.green(`\nWhitelisting successful for ${address}.`)); +function showReport(_name, _tokenSymbol, _amount, _witthheld, _claimed, _investorArray, _claimedArray, _excludedArray, _withheldArray, _amountArray) { + let title = `${_name.toUpperCase()} DIVIDEND REPORT`; + let dataTable = + [[ + 'Investor', + 'Amount sent', + 'Taxes withheld (%)', + `Taxes withheld (${_tokenSymbol})`, + 'Amount received', + 'Withdrawn' + ]]; + for (let i = 0; i < _investorArray.length; i++) { + let investor = _investorArray[i]; + let excluded = _excludedArray[i]; + let withdrawn = _claimedArray[i] ? 'YES' : 'NO'; + let amount = !excluded ? web3.utils.fromWei(web3.utils.toBN(_amountArray[i]).add(web3.utils.toBN(_withheldArray[i]))) : 0; + let withheld = !excluded ? web3.utils.fromWei(_withheldArray[i]) : 'NA'; + let withheldPercentage = (!excluded) ? (withheld !== '0' ? parseFloat(withheld) / parseFloat(amount) * 100 : 0) : 'NA'; + let received = !excluded ? web3.utils.fromWei(_amountArray[i]) : 0; + dataTable.push([ + investor, + amount, + withheldPercentage, + withheld, + received, + withdrawn + ]); + } + console.log(chalk.yellow(`-----------------------------------------------------------------------------------------------------------------------------------------------------------`)); + console.log(title.padStart((50 - title.length) / 2, '*').padEnd((50 - title.length) / 2, '*')); + console.log(); + console.log(`- Total amount of dividends sent: ${web3.utils.fromWei(_amount)} ${_tokenSymbol} `); + console.log(`- Total amount of taxes withheld: ${web3.utils.fromWei(_witthheld)} ${_tokenSymbol} `); + console.log(`- Total amount of dividends distributed: ${web3.utils.fromWei(_claimed)} ${_tokenSymbol} `); + console.log(`- Total amount of investors: ${_investorArray.length} `); + console.log(); + console.log(table(dataTable)); + console.log(); + console.log(chalk.yellow(`NOTE: If investor has not claimed the dividend yet, TAX and AMOUNT are calculated with the current values set and they might change.`)); + console.log(chalk.yellow(`-----------------------------------------------------------------------------------------------------------------------------------------------------------`)); + console.log(); } -// Helper functions -async function getBalance(address) { - let balance; - if (dividendsType == 'POLY') { - balance = (await polyToken.methods.balanceOf(address).call()).toString(); - } else if (dividendsType == 'ETH') { - balance = (await web3.eth.getBalance(address)).toString(); - } - - return balance; -} -async function checkBalance(_dividend) { - let issuerBalance = await getBalance(Issuer.address); - if (parseInt(web3.utils.fromWei(issuerBalance)) < parseInt(_dividend)) { - console.log(chalk.red(` - You have ${web3.utils.fromWei(issuerBalance)} ${dividendsType} need ${(parseInt(_dividend) - parseInt(web3.utils.fromWei(issuerBalance)))} more ${dividendsType} - `)); - process.exit(0); +async function pushDividends(dividendIndex, checkpointId) { + let accounts = readlineSync.question('Enter addresses to push dividends to (ex- add1,add2,add3,...) or leave empty to push to all addresses: ', { + limit: function (input) { + return input === '' || (input.split(',').every(a => web3.utils.isAddress(a))); + }, + limitMessage: `All addresses must be valid` + }).split(','); + if (accounts[0] !== '') { + let action = currentDividendsModule.methods.pushDividendPaymentToAddresses(dividendIndex, accounts); + let receipt = await common.sendTransaction(action); + logPushResults(receipt); + } else { + let investorsAtCheckpoint = await securityToken.methods.getInvestorsAt(checkpointId).call(); + console.log(`There are ${investorsAtCheckpoint.length} investors at checkpoint ${checkpointId} `); + let batchSize = readlineSync.questionInt(`How many investors per transaction do you want to push to? `); + for (let i = 0; i < investorsAtCheckpoint.length; i += batchSize) { + let action = currentDividendsModule.methods.pushDividendPayment(dividendIndex, i, batchSize); + let receipt = await common.sendTransaction(action); + logPushResults(receipt); + } } } -async function isDividendsModuleAttached() { - let dividendsModuleName; - if (dividendsType == 'POLY') { - dividendsModuleName = 'ERC20DividendCheckpoint'; - } else if (dividendsType == 'ETH') { - dividendsModuleName = 'EtherDividendCheckpoint'; - } +async function exploreAccount(dividendIndex, dividendTokenAddress, dividendTokenSymbol) { + let account = readlineSync.question('Enter address to explore: ', { + limit: function (input) { + return web3.utils.isAddress(input); + }, + limitMessage: "Must be a valid address", + }); + let isExcluded = await currentDividendsModule.methods.isExcluded(account, dividendIndex).call(); + let hasClaimed = await currentDividendsModule.methods.isClaimed(account, dividendIndex).call(); + let dividendAmounts = await currentDividendsModule.methods.calculateDividend(dividendIndex, account).call(); + let dividendBalance = dividendAmounts[0]; + let dividendTax = dividendAmounts[1]; + let dividendTokenBalance = await getBalance(account, dividendTokenAddress); + let securityTokenBalance = await getBalance(account, securityToken.options.address); - let result = await securityToken.methods.getModulesByName(web3.utils.toHex(dividendsModuleName)).call(); - if (result.length > 0) { - let dividendsModuleAddress = result[0]; - let dividendsModuleABI; - if (dividendsType == 'POLY') { - dividendsModuleABI = abis.erc20DividendCheckpoint(); - } else if (dividendsType == 'ETH') { - dividendsModuleABI = abis.etherDividendCheckpoint(); + console.log(); + console.log(`Security token balance: ${web3.utils.fromWei(securityTokenBalance)} ${tokenSymbol} `); + console.log(`Dividend token balance: ${web3.utils.fromWei(dividendTokenBalance)} ${dividendTokenSymbol} `); + console.log(`Is excluded: ${isExcluded ? 'YES' : 'NO'} `); + if (!isExcluded) { + console.log(`Has claimed: ${hasClaimed ? 'YES' : 'NO'} `); + if (!hasClaimed) { + console.log(`Dividends available: ${web3.utils.fromWei(dividendBalance)} ${dividendTokenSymbol} `); + console.log(`Tax withheld: ${web3.utils.fromWei(dividendTax)} ${dividendTokenSymbol} `); } - currentDividendsModule = new web3.eth.Contract(dividendsModuleABI, dividendsModuleAddress); - currentDividendsModule.setProvider(web3.currentProvider); } + console.log(); +} + +async function withdrawWithholding(dividendIndex, dividendTokenSymbol) { + let action = currentDividendsModule.methods.withdrawWithholding(dividendIndex); + let receipt = await common.sendTransaction(action); + let eventName = dividendsType === 'ERC20' ? 'ERC20DividendWithholdingWithdrawn' : 'EtherDividendWithholdingWithdrawn'; + let event = common.getEventFromLogs(currentDividendsModule._jsonInterface, receipt.logs, eventName); + console.log(chalk.green(`Successfully withdrew ${web3.utils.fromWei(event._withheldAmount)} ${dividendTokenSymbol} from dividend ${event._dividendIndex} tax withholding.`)); +} - return (typeof currentDividendsModule !== 'undefined'); +async function reclaimedDividend(dividendIndex, dividendTokenSymbol) { + let action = currentDividendsModule.methods.reclaimDividend(dividendIndex); + let receipt = await common.sendTransaction(action); + let eventName = dividendsType === 'ERC20' ? 'ERC20DividendReclaimed' : 'EtherDividendReclaimed'; + let event = common.getEventFromLogs(currentDividendsModule._jsonInterface, receipt.logs, eventName); + console.log(` +Reclaimed amount ${ web3.utils.fromWei(event._claimedAmount)} ${dividendTokenSymbol} +to account ${ event._claimer} ` + ); } async function addDividendsModule() { - if (!(await isDividendsModuleAttached())) { - let dividendsFactoryName; - let dividendsModuleABI; - if (dividendsType == 'POLY') { - dividendsFactoryName = 'ERC20DividendCheckpoint'; - dividendsModuleABI = abis.erc20DividendCheckpoint(); - } else if (dividendsType == 'ETH') { - dividendsFactoryName = 'EtherDividendCheckpoint'; - dividendsModuleABI = abis.etherDividendCheckpoint(); - } + let availableModules = await moduleRegistry.methods.getModulesByTypeAndToken(gbl.constants.MODULES_TYPES.DIVIDENDS, securityToken.options.address).call(); + let options = await Promise.all(availableModules.map(async function (m) { + let moduleFactoryABI = abis.moduleFactory(); + let moduleFactory = new web3.eth.Contract(moduleFactoryABI, m); + return web3.utils.hexToUtf8(await moduleFactory.methods.name().call()); + })); + + let index = readlineSync.keyInSelect(options, 'Which dividends module do you want to add? ', { cancel: 'Return' }); + if (index != -1 && readlineSync.keyInYNStrict(`Are you sure you want to add ${options[index]} module? `)) { + let wallet = readlineSync.question('Enter the account address to receive reclaimed dividends and tax: ', { + limit: function (input) { + return web3.utils.isAddress(input); + }, + limitMessage: "Must be a valid address", + }); + let configureFunction = abis.erc20DividendCheckpoint().find(o => o.name === 'configure' && o.type === 'function'); + let bytes = web3.eth.abi.encodeFunctionCall(configureFunction, [wallet]); - let dividendsFactoryAddress = await contracts.getModuleFactoryAddressByName(securityToken.options.address, gbl.constants.MODULES_TYPES.DIVIDENDS, dividendsFactoryName); - let addModuleAction = securityToken.methods.addModule(dividendsFactoryAddress, web3.utils.fromAscii('', 16), 0, 0); + let selectedDividendFactoryAddress = await contracts.getModuleFactoryAddressByName(securityToken.options.address, gbl.constants.MODULES_TYPES.DIVIDENDS, options[index]); + let addModuleAction = securityToken.methods.addModule(selectedDividendFactoryAddress, bytes, 0, 0); let receipt = await common.sendTransaction(addModuleAction); let event = common.getEventFromLogs(securityToken._jsonInterface, receipt.logs, 'ModuleAdded'); - console.log(`Module deployed at address: ${event._module}`); - currentDividendsModule = new web3.eth.Contract(dividendsModuleABI, event._module); - currentDividendsModule.setProvider(web3.currentProvider); + console.log(chalk.green(`Module deployed at address: ${event._module} `)); } } -async function selectCheckpoint(includeCreate) { - let options = []; - let fix = 1; //Checkpoint 0 is not included, so I need to add 1 to fit indexes for checkpoints and options - let checkpoints = (await getCheckpoints()).map(function(c) { return c.timestamp }); - if (includeCreate) { - options.push('Create new checkpoint'); - fix = 0; //If this option is added, fix isn't needed. +// Helper functions +async function getBalance(address, tokenAddress) { + if (tokenAddress !== gbl.constants.ADDRESS_ZERO) { + let token = new web3.eth.Contract(abis.erc20(), tokenAddress); + return await token.methods.balanceOf(address).call(); + } else { + return await web3.eth.getBalance(address); + } +} + +function logPushResults(receipt) { + let successEventName; + if (dividendsType == 'ERC20') { + successEventName = 'ERC20DividendClaimed'; } - options = options.concat(checkpoints); + else if (dividendsType == 'ETH') { + successEventName = 'EtherDividendClaimed'; + let failedEventName = 'EtherDividendClaimFailed'; + let failedEvents = common.getMultipleEventsFromLogs(currentDividendsModule._jsonInterface, receipt.logs, failedEventName); + for (const event of failedEvents) { + console.log(chalk.red(`Failed to claim ${web3.utils.fromWei(event._amount)} ${dividendsType} to account ${event._payee} `, '\n')); + } + } + let successEvents = common.getMultipleEventsFromLogs(currentDividendsModule._jsonInterface, receipt.logs, successEventName); + for (const event of successEvents) { + console.log(chalk.green(` Claimed ${web3.utils.fromWei(event._amount)} ${dividendsType} +to account ${ event._payee} +${ web3.utils.fromWei(event._withheld)} ${dividendsType} of tax withheld`, '\n')); + } +} + +async function selectCheckpoint(includeCreate) { + if (await securityToken.methods.currentCheckpointId().call() > 0) { + let options = []; + let fix = 1; //Checkpoint 0 is not included, so I need to add 1 to fit indexes for checkpoints and options + let checkpoints = (await getCheckpoints()).map(function (c) { return c.timestamp }); + if (includeCreate) { + options.push('Create new checkpoint'); + fix = 0; //If this option is added, fix isn't needed. + } + options = options.concat(checkpoints); - return readlineSync.keyInSelect(options, 'Select a checkpoint:', {cancel: false}) + fix; + return readlineSync.keyInSelect(options, 'Select a checkpoint:', { cancel: false }) + fix; + } else { + return 0; + } } async function getCheckpoints() { let result = []; - + let checkPointsTimestamps = await securityToken.methods.getCheckpointTimes().call(); for (let index = 0; index < checkPointsTimestamps.length; index++) { let checkpoint = {}; @@ -498,91 +673,217 @@ async function getCheckpoints() { return result.sort((a, b) => a.id - b.id); } -async function selectDividend(filter) { - let result = null; - let dividends = await getDividends(); +function isValidDividend(dividend) { + let now = Math.floor(Date.now() / 1000); + return now > dividend.maturity; +} - let now = Math.floor(Date.now()/1000); - if (typeof filter !== 'undefined') { - if (typeof filter.valid !== 'undefined') { - dividends = dividends.filter(d => filter.valid == (now > d.maturity)); - } - if (typeof filter.expired !== 'undefined') { - dividends = dividends.filter(d => filter.expired == (d.expiry < now)); - } - if (typeof filter.reclaimed !== 'undefined') { - dividends = dividends.filter(d => filter.reclaimed == d.reclaimed); - } - if (typeof filter.withRemainingWithheld !== 'undefined') { - dividends = dividends.filter(d => new web3.utils.BN(d.dividendWithheld).sub(new web3.utils.BN(d.dividendWithheldReclaimed)) > 0); - } - if (typeof filter.withRemaining !== 'undefined') { - dividends = dividends.filter(d => new web3.utils.BN(d.amount).sub(new web3.utils.BN(d.claimedAmount)) > 0); - } - } +function isExpiredDividend(dividend) { + let now = Math.floor(Date.now() / 1000); + return now > dividend.expiry; +} + +function hasRemaining(dividend) { + return Number(new web3.utils.BN(dividend.amount).sub(new web3.utils.BN(dividend.claimedAmount))).toFixed(10) > 0; +} + +function hasRemainingWithheld(dividend) { + return Number(new web3.utils.BN(dividend.dividendWithheld).sub(new web3.utils.BN(dividend.dividendWithheldReclaimed))).toFixed(10) > 0; +} - if (dividends.length > 0) { - let options = dividends.map(function(d) { - return `${web3.utils.toAscii(d.name)} +async function selectDividend(dividends) { + let result = null; + let options = dividends.map(function (d) { + return `${d.name} + Amount: ${web3.utils.fromWei(d.amount)} ${d.tokenSymbol} + Status: ${isExpiredDividend(d) ? 'Expired' : hasRemaining(d) ? 'In progress' : 'Completed'} + Token: ${d.tokenSymbol} Created: ${moment.unix(d.created).format('MMMM Do YYYY, HH:mm:ss')} - Maturity: ${moment.unix(d.maturity).format('MMMM Do YYYY, HH:mm:ss')} - Expiry: ${moment.unix(d.expiry).format('MMMM Do YYYY, HH:mm:ss')} - At checkpoint: ${d.checkpointId} - Amount: ${web3.utils.fromWei(d.amount)} ${dividendsType} - Claimed Amount: ${web3.utils.fromWei(d.claimedAmount)} ${dividendsType} - Withheld: ${web3.utils.fromWei(d.dividendWithheld)} ${dividendsType} - Withheld claimed: ${web3.utils.fromWei(d.dividendWithheldReclaimed)} ${dividendsType}` - }); + Expiry: ${moment.unix(d.expiry).format('MMMM Do YYYY, HH:mm:ss')} ` + }); - let index = readlineSync.keyInSelect(options, 'Select a dividend:'); - if (index != -1) { - result = dividends[index]; - } - } else { - console.log(chalk.red(`No dividends were found meeting the requirements`)) - console.log(chalk.red(`Requirements: Valid: ${filter.valid} - Expired: ${filter.expired} - Reclaimed: ${filter.reclaimed} - WithRemainingWithheld: ${filter.withRemainingWithheld} - WithRemaining: ${filter.withRemaining}\n`)) + let index = readlineSync.keyInSelect(options, 'Select a dividend:', { cancel: 'RETURN' }); + if (index != -1) { + result = dividends[index]; } return result; } async function getDividends() { - let result = []; + function DividendData(_index, _created, _maturity, _expiry, _amount, _claimedAmount, _name, _tokenSymbol) { + this.index = _index; + this.created = _created; + this.maturity = _maturity; + this.expiry = _expiry; + this.amount = _amount; + this.claimedAmount = _claimedAmount; + this.name = _name; + this.tokenSymbol = _tokenSymbol; + } - let currentCheckpoint = await securityToken.methods.currentCheckpointId().call(); - for (let index = 1; index <= currentCheckpoint; index++) { - let dividendIndexes = await currentDividendsModule.methods.getDividendIndex(index).call(); - for (const i of dividendIndexes) { - let dividend = await currentDividendsModule.methods.dividends(i).call(); - dividend.index = i; - result.push(dividend); + let dividends = []; + let dividendsData = await currentDividendsModule.methods.getDividendsData().call(); + let createdArray = dividendsData.createds; + let maturityArray = dividendsData.maturitys; + let expiryArray = dividendsData.expirys; + let amountArray = dividendsData.amounts; + let claimedAmountArray = dividendsData.claimedAmounts; + let nameArray = dividendsData.names; + for (let i = 0; i < nameArray.length; i++) { + let tokenSymbol = 'ETH'; + if (dividendsType === 'ERC20') { + let tokenAddress = await currentDividendsModule.methods.dividendTokens(i).call(); + let erc20token = new web3.eth.Contract(abis.erc20(), tokenAddress); + tokenSymbol = await erc20token.methods.symbol().call(); } + dividends.push( + new DividendData( + i, + createdArray[i], + maturityArray[i], + expiryArray[i], + amountArray[i], + claimedAmountArray[i], + web3.utils.hexToUtf8(nameArray[i]), + tokenSymbol + ) + ); } - return result; + return dividends; } function getExcludedFromDataFile() { - let excludedFromFile = require('fs').readFileSync('./CLI/data/dividendsExclusions_data.csv').toString().split("\n"); - let excluded = excludedFromFile.filter(function (address) { - return web3.utils.isAddress(address); - }); - return excluded; + let parsedData = csvParse(EXCLUSIONS_DATA_CSV); + let validData = parsedData.filter(row => web3.utils.isAddress(row[0])); + let invalidRows = parsedData.filter(row => !validData.includes(row)); + if (invalidRows.length > 0) { + console.log(chalk.red(`The following lines from csv file are not valid: ${invalidRows.map(r => parsedData.indexOf(r) + 1).join(',')} `)); + } + let batches = common.splitIntoBatches(validData, validData.length); + let [data] = common.transposeBatches(batches); + + return data; } function showExcluded(excluded) { - if (excluded.length > 0) { - console.log('Current default excluded addresses:') - excluded.map(function (address) { console.log(' ', address) }); + console.log('Current default excluded addresses:') + excluded.map(address => console.log(address)); + console.log(); +} + +async function getAllModulesByType(type) { + function ModuleInfo(_moduleType, _name, _address, _factoryAddress, _archived, _paused) { + this.name = _name; + this.type = _moduleType; + this.address = _address; + this.factoryAddress = _factoryAddress; + this.archived = _archived; + this.paused = _paused; + } + + let modules = []; + + let allModules = await securityToken.methods.getModulesByType(type).call(); + + for (let i = 0; i < allModules.length; i++) { + let details = await securityToken.methods.getModule(allModules[i]).call(); + let nameTemp = web3.utils.hexToUtf8(details[0]); + let pausedTemp = null; + if (type == gbl.constants.MODULES_TYPES.STO || type == gbl.constants.MODULES_TYPES.TRANSFER) { + let abiTemp = JSON.parse(require('fs').readFileSync(`${__dirname} /../../ build / contracts / ${nameTemp}.json`).toString()).abi; + let contractTemp = new web3.eth.Contract(abiTemp, details[1]); + pausedTemp = await contractTemp.methods.paused().call(); + } + modules.push(new ModuleInfo(type, nameTemp, details[1], details[2], details[3], pausedTemp)); + } + + return modules; +} + +async function initialize(_tokenSymbol) { + welcome(); + await setup(); + if (typeof _tokenSymbol === 'undefined') { + tokenSymbol = await selectToken(); } else { - console.log('There are not default excluded addresses.') + tokenSymbol = _tokenSymbol; } - console.log(); + let securityTokenAddress = await securityTokenRegistry.methods.getSecurityTokenAddress(tokenSymbol).call(); + if (securityTokenAddress == '0x0000000000000000000000000000000000000000') { + console.log(chalk.red(`Selected Security Token ${tokenSymbol} does not exist.`)); + process.exit(0); + } + let securityTokenABI = abis.securityToken(); + securityToken = new web3.eth.Contract(securityTokenABI, securityTokenAddress); + securityToken.setProvider(web3.currentProvider); +} + +function welcome() { + common.logAsciiBull(); + console.log("**********************************************"); + console.log("Welcome to the Command-Line Dividends Manager."); + console.log("**********************************************"); + console.log("Issuer Account: " + Issuer.address + "\n"); +} + +async function setup() { + try { + let securityTokenRegistryAddress = await contracts.securityTokenRegistry(); + let securityTokenRegistryABI = abis.securityTokenRegistry(); + securityTokenRegistry = new web3.eth.Contract(securityTokenRegistryABI, securityTokenRegistryAddress); + securityTokenRegistry.setProvider(web3.currentProvider); + + let polyTokenAddress = await contracts.polyToken(); + let polyTokenABI = abis.polyToken(); + polyToken = new web3.eth.Contract(polyTokenABI, polyTokenAddress); + polyToken.setProvider(web3.currentProvider); + + let moduleRegistryAddress = await contracts.moduleRegistry(); + let moduleRegistryABI = abis.moduleRegistry(); + moduleRegistry = new web3.eth.Contract(moduleRegistryABI, moduleRegistryAddress); + moduleRegistry.setProvider(web3.currentProvider); + } catch (err) { + console.log(err) + console.log('\x1b[31m%s\x1b[0m', "There was a problem getting the contracts. Make sure they are deployed to the selected network."); + process.exit(0); + } +} + +async function selectToken() { + let result = null; + + let userTokens = await securityTokenRegistry.methods.getTokensByOwner(Issuer.address).call(); + let tokenDataArray = await Promise.all(userTokens.map(async function (t) { + let tokenData = await securityTokenRegistry.methods.getSecurityTokenData(t).call(); + return { symbol: tokenData[0], address: t }; + })); + let options = tokenDataArray.map(function (t) { + return `${t.symbol} - Deployed at ${t.address} `; + }); + options.push('Enter token symbol manually'); + + let index = readlineSync.keyInSelect(options, 'Select a token:', { cancel: 'Exit' }); + let selected = index != -1 ? options[index] : 'Exit'; + switch (selected) { + case 'Enter token symbol manually': + result = readlineSync.question('Enter the token symbol: '); + break; + case 'Exit': + process.exit(); + break; + default: + result = tokenDataArray[index].symbol; + break; + } + + return result; } module.exports = { - executeApp: async function(type) { - return executeApp(type); + executeApp: async function (_tokenSymbol) { + await initialize(_tokenSymbol); + return executeApp(); } } diff --git a/CLI/commands/helpers/contract_abis.js b/CLI/commands/helpers/contract_abis.js index 8a551b70b..b6abc7316 100644 --- a/CLI/commands/helpers/contract_abis.js +++ b/CLI/commands/helpers/contract_abis.js @@ -8,8 +8,11 @@ let cappedSTOABI; let usdTieredSTOABI; let generalTransferManagerABI; let manualApprovalTransferManagerABI; +let blacklistTransferManagerABI; let countTransferManagerABI; let percentageTransferManagerABI; +let lockUpTransferManagerABI; +let volumeRestrictionTMABI; let generalPermissionManagerABI; let polyTokenABI; let cappedSTOFactoryABI; @@ -21,31 +24,36 @@ let ownableABI; let iSTOABI; let iTransferManagerABI; let moduleFactoryABI; +let erc20ABI; try { - polymathRegistryABI = JSON.parse(require('fs').readFileSync('./build/contracts/PolymathRegistry.json').toString()).abi; - securityTokenRegistryABI = JSON.parse(require('fs').readFileSync('./build/contracts/SecurityTokenRegistry.json').toString()).abi; - featureRegistryABI = JSON.parse(require('fs').readFileSync('./build/contracts/FeatureRegistry.json').toString()).abi; - moduleRegistryABI = JSON.parse(require('fs').readFileSync('./build/contracts/ModuleRegistry.json').toString()).abi; - securityTokenABI = JSON.parse(require('fs').readFileSync('./build/contracts/SecurityToken.json').toString()).abi; - stoInterfaceABI = JSON.parse(require('fs').readFileSync('./build/contracts/ISTO.json').toString()).abi; - cappedSTOABI = JSON.parse(require('fs').readFileSync('./build/contracts/CappedSTO.json').toString()).abi; - usdTieredSTOABI = JSON.parse(require('fs').readFileSync('./build/contracts/USDTieredSTO.json').toString()).abi; - generalTransferManagerABI = JSON.parse(require('fs').readFileSync('./build/contracts/GeneralTransferManager.json').toString()).abi; - manualApprovalTransferManagerABI = JSON.parse(require('fs').readFileSync('./build/contracts/ManualApprovalTransferManager.json').toString()).abi; - countTransferManagerABI = JSON.parse(require('fs').readFileSync('./build/contracts/CountTransferManager.json').toString()).abi; - percentageTransferManagerABI = JSON.parse(require('fs').readFileSync('./build/contracts/PercentageTransferManager.json').toString()).abi; - generalPermissionManagerABI = JSON.parse(require('fs').readFileSync('./build/contracts/GeneralPermissionManager.json').toString()).abi; - polyTokenABI = JSON.parse(require('fs').readFileSync('./build/contracts/PolyTokenFaucet.json').toString()).abi; - cappedSTOFactoryABI = JSON.parse(require('fs').readFileSync('./build/contracts/CappedSTOFactory.json').toString()).abi; - usdTieredSTOFactoryABI = JSON.parse(require('fs').readFileSync('./build/contracts/USDTieredSTOFactory.json').toString()).abi; - erc20DividendCheckpointABI = JSON.parse(require('fs').readFileSync('./build/contracts/ERC20DividendCheckpoint.json').toString()).abi; - etherDividendCheckpointABI = JSON.parse(require('fs').readFileSync('./build/contracts/EtherDividendCheckpoint.json').toString()).abi; - moduleInterfaceABI = JSON.parse(require('fs').readFileSync('./build/contracts/IModule.json').toString()).abi; - ownableABI = JSON.parse(require('fs').readFileSync('./build/contracts/Ownable.json').toString()).abi; - iSTOABI = JSON.parse(require('fs').readFileSync('./build/contracts/ISTO.json').toString()).abi - iTransferManagerABI = JSON.parse(require('fs').readFileSync('./build/contracts/ITransferManager.json').toString()).abi - moduleFactoryABI = JSON.parse(require('fs').readFileSync('./build/contracts/ModuleFactory.json').toString()).abi; + polymathRegistryABI = JSON.parse(require('fs').readFileSync(`${__dirname}/../../../build/contracts/PolymathRegistry.json`).toString()).abi; + securityTokenRegistryABI = JSON.parse(require('fs').readFileSync(`${__dirname}/../../../build/contracts/SecurityTokenRegistry.json`).toString()).abi; + featureRegistryABI = JSON.parse(require('fs').readFileSync(`${__dirname}/../../../build/contracts/FeatureRegistry.json`).toString()).abi; + moduleRegistryABI = JSON.parse(require('fs').readFileSync(`${__dirname}/../../../build/contracts/ModuleRegistry.json`).toString()).abi; + securityTokenABI = JSON.parse(require('fs').readFileSync(`${__dirname}/../../../build/contracts/SecurityToken.json`).toString()).abi; + stoInterfaceABI = JSON.parse(require('fs').readFileSync(`${__dirname}/../../../build/contracts/ISTO.json`).toString()).abi; + cappedSTOABI = JSON.parse(require('fs').readFileSync(`${__dirname}/../../../build/contracts/CappedSTO.json`).toString()).abi; + usdTieredSTOABI = JSON.parse(require('fs').readFileSync(`${__dirname}/../../../build/contracts/USDTieredSTO.json`).toString()).abi; + generalTransferManagerABI = JSON.parse(require('fs').readFileSync(`${__dirname}/../../../build/contracts/GeneralTransferManager.json`).toString()).abi; + manualApprovalTransferManagerABI = JSON.parse(require('fs').readFileSync(`${__dirname}/../../../build/contracts/ManualApprovalTransferManager.json`).toString()).abi; + countTransferManagerABI = JSON.parse(require('fs').readFileSync(`${__dirname}/../../../build/contracts/CountTransferManager.json`).toString()).abi; + percentageTransferManagerABI = JSON.parse(require('fs').readFileSync(`${__dirname}/../../../build/contracts/PercentageTransferManager.json`).toString()).abi; + blacklistTransferManagerABI = JSON.parse(require('fs').readFileSync(`${__dirname}/../../../build/contracts/BlacklistTransferManager.json`).toString()).abi; + volumeRestrictionTMABI = JSON.parse(require('fs').readFileSync(`${__dirname}/../../../build/contracts/VolumeRestrictionTM.json`).toString()).abi; + lockUpTransferManagerABI = JSON.parse(require('fs').readFileSync(`${__dirname}/../../../build/contracts/LockUpTransferManager.json`).toString()).abi; + generalPermissionManagerABI = JSON.parse(require('fs').readFileSync(`${__dirname}/../../../build/contracts/GeneralPermissionManager.json`).toString()).abi; + polyTokenABI = JSON.parse(require('fs').readFileSync(`${__dirname}/../../../build/contracts/PolyTokenFaucet.json`).toString()).abi; + cappedSTOFactoryABI = JSON.parse(require('fs').readFileSync(`${__dirname}/../../../build/contracts/CappedSTOFactory.json`).toString()).abi; + usdTieredSTOFactoryABI = JSON.parse(require('fs').readFileSync(`${__dirname}/../../../build/contracts/USDTieredSTOFactory.json`).toString()).abi; + erc20DividendCheckpointABI = JSON.parse(require('fs').readFileSync(`${__dirname}/../../../build/contracts/ERC20DividendCheckpoint.json`).toString()).abi; + etherDividendCheckpointABI = JSON.parse(require('fs').readFileSync(`${__dirname}/../../../build/contracts/EtherDividendCheckpoint.json`).toString()).abi; + moduleInterfaceABI = JSON.parse(require('fs').readFileSync(`${__dirname}/../../../build/contracts/IModule.json`).toString()).abi; + ownableABI = JSON.parse(require('fs').readFileSync(`${__dirname}/../../../build/contracts/Ownable.json`).toString()).abi; + iSTOABI = JSON.parse(require('fs').readFileSync(`${__dirname}/../../../build/contracts/ISTO.json`).toString()).abi + iTransferManagerABI = JSON.parse(require('fs').readFileSync(`${__dirname}/../../../build/contracts/ITransferManager.json`).toString()).abi + moduleFactoryABI = JSON.parse(require('fs').readFileSync(`${__dirname}/../../../build/contracts/ModuleFactory.json`).toString()).abi; + erc20ABI = JSON.parse(require('fs').readFileSync(`${__dirname}/../../../build/contracts/DetailedERC20.json`).toString()).abi; } catch (err) { console.log('\x1b[31m%s\x1b[0m', "Couldn't find contracts' artifacts. Make sure you ran truffle compile first"); throw err; @@ -82,12 +90,21 @@ module.exports = { manualApprovalTransferManager: function () { return manualApprovalTransferManagerABI; }, + blacklistTransferManager: function () { + return blacklistTransferManagerABI; + }, countTransferManager: function () { return countTransferManagerABI; }, percentageTransferManager: function () { return percentageTransferManagerABI; }, + lockUpTransferManager: function () { + return lockUpTransferManagerABI; + }, + volumeRestrictionTM: function () { + return volumeRestrictionTMABI; + }, generalPermissionManager: function () { return generalPermissionManagerABI; }, @@ -120,5 +137,8 @@ module.exports = { }, moduleFactory: function () { return moduleFactoryABI; + }, + erc20: function () { + return erc20ABI; } -} \ No newline at end of file +} diff --git a/CLI/commands/helpers/contract_addresses.js b/CLI/commands/helpers/contract_addresses.js index 59a2f39c4..79f5cba10 100644 --- a/CLI/commands/helpers/contract_addresses.js +++ b/CLI/commands/helpers/contract_addresses.js @@ -13,7 +13,7 @@ function getPolymathRegistryAddress(networkId) { result = ""; break; case 15: // GANACHE - result = JSON.parse(require('fs').readFileSync('./build/contracts/PolymathRegistry.json').toString()).networks[networkId].address; + result = JSON.parse(require('fs').readFileSync(`${__dirname}/../../../build/contracts/PolymathRegistry.json`).toString()).networks[networkId].address; break; case 42: // KOVAN result = "0x5b215a7d39ee305ad28da29bf2f0425c6c2a00b3"; @@ -52,35 +52,35 @@ module.exports = { let networkId = await web3.eth.net.getId(); return getPolymathRegistryAddress(networkId); }, - securityTokenRegistry: async function() { + securityTokenRegistry: async function () { let polymathRegistry = await getPolymathRegistry(); return await polymathRegistry.methods.getAddress("SecurityTokenRegistry").call(); }, - moduleRegistry: async function() { + moduleRegistry: async function () { let polymathRegistry = await getPolymathRegistry(); return await polymathRegistry.methods.getAddress("ModuleRegistry").call(); }, - featureRegistry: async function() { + featureRegistry: async function () { let polymathRegistry = await getPolymathRegistry(); return await polymathRegistry.methods.getAddress("FeatureRegistry").call(); }, - polyToken: async function() { + polyToken: async function () { let polymathRegistry = await getPolymathRegistry(); return await polymathRegistry.methods.getAddress("PolyToken").call(); }, - usdToken: async function() { + usdToken: async function () { let networkId = await web3.eth.net.getId(); if (networkId == 1) return "0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359"; else if (networkId == 42) return "0xc4375b7de8af5a38a93548eb8453a498222c4ff2"; else - return JSON.parse(require('fs').readFileSync('./build/contracts/PolyTokenFaucet.json').toString()).networks[networkId].address; + return JSON.parse(require('fs').readFileSync(`${__dirname}/../../../build/contracts/PolyTokenFaucet.json`).toString()).networks[networkId].address; }, - getModuleFactoryAddressByName: async function(stAddress, moduleType, moduleName) { + getModuleFactoryAddressByName: async function (stAddress, moduleType, moduleName) { let moduleRegistry = await getModuleRegistry(); let availableModules = await moduleRegistry.methods.getModulesByTypeAndToken(moduleType, stAddress).call(); - + let result = null; let counter = 0; let moduleFactoryABI = abis.moduleFactory(); diff --git a/CLI/commands/investor_portal.js b/CLI/commands/investor_portal.js index cdfa66828..d59fd4bac 100644 --- a/CLI/commands/investor_portal.js +++ b/CLI/commands/investor_portal.js @@ -9,12 +9,15 @@ var gbl = require('./common/global'); var contracts = require('./helpers/contract_addresses'); var abis = require('./helpers/contract_abis'); +const ETH = 'ETH'; +const POLY = 'POLY'; +const STABLE = 'STABLE'; + let securityTokenRegistry; let securityToken; let selectedSTO; let currentSTO; let polyToken; -let usdToken; let generalTransferManager; let raiseTypes = []; @@ -55,17 +58,18 @@ async function executeApp(investorAddress, investorPrivKey, symbol, currency, am try { await inputSymbol(symbol); - await showUserInfo(User.address); switch (selectedSTO) { case 'CappedSTO': let cappedSTOABI = abis.cappedSTO(); currentSTO = new web3.eth.Contract(cappedSTOABI, STOAddress); + await showUserInfo(User.address); await showCappedSTOInfo(); await investCappedSTO(currency, amount); break; case 'USDTieredSTO': let usdTieredSTOABI = abis.usdTieredSTO(); currentSTO = new web3.eth.Contract(usdTieredSTOABI, STOAddress); + await showUserInfo(User.address); await showUserInfoForUSDTieredSTO(); await showUSDTieredSTOInfo(); await investUsdTieredSTO(currency, amount) @@ -87,10 +91,6 @@ async function setup() { let polytokenABI = abis.polyToken(); polyToken = new web3.eth.Contract(polytokenABI, polytokenAddress); polyToken.setProvider(web3.currentProvider); - - let usdTokenAddress = await contracts.usdToken(); - usdToken = new web3.eth.Contract(polytokenABI, usdTokenAddress); - usdToken.setProvider(web3.currentProvider); } catch (err) { console.log(err); console.log(chalk.red(`There was a problem getting the contracts. Make sure they are deployed to the selected network.`)); @@ -152,17 +152,22 @@ async function showTokenInfo() { // Show info async function showUserInfo(_user) { + let listOfStableCoins = await currentSTO.methods.getUsdTokens().call(); + console.log(` ******************* User Information ******************** - Address: ${_user}`); - if (await currentSTO.methods.fundRaiseTypes(gbl.constants.FUND_RAISE_TYPES.POLY)) { + if (await currentSTO.methods.fundRaiseTypes(gbl.constants.FUND_RAISE_TYPES.POLY).call()) { console.log(` - POLY balance:\t ${await polyBalance(_user)}`); } - if (await currentSTO.methods.fundRaiseTypes(gbl.constants.FUND_RAISE_TYPES.ETH)) { + if (await currentSTO.methods.fundRaiseTypes(gbl.constants.FUND_RAISE_TYPES.ETH).call()) { console.log(` - ETH balance:\t ${web3.utils.fromWei(await web3.eth.getBalance(_user))}`); } - if (await currentSTO.methods.fundRaiseTypes(gbl.constants.FUND_RAISE_TYPES.DAI)) { - console.log(` - DAI balance:\t ${await usdBalance(_user)}`); + if (await currentSTO.methods.fundRaiseTypes(gbl.constants.FUND_RAISE_TYPES.STABLE).call()) { + let stableSymbolsAndBalance = await processAddressWithBalance(listOfStableCoins); + stableSymbolsAndBalance.forEach(stable => { + console.log(` - ${stable.symbol} balance:\t ${web3.utils.fromWei(stable.balance)}`); + }); } } @@ -235,16 +240,63 @@ async function showCappedSTOInfo() { } } +async function processAddressWithBalance(array) { + let list = []; + for (const address of array) { + let symbol = await checkSymbol(address); + let balance = await checkBalance(address); + list.push({ 'address': address, 'symbol': symbol, 'balance': balance }) + } + return list +} + +async function processAddress(array) { + let list = []; + for (const address of array) { + let symbol = await checkSymbol(address); + list.push({ "symbol": symbol, "address": address }) + } + return list +} + +async function checkSymbol(address) { + let stableCoin = common.connect(abis.erc20(), address); + try { + return await stableCoin.methods.symbol().call(); + } catch (e) { + return "" + } +} + +async function checkBalance(address) { + let stableCoin = common.connect(abis.erc20(), address); + try { + return await stableCoin.methods.balanceOf(User.address).call(); + } catch (e) { + return "" + } +} + async function showUserInfoForUSDTieredSTO() { + let stableSymbols = []; + let listOfStableCoins = await currentSTO.methods.getUsdTokens().call(); + for (const fundType in gbl.constants.FUND_RAISE_TYPES) { if (await currentSTO.methods.fundRaiseTypes(gbl.constants.FUND_RAISE_TYPES[fundType]).call()) { + if (fundType == STABLE) { + stableSymbols = await processAddress(listOfStableCoins) + } let displayInvestorInvested = web3.utils.fromWei(await currentSTO.methods.investorInvested(User.address, gbl.constants.FUND_RAISE_TYPES[fundType]).call()); - console.log(` - Invested in ${fundType}:\t ${displayInvestorInvested} ${fundType}`); + if ((fundType == STABLE) && (stableSymbols.length)) { + console.log(` - Invested in stable coin(s): ${displayInvestorInvested} USD`); + } else { + console.log(` - Invested in ${fundType}:\t ${displayInvestorInvested} ${fundType}`); + } } } let displayInvestorInvestedUSD = web3.utils.fromWei(await currentSTO.methods.investorInvestedUSD(User.address).call()); - console.log(` - Invested in USD: ${displayInvestorInvestedUSD} USD`); + console.log(` - Total invested in USD: ${displayInvestorInvestedUSD} USD`); await generalTransferManager.methods.whitelist(User.address).call({}, function (error, result) { displayCanBuy = result.canBuyFromSTO; @@ -253,11 +305,12 @@ async function showUserInfoForUSDTieredSTO() { console.log(` - Whitelisted: ${(displayCanBuy) ? 'YES' : 'NO'}`); console.log(` - Valid KYC: ${(displayValidKYC) ? 'YES' : 'NO'}`); - let displayIsUserAccredited = await currentSTO.methods.accredited(User.address).call(); + let investorData = await currentSTO.methods.investors(User.address).call(); + let displayIsUserAccredited = investorData.accredited == 1; console.log(` - Accredited: ${(displayIsUserAccredited) ? "YES" : "NO"}`) - if (!await currentSTO.methods.accredited(User.address).call()) { - let displayOverrideNonAccreditedLimitUSD = web3.utils.fromWei(await currentSTO.methods.nonAccreditedLimitUSDOverride(User.address).call()) + if (!displayIsUserAccredited) { + let displayOverrideNonAccreditedLimitUSD = web3.utils.fromWei(investorData.nonAccreditedLimitUSDOverride); let displayNonAccreditedLimitUSD = displayOverrideNonAccreditedLimitUSD != 0 ? displayOverrideNonAccreditedLimitUSD : web3.utils.fromWei(await currentSTO.methods.nonAccreditedLimitUSD().call()); let displayTokensRemainingAllocation = displayNonAccreditedLimitUSD - displayInvestorInvestedUSD; console.log(` - Remaining allocation: ${(displayTokensRemainingAllocation > 0 ? displayTokensRemainingAllocation : 0)} USD`); @@ -277,10 +330,15 @@ async function showUSDTieredSTOInfo() { let displayIsOpen = await currentSTO.methods.isOpen().call(); let displayTokenSymbol = await securityToken.methods.symbol().call(); let tiersLength = await currentSTO.methods.getNumberOfTiers().call(); + let stableSymbols = []; + let listOfStableCoins = await currentSTO.methods.getUsdTokens().call(); for (const fundType in gbl.constants.FUND_RAISE_TYPES) { if (await currentSTO.methods.fundRaiseTypes(gbl.constants.FUND_RAISE_TYPES[fundType]).call()) { raiseTypes.push(fundType); + if (fundType == STABLE) { + stableSymbols = await processAddress(listOfStableCoins) + } } } @@ -310,8 +368,13 @@ async function showUSDTieredSTOInfo() { let mintedPerTier = mintedPerTierPerRaiseType[gbl.constants.FUND_RAISE_TYPES[type]]; - displayMintedPerTierPerType += ` + if ((type == STABLE) && (stableSymbols.length)) { + displayMintedPerTierPerType += ` + Sold for stable coin(s): ${web3.utils.fromWei(mintedPerTier)} ${displayTokenSymbol} ${displayDiscountMinted}`; + } else { + displayMintedPerTierPerType += ` Sold for ${type}:\t\t ${web3.utils.fromWei(mintedPerTier)} ${displayTokenSymbol} ${displayDiscountMinted}`; + } } displayTiers += ` @@ -331,18 +394,34 @@ async function showUSDTieredSTOInfo() { let displayTokensSoldPerType = ''; for (const type of raiseTypes) { let fundsRaised = web3.utils.fromWei(await currentSTO.methods.fundsRaised(gbl.constants.FUND_RAISE_TYPES[type]).call()); - displayFundsRaisedPerType += ` + if ((type == STABLE) && (stableSymbols.length)) { + stableSymbols.forEach(async (stable) => { + let raised = await getStableCoinsRaised(currentSTO, stable.address); + displayFundsRaisedPerType += ` + ${stable.symbol}:\t\t\t ${web3.utils.fromWei(raised)} ${stable.symbol}`; + }) + } else { + displayFundsRaisedPerType += ` ${type}:\t\t\t ${fundsRaised} ${type}`; - + } //Only show sold per raise type is more than one are allowed if (raiseTypes.length > 1) { let tokensSoldPerType = web3.utils.fromWei(await currentSTO.methods.getTokensSoldFor(gbl.constants.FUND_RAISE_TYPES[type]).call()); - displayTokensSoldPerType += ` + if ((type == STABLE) && (stableSymbols.length)) { + displayTokensSoldPerType += ` + Sold for stable coin(s): ${tokensSoldPerType} ${displayTokenSymbol}`; + } else { + displayTokensSoldPerType += ` Sold for ${type}:\t\t ${tokensSoldPerType} ${displayTokenSymbol}`; + } } } let displayRaiseType = raiseTypes.join(' - '); + //If STO has stable coins, we list them one by one + if (stableSymbols.length) { + displayRaiseType = displayRaiseType.replace(STABLE, "") + `${stableSymbols.map((obj) => { return obj.symbol }).toString().replace(`,`, ` - `)}` + } let now = Math.floor(Date.now() / 1000); let timeTitle; @@ -375,7 +454,7 @@ async function showUSDTieredSTOInfo() { - Investor count: ${displayInvestorCount} - Funds Raised` + displayFundsRaisedPerType + ` - USD: ${displayFundsRaisedUSD} USD + Total USD: ${displayFundsRaisedUSD} USD `); if (!displayCanBuy) { @@ -393,6 +472,10 @@ async function showUSDTieredSTOInfo() { } } +async function getStableCoinsRaised(currentSTO, address) { + return await currentSTO.methods.stableCoinsRaised(address).call() +} + // Allow investor to buy tokens. async function investCappedSTO(currency, amount) { if (typeof currency !== 'undefined' && !raiseTypes.inlcudes(currency)) { @@ -439,7 +522,11 @@ async function investCappedSTO(currency, amount) { // Allow investor to buy tokens. async function investUsdTieredSTO(currency, amount) { + let listOfStableCoins = await currentSTO.methods.getUsdTokens().call(); + let stableSymbols = await processAddress(listOfStableCoins); + let raiseType; + if (typeof currency !== 'undefined') { if (!raiseTypes.inlcudes(currency)) { console.log(chalk.red(`${currency} is not allowed for current STO`)); @@ -450,14 +537,30 @@ async function investUsdTieredSTO(currency, amount) { } else { for (const type of raiseTypes) { let displayPrice = web3.utils.fromWei(await currentSTO.methods.convertToUSD(gbl.constants.FUND_RAISE_TYPES[type], web3.utils.toWei("1")).call()); - console.log(chalk.green(` Current ${type} price:\t\t ${displayPrice} USD`)); + if (!((type == STABLE) && (stableSymbols.length))) { + console.log(chalk.green(` Current ${type} price:\t\t ${displayPrice} USD`)); + } } if (raiseTypes.length > 1) { - let index = readlineSync.keyInSelect(raiseTypes, 'Choose one of the allowed raise types: ', { cancel: false }); - raiseType = raiseTypes[index]; + const stableIndex = raiseTypes.indexOf(STABLE); + if (stableIndex > -1) { + raiseTypes.splice(stableIndex, 1) + stableSymbols.forEach((stable) => { + raiseTypes.push(stable.symbol) + }) + } + raiseType = raiseTypes[selectToken('Choose one of the allowed raise types: ')]; } else { - raiseType = raiseTypes[0]; - console.log(''); + if (raiseTypes[0] == STABLE) { + raiseTypes.splice(raiseTypes.indexOf(STABLE), 1) + stableSymbols.forEach((stable) => { + raiseTypes.push(stable.symbol) + }) + raiseType = raiseTypes[selectToken('Choose one of the allowed stable coin(s): ')]; + } else { + raiseType = raiseTypes[0]; + console.log(''); + } } } @@ -465,7 +568,14 @@ async function investUsdTieredSTO(currency, amount) { if (typeof amount === 'undefined') { let investorInvestedUSD = web3.utils.fromWei(await currentSTO.methods.investorInvestedUSD(User.address).call()); let minimumInvestmentUSD = await currentSTO.methods.minimumInvestmentUSD().call(); - let minimumInvestmentRaiseType = await currentSTO.methods.convertFromUSD(gbl.constants.FUND_RAISE_TYPES[raiseType], minimumInvestmentUSD).call(); + let minimumInvestmentRaiseType; + + // if raiseType is different than ETH or POLY, we assume is STABLE + if ((raiseType != ETH) && (raiseType != POLY)) { + minimumInvestmentRaiseType = await currentSTO.methods.convertFromUSD(gbl.constants.FUND_RAISE_TYPES[STABLE], minimumInvestmentUSD).call(); + } else { + minimumInvestmentRaiseType = await currentSTO.methods.convertFromUSD(gbl.constants.FUND_RAISE_TYPES[raiseType], minimumInvestmentUSD).call(); + } cost = readlineSync.question(chalk.yellow(`Enter the amount of ${raiseType} you would like to invest or press 'Enter' to exit: `), { limit: function (input) { return investorInvestedUSD != 0 || parseInt(input) > parseInt(web3.utils.fromWei(minimumInvestmentRaiseType)); @@ -479,7 +589,14 @@ async function investUsdTieredSTO(currency, amount) { let costWei = web3.utils.toWei(cost.toString()); - let tokensToBuy = await currentSTO.methods.buyTokensView(User.address, costWei, gbl.constants.FUND_RAISE_TYPES[raiseType]).call(); + let tokensToBuy; + // if raiseType is different than ETH or POLY, we assume is STABLE + if ((raiseType != ETH) && (raiseType != POLY)) { + tokensToBuy = await currentSTO.methods.buyTokensView(User.address, costWei, gbl.constants.FUND_RAISE_TYPES[STABLE]).call(); + } else { + tokensToBuy = await currentSTO.methods.buyTokensView(User.address, costWei, gbl.constants.FUND_RAISE_TYPES[raiseType]).call(); + } + let minTokenToBuy = tokensToBuy.tokensMinted; console.log(chalk.yellow(`You are going to spend ${web3.utils.fromWei(tokensToBuy.spentValue)} ${raiseType} (${web3.utils.fromWei(tokensToBuy.spentUSD)} USD) to buy ${web3.utils.fromWei(minTokenToBuy)} ${STSymbol} approx.`)); console.log(chalk.yellow(`Due to ${raiseType} price changes and network delays, it is possible that the final amount of purchased tokens is lower.`)); @@ -487,7 +604,7 @@ async function investUsdTieredSTO(currency, amount) { minTokenToBuy = 0; } - if (raiseType == 'POLY') { + if (raiseType == POLY) { let userBalance = await polyBalance(User.address); if (parseInt(userBalance) >= parseInt(cost)) { let allowance = await polyToken.methods.allowance(STOAddress, User.address).call(); @@ -503,19 +620,24 @@ async function investUsdTieredSTO(currency, amount) { console.log(chalk.red(`Please purchase a smaller amount of tokens or access the POLY faucet to get the POLY to complete this txn.`)); process.exit(); } - } else if (raiseType == 'DAI') { - let userBalance = await usdBalance(User.address); - if (parseInt(userBalance) >= parseInt(cost)) { - let allowance = await usdToken.methods.allowance(STOAddress, User.address).call(); + } else if ((raiseType != POLY) && (raiseType != ETH)) { + + let listOfStableCoins = await currentSTO.methods.getUsdTokens().call(); + let stableSymbolsAndBalance = await processAddressWithBalance(listOfStableCoins); + let stableInfo = stableSymbolsAndBalance.find(o => o.symbol === raiseType); + + if (parseInt(stableInfo.balance) >= parseInt(cost)) { + let stableCoin = common.connect(abis.erc20(), stableInfo.address); + let allowance = await stableCoin.methods.allowance(STOAddress, User.address).call(); if (allowance < costWei) { - let approveAction = usdToken.methods.approve(STOAddress, costWei); + let approveAction = stableCoin.methods.approve(STOAddress, costWei); await common.sendTransaction(approveAction, { from: User }); } - let actionBuyWithUSD = currentSTO.methods.buyWithUSDRateLimited(User.address, costWei, minTokenToBuy); + let actionBuyWithUSD = currentSTO.methods.buyWithUSDRateLimited(User.address, costWei, minTokenToBuy, stableInfo.address); let receipt = await common.sendTransaction(actionBuyWithUSD, { from: User, factor: 1.5 }); logTokensPurchasedUSDTieredSTO(receipt); } else { - console.log(chalk.red(`Not enough balance to Buy tokens, Require ${cost} DAI but have ${userBalance} DAI.`)); + console.log(chalk.red(`Not enough balance to Buy tokens, Require ${cost} ${stableInfo.symbol} but have ${stableInfo.balance} ${stableInfo.symbol}.`)); console.log(chalk.red(`Please purchase a smaller amount of tokens.`)); process.exit(); } @@ -529,13 +651,12 @@ async function investUsdTieredSTO(currency, amount) { await showUserInfoForUSDTieredSTO(); } -async function polyBalance(_user) { - let balance = await polyToken.methods.balanceOf(_user).call(); - return web3.utils.fromWei(balance); +function selectToken(msg) { + return readlineSync.keyInSelect(raiseTypes, msg, { cancel: false }); } -async function usdBalance(_user) { - let balance = await usdToken.methods.balanceOf(_user).call(); +async function polyBalance(_user) { + let balance = await polyToken.methods.balanceOf(_user).call(); return web3.utils.fromWei(balance); } diff --git a/CLI/commands/sto_manager.js b/CLI/commands/sto_manager.js index 4f19df76b..f581b7339 100644 --- a/CLI/commands/sto_manager.js +++ b/CLI/commands/sto_manager.js @@ -5,11 +5,13 @@ const abis = require('./helpers/contract_abis'); const common = require('./common/common_functions'); const gbl = require('./common/global'); const csvParse = require('./helpers/csv'); +const { table } = require('table'); +const STABLE = 'STABLE'; /////////////////// // Constants -const ACCREDIT_DATA_CSV = './CLI/data/STO/USDTieredSTO/accredited_data.csv'; -const NON_ACCREDIT_LIMIT_DATA_CSV = './CLI/data/STO/USDTieredSTO/nonAccreditedLimits_data.csv' +const ACCREDIT_DATA_CSV = `${__dirname}/../data/STO/USDTieredSTO/accredited_data.csv`; +const NON_ACCREDIT_LIMIT_DATA_CSV = `${__dirname}/../data/STO/USDTieredSTO/nonAccreditedLimits_data.csv`; /////////////////// // Crowdsale params @@ -44,8 +46,8 @@ async function executeApp() { } options.push('Add new STO module'); - let index = readlineSync.keyInSelect(options, 'What do you want to do?', { cancel: 'Exit' }); - let optionSelected = index != -1 ? options[index] : 'Exit'; + let index = readlineSync.keyInSelect(options, 'What do you want to do?', { cancel: 'EXIT' }); + let optionSelected = index != -1 ? options[index] : 'EXIT'; console.log('Selected:', optionSelected, '\n'); switch (optionSelected) { case 'Show existing STO information': @@ -59,7 +61,7 @@ async function executeApp() { case 'Add new STO module': await addSTOModule(); break; - case 'Exit': + case 'EXIT': exit = true; break; } @@ -96,6 +98,7 @@ async function showSTO(selectedSTO, currentSTO) { break; case 'USDTieredSTO': await usdTieredSTO_status(currentSTO); + await showAccreditedData(currentSTO); break; } } @@ -125,8 +128,8 @@ async function addSTOModule(stoConfig) { let moduleFactory = new web3.eth.Contract(moduleFactoryABI, m); return web3.utils.hexToUtf8(await moduleFactory.methods.name().call()); })); - let index = readlineSync.keyInSelect(options, 'What type of STO do you want?', { cancel: 'Return' }); - optionSelected = index != -1 ? options[index] : 'Return'; + let index = readlineSync.keyInSelect(options, 'What type of STO do you want?', { cancel: 'RETURN' }); + optionSelected = index != -1 ? options[index] : 'RETURN'; } else { optionSelected = stoConfig.type; } @@ -270,8 +273,6 @@ async function cappedSTO_status(currentSTO) { - Tokens remaining: ${web3.utils.fromWei(displayCap.sub(displayTokensSold))} ${displayTokenSymbol.toUpperCase()} - Investor count: ${displayInvestorCount} `); - - console.log(chalk.green(`\n${(web3.utils.fromWei(await getBalance(Issuer.address, gbl.constants.FUND_RAISE_TYPES.POLY)))} POLY balance remaining at issuer address ${Issuer.address}`)); } //////////////////// @@ -280,7 +281,7 @@ async function cappedSTO_status(currentSTO) { function fundingConfigUSDTieredSTO() { let funding = {}; - let selectedFunding = readlineSync.question('Enter' + chalk.green(` P `) + 'for POLY raise,' + chalk.green(` D `) + 'for DAI raise,' + chalk.green(` E `) + 'for Ether raise or any combination of them (i.e.' + chalk.green(` PED `) + 'for all): ').toUpperCase(); + let selectedFunding = readlineSync.question('Enter' + chalk.green(` P `) + 'for POLY raise,' + chalk.green(` S `) + 'for Stable Coin raise,' + chalk.green(` E `) + 'for Ether raise or any combination of them (i.e.' + chalk.green(` PSE `) + 'for all): ').toUpperCase(); funding.raiseType = []; if (selectedFunding.includes('E')) { @@ -289,53 +290,110 @@ function fundingConfigUSDTieredSTO() { if (selectedFunding.includes('P')) { funding.raiseType.push(gbl.constants.FUND_RAISE_TYPES.POLY); } - if (selectedFunding.includes('D')) { - funding.raiseType.push(gbl.constants.FUND_RAISE_TYPES.DAI); + if (selectedFunding.includes('S')) { + funding.raiseType.push(gbl.constants.FUND_RAISE_TYPES.STABLE); } if (funding.raiseType.length == 0) { - funding.raiseType = [gbl.constants.FUND_RAISE_TYPES.ETH, gbl.constants.FUND_RAISE_TYPES.POLY, gbl.constants.FUND_RAISE_TYPES.DAI]; + funding.raiseType = [gbl.constants.FUND_RAISE_TYPES.ETH, gbl.constants.FUND_RAISE_TYPES.POLY, gbl.constants.FUND_RAISE_TYPES.STABLE]; } return funding; } -function addressesConfigUSDTieredSTO(usdTokenRaise) { - let addresses = {}; +async function addressesConfigUSDTieredSTO(usdTokenRaise) { - addresses.wallet = readlineSync.question('Enter the address that will receive the funds from the STO (' + Issuer.address + '): ', { - limit: function (input) { - return web3.utils.isAddress(input); - }, - limitMessage: "Must be a valid address", - defaultInput: Issuer.address - }); - if (addresses.wallet == "") addresses.wallet = Issuer.address; + let addresses, menu; - addresses.reserveWallet = readlineSync.question('Enter the address that will receive remaining tokens in the case the cap is not met (' + Issuer.address + '): ', { - limit: function (input) { - return web3.utils.isAddress(input); - }, - limitMessage: "Must be a valid address", - defaultInput: Issuer.address - }); - if (addresses.reserveWallet == "") addresses.reserveWallet = Issuer.address; + do { - if (usdTokenRaise) { - addresses.usdToken = readlineSync.question('Enter the address of the USD Token or stable coin (' + usdToken.options.address + '): ', { + addresses = {}; + + addresses.wallet = readlineSync.question('Enter the address that will receive the funds from the STO (' + Issuer.address + '): ', { limit: function (input) { return web3.utils.isAddress(input); }, limitMessage: "Must be a valid address", - defaultInput: usdToken.options.address + defaultInput: Issuer.address }); - if (addresses.usdToken == "") addresses.usdToken = usdToken.options.address; - } else { - addresses.usdToken = '0x0000000000000000000000000000000000000000'; - } + if (addresses.wallet == "") addresses.wallet = Issuer.address; + + addresses.reserveWallet = readlineSync.question('Enter the address that will receive remaining tokens in the case the cap is not met (' + Issuer.address + '): ', { + limit: function (input) { + return web3.utils.isAddress(input); + }, + limitMessage: "Must be a valid address", + defaultInput: Issuer.address + }); + if (addresses.reserveWallet == "") addresses.reserveWallet = Issuer.address; + + let listOfAddress; + + if (usdTokenRaise) { + addresses.usdToken = readlineSync.question('Enter the address (or multiple addresses separated by commas) of the USD stable coin(s) (' + usdToken.options.address + '): ', { + limit: function (input) { + listOfAddress = input.split(','); + return listOfAddress.every((addr) => { + return web3.utils.isAddress(addr) + }) + }, + limitMessage: "Must be a valid address", + defaultInput: usdToken.options.address + }); + if (addresses.usdToken == "") { + listOfAddress = [usdToken.options.address] + addresses.usdToken = [usdToken.options.address]; + } + } else { + listOfAddress = [] + addresses.usdToken = []; + } + + if ((usdTokenRaise) && (!await processArray(listOfAddress))) { + console.log(chalk.yellow(`\nPlease, verify your stable coins addresses to continue with this process.\n`)) + menu = true; + } else { + menu = false; + } + + if (typeof addresses.usdToken === 'string') { + addresses.usdToken = addresses.usdToken.split(",") + } + + } while (menu); return addresses; } +async function checkSymbol(address) { + let stableCoin = common.connect(abis.erc20(), address); + try { + return await stableCoin.methods.symbol().call(); + } catch (e) { + return "" + } +} + +async function processArray(array) { + let result = true; + for (const address of array) { + let symbol = await checkSymbol(address); + if (symbol == "") { + result = false; + console.log(`${address} seems not to be a stable coin`) + } + } + return result +} + +async function processAddress(array) { + let list = []; + for (const address of array) { + let symbol = await checkSymbol(address); + list.push({ "symbol": symbol, "address": address }) + } + return list +} + function tiersConfigUSDTieredSTO(polyRaise) { let tiers = {}; @@ -484,7 +542,7 @@ async function usdTieredSTO_launch(stoConfig) { let useConfigFile = typeof stoConfig !== 'undefined'; let funding = useConfigFile ? stoConfig.funding : fundingConfigUSDTieredSTO(); - let addresses = useConfigFile ? stoConfig.addresses : addressesConfigUSDTieredSTO(funding.raiseType.includes(gbl.constants.FUND_RAISE_TYPES.DAI)); + let addresses = useConfigFile ? stoConfig.addresses : await addressesConfigUSDTieredSTO(funding.raiseType.includes(gbl.constants.FUND_RAISE_TYPES.STABLE)); let tiers = useConfigFile ? stoConfig.tiers : tiersConfigUSDTieredSTO(funding.raiseType.includes(gbl.constants.FUND_RAISE_TYPES.POLY)); let limits = useConfigFile ? stoConfig.limits : limitsConfigUSDTieredSTO(); let times = timesConfigUSDTieredSTO(stoConfig); @@ -521,6 +579,7 @@ async function usdTieredSTO_status(currentSTO) { let displayStartTime = await currentSTO.methods.startTime().call(); let displayEndTime = await currentSTO.methods.endTime().call(); let displayCurrentTier = parseInt(await currentSTO.methods.currentTier().call()) + 1; + let test = await currentSTO.methods.nonAccreditedLimitUSD().call(); let displayNonAccreditedLimitUSD = web3.utils.fromWei(await currentSTO.methods.nonAccreditedLimitUSD().call()); let displayMinimumInvestmentUSD = web3.utils.fromWei(await currentSTO.methods.minimumInvestmentUSD().call()); let displayWallet = await currentSTO.methods.wallet().call(); @@ -529,12 +588,16 @@ async function usdTieredSTO_status(currentSTO) { let displayInvestorCount = await currentSTO.methods.investorCount().call(); let displayIsFinalized = await currentSTO.methods.isFinalized().call() ? "YES" : "NO"; let displayTokenSymbol = await securityToken.methods.symbol().call(); - - let tiersLength = await currentSTO.methods.getNumberOfTiers().call();; - + let tiersLength = await currentSTO.methods.getNumberOfTiers().call(); + let listOfStableCoins = await currentSTO.methods.getUsdTokens().call(); let raiseTypes = []; + let stableSymbols = []; + for (const fundType in gbl.constants.FUND_RAISE_TYPES) { if (await currentSTO.methods.fundRaiseTypes(gbl.constants.FUND_RAISE_TYPES[fundType]).call()) { + if (fundType == STABLE) { + stableSymbols = await processAddress(listOfStableCoins) + } raiseTypes.push(fundType); } } @@ -564,8 +627,13 @@ async function usdTieredSTO_status(currentSTO) { } let mintedPerTier = mintedPerTierPerRaiseType[gbl.constants.FUND_RAISE_TYPES[type]]; - displayMintedPerTierPerType += ` - Sold for ${type}:\t\t ${web3.utils.fromWei(mintedPerTier)} ${displayTokenSymbol} ${displayDiscountMinted}`; + if ((type == STABLE) && (stableSymbols.length)) { + displayMintedPerTierPerType += ` + Sold for stable coin(s): ${web3.utils.fromWei(mintedPerTier)} ${displayTokenSymbol} ${displayDiscountMinted}`; + } else { + displayMintedPerTierPerType += ` + Sold for ${type}:\t\t ${web3.utils.fromWei(mintedPerTier)} ${displayTokenSymbol} ${displayDiscountMinted}`; + } } displayTiers += ` @@ -588,29 +656,62 @@ async function usdTieredSTO_status(currentSTO) { for (const type of raiseTypes) { let balance = await getBalance(displayWallet, gbl.constants.FUND_RAISE_TYPES[type]); let walletBalance = web3.utils.fromWei(balance); - let walletBalanceUSD = web3.utils.fromWei(await currentSTO.methods.convertToUSD(gbl.constants.FUND_RAISE_TYPES[type], balance).call()); - displayWalletBalancePerType += ` + if ((type == STABLE) && (stableSymbols.length)) { + stableSymbols.forEach(async (stable) => { + let raised = await checkStableBalance(displayWallet, stable.address); + displayWalletBalancePerType += ` + Balance ${stable.symbol}:\t\t ${web3.utils.fromWei(raised)} ${stable.symbol}`; + }) + } else { + let walletBalanceUSD = web3.utils.fromWei(await currentSTO.methods.convertToUSD(gbl.constants.FUND_RAISE_TYPES[type], balance).call()); + displayWalletBalancePerType += ` Balance ${type}:\t\t ${walletBalance} ${type} (${walletBalanceUSD} USD)`; + } balance = await getBalance(displayReserveWallet, gbl.constants.FUND_RAISE_TYPES[type]); let reserveWalletBalance = web3.utils.fromWei(balance); let reserveWalletBalanceUSD = web3.utils.fromWei(await currentSTO.methods.convertToUSD(gbl.constants.FUND_RAISE_TYPES[type], balance).call()); - displayReserveWalletBalancePerType += ` + if ((type == STABLE) && (stableSymbols.length)) { + stableSymbols.forEach(async (stable) => { + let raised = await checkStableBalance(displayReserveWallet, stable.address); + displayReserveWalletBalancePerType += ` + Balance ${stable.symbol}:\t\t ${web3.utils.fromWei(raised)} ${stable.symbol}`; + }) + } else { + displayReserveWalletBalancePerType += ` Balance ${type}:\t\t ${reserveWalletBalance} ${type} (${reserveWalletBalanceUSD} USD)`; + } let fundsRaised = web3.utils.fromWei(await currentSTO.methods.fundsRaised(gbl.constants.FUND_RAISE_TYPES[type]).call()); - displayFundsRaisedPerType += ` + if ((type == STABLE) && (stableSymbols.length)) { + stableSymbols.forEach(async (stable) => { + let raised = await getStableCoinsRaised(currentSTO, stable.address); + displayFundsRaisedPerType += ` + ${stable.symbol}:\t\t\t ${web3.utils.fromWei(raised)} ${stable.symbol}`; + }) + } else { + displayFundsRaisedPerType += ` ${type}:\t\t\t ${fundsRaised} ${type}`; + } //Only show sold for if more than one raise type are allowed if (raiseTypes.length > 1) { let tokensSoldPerType = web3.utils.fromWei(await currentSTO.methods.getTokensSoldFor(gbl.constants.FUND_RAISE_TYPES[type]).call()); - displayTokensSoldPerType += ` - Sold for ${type}:\t\t ${tokensSoldPerType} ${displayTokenSymbol}`; + if ((type == STABLE) && (stableSymbols.length)) { + displayTokensSoldPerType += ` + Sold for stable coin(s): ${tokensSoldPerType} ${displayTokenSymbol}`; + } else { + displayTokensSoldPerType += ` + Sold for ${type}:\t\t ${tokensSoldPerType} ${displayTokenSymbol}`; + } } } let displayRaiseType = raiseTypes.join(' - '); + //If STO has stable coins, we list them one by one + if (stableSymbols.length) { + displayRaiseType = displayRaiseType.replace(STABLE, "") + `${stableSymbols.map((obj) => { return obj.symbol }).toString().replace(`,`, ` - `)}` + } let now = Math.floor(Date.now() / 1000); let timeTitle; @@ -650,10 +751,21 @@ async function usdTieredSTO_status(currentSTO) { - Investor count: ${displayInvestorCount} - Funds Raised` + displayFundsRaisedPerType + ` - USD: ${displayFundsRaisedUSD} USD + Total USD: ${displayFundsRaisedUSD} USD `); +} + +async function checkStableBalance(walletAddress, stableAddress) { + let stableCoin = common.connect(abis.erc20(), stableAddress); + try { + return await stableCoin.methods.balanceOf(walletAddress).call(); + } catch (e) { + return "" + } +} - console.log(chalk.green(`\n${(web3.utils.fromWei(await getBalance(Issuer.address, gbl.constants.FUND_RAISE_TYPES.POLY)))} POLY balance remaining at issuer address ${Issuer.address}`)); +async function getStableCoinsRaised(currentSTO, address) { + return await currentSTO.methods.stableCoinsRaised(address).call() } async function usdTieredSTO_configure(currentSTO) { @@ -676,9 +788,10 @@ async function usdTieredSTO_configure(currentSTO) { 'Modify limits configuration', 'Modify funding configuration'); } - let index = readlineSync.keyInSelect(options, 'What do you want to do?'); - switch (index) { - case 0: + let index = readlineSync.keyInSelect(options, 'What do you want to do?', { cancel: 'RETURN' }); + let selected = index != -1 ? options[index] : 'Exit'; + switch (selected) { + case 'Finalize STO': let reserveWallet = await currentSTO.methods.reserveWallet().call(); let isVerified = await securityToken.methods.verifyTransfer('0x0000000000000000000000000000000000000000', reserveWallet, 0, web3.utils.fromAscii("")).call(); if (isVerified) { @@ -690,7 +803,7 @@ async function usdTieredSTO_configure(currentSTO) { console.log(chalk.red(`Reserve wallet (${reserveWallet}) is not able to receive remaining tokens. Check if this address is whitelisted.`)); } break; - case 1: + case 'Change accredited account': let investor = readlineSync.question('Enter the address to change accreditation: '); let isAccredited = readlineSync.keyInYNStrict(`Is ${investor} accredited?`); let investors = [investor]; @@ -699,10 +812,10 @@ async function usdTieredSTO_configure(currentSTO) { // 2 GAS? await common.sendTransaction(changeAccreditedAction); break; - case 2: + case 'Change accredited in batch': await changeAccreditedInBatch(currentSTO); break; - case 3: + case 'Change non accredited limit for an account': let account = readlineSync.question('Enter the address to change non accredited limit: '); let limit = readlineSync.question(`Enter the limit in USD: `); let accounts = [account]; @@ -710,26 +823,26 @@ async function usdTieredSTO_configure(currentSTO) { let changeNonAccreditedLimitAction = currentSTO.methods.changeNonAccreditedLimit(accounts, limits); await common.sendTransaction(changeNonAccreditedLimitAction); break; - case 4: + case 'Change non accredited limits in batch': await changeNonAccreditedLimitsInBatch(currentSTO); break; - case 5: + case 'Modify times configuration': await modfifyTimes(currentSTO); await usdTieredSTO_status(currentSTO); break; - case 6: + case 'Modify tiers configuration': await modfifyTiers(currentSTO); await usdTieredSTO_status(currentSTO); break; - case 7: + case 'Modify addresses configuration': await modfifyAddresses(currentSTO); await usdTieredSTO_status(currentSTO); break; - case 8: + case 'Modify limits configuration': await modfifyLimits(currentSTO); await usdTieredSTO_status(currentSTO); break; - case 9: + case 'Modify funding configuration': await modfifyFunding(currentSTO); await usdTieredSTO_status(currentSTO); break; @@ -737,6 +850,33 @@ async function usdTieredSTO_configure(currentSTO) { } } +async function showAccreditedData(currentSTO) { + let accreditedData = await currentSTO.methods.getAccreditedData().call(); + let investorArray = accreditedData[0]; + let accreditedArray = accreditedData[1]; + let nonAccreditedLimitArray = accreditedData[2]; + + if (investorArray.length > 0) { + let dataTable = [['Investor', 'Is accredited', 'Non-accredited limit (USD)']]; + for (let i = 0; i < investorArray.length; i++) { + dataTable.push([ + investorArray[i], + accreditedArray[i] ? 'YES' : 'NO', + accreditedArray[i] ? 'N/A' : (nonAccreditedLimitArray[i] !== '0' ? web3.utils.fromWei(nonAccreditedLimitArray[i]) : 'default') + ]); + } + console.log(); + console.log(`************************************ ACCREDITED DATA *************************************`); + console.log(); + console.log(table(dataTable)); + } else { + console.log(); + console.log(chalk.yellow(`There is no accredited data to show`)); + console.log(); + } + +} + async function changeAccreditedInBatch(currentSTO) { let csvFilePath = readlineSync.question(`Enter the path for csv data file (${ACCREDIT_DATA_CSV}): `, { defaultInput: ACCREDIT_DATA_CSV @@ -758,7 +898,7 @@ async function changeAccreditedInBatch(currentSTO) { let [investorArray, isAccreditedArray] = common.transposeBatches(batches); for (let batch = 0; batch < batches.length; batch++) { console.log(`Batch ${batch + 1} - Attempting to change accredited accounts:\n\n`, investorArray[batch], '\n'); - let action = await currentSTO.methods.changeAccredited(investorArray[batch], isAccreditedArray[batch]); + let action = currentSTO.methods.changeAccredited(investorArray[batch], isAccreditedArray[batch]); let receipt = await common.sendTransaction(action); console.log(chalk.green('Change accredited transaction was successful.')); console.log(`${receipt.gasUsed} gas used. Spent: ${web3.utils.fromWei((new web3.utils.BN(receipt.gasUsed)).mul(new web3.utils.BN(defaultGasPrice)))} ETH`); @@ -785,8 +925,9 @@ async function changeNonAccreditedLimitsInBatch(currentSTO) { let batches = common.splitIntoBatches(validData, batchSize); let [investorArray, limitArray] = common.transposeBatches(batches); for (let batch = 0; batch < batches.length; batch++) { + limitArray[batch] = limitArray[batch].map(a => web3.utils.toWei(new web3.utils.BN(a))); console.log(`Batch ${batch + 1} - Attempting to change non accredited limit to accounts:\n\n`, investorArray[batch], '\n'); - let action = await currentSTO.methods.changeNonAccreditedLimit(investorArray[batch], limitArray[batch]); + let action = currentSTO.methods.changeNonAccreditedLimit(investorArray[batch], limitArray[batch]); let receipt = await common.sendTransaction(action); console.log(chalk.green('Change non accredited limits transaction was successful.')); console.log(`${receipt.gasUsed} gas used. Spent: ${web3.utils.fromWei((new web3.utils.BN(receipt.gasUsed)).mul(new web3.utils.BN(defaultGasPrice)))} ETH`); @@ -812,7 +953,7 @@ async function modfifyFunding(currentSTO) { } async function modfifyAddresses(currentSTO) { - let addresses = addressesConfigUSDTieredSTO(await currentSTO.methods.fundRaiseTypes(gbl.constants.FUND_RAISE_TYPES.DAI).call()); + let addresses = await addressesConfigUSDTieredSTO(await currentSTO.methods.fundRaiseTypes(gbl.constants.FUND_RAISE_TYPES.STABLE).call()); let modifyAddressesAction = currentSTO.methods.modifyAddresses(addresses.wallet, addresses.reserveWallet, addresses.usdToken); await common.sendTransaction(modifyAddressesAction); } @@ -837,7 +978,7 @@ async function getBalance(from, type) { return await web3.eth.getBalance(from); case gbl.constants.FUND_RAISE_TYPES.POLY: return await polyToken.methods.balanceOf(from).call(); - case gbl.constants.FUND_RAISE_TYPES.DAI: + case gbl.constants.FUND_RAISE_TYPES.STABLE: return await usdToken.methods.balanceOf(from).call(); } } @@ -861,7 +1002,7 @@ async function getAllModulesByType(type) { let nameTemp = web3.utils.hexToUtf8(details[0]); let pausedTemp = null; if (type == gbl.constants.MODULES_TYPES.STO || type == gbl.constants.MODULES_TYPES.TRANSFER) { - let abiTemp = JSON.parse(require('fs').readFileSync(`./build/contracts/${nameTemp}.json`).toString()).abi; + let abiTemp = JSON.parse(require('fs').readFileSync(`${__dirname}/../../build/contracts/${nameTemp}.json`).toString()).abi; let contractTemp = new web3.eth.Contract(abiTemp, details[1]); pausedTemp = await contractTemp.methods.paused().call(); } @@ -939,8 +1080,8 @@ async function selectToken() { }); options.push('Enter token symbol manually'); - let index = readlineSync.keyInSelect(options, 'Select a token:', { cancel: 'Exit' }); - let selected = index != -1 ? options[index] : 'Exit'; + let index = readlineSync.keyInSelect(options, 'Select a token:', { cancel: 'EXIT' }); + let selected = index != -1 ? options[index] : 'EXIT'; switch (selected) { case 'Enter token symbol manually': result = readlineSync.question('Enter the token symbol: '); @@ -965,4 +1106,4 @@ module.exports = { await initialize(_tokenSymbol); return addSTOModule(stoConfig) } -} \ No newline at end of file +} diff --git a/CLI/commands/strMigrator.js b/CLI/commands/strMigrator.js index 4a8dabfe0..7d4e0248f 100644 --- a/CLI/commands/strMigrator.js +++ b/CLI/commands/strMigrator.js @@ -54,7 +54,7 @@ async function step_instance_toSTR(toStrAddress) { // return web3.utils.isAddress(input); // }, // limitMessage: "Must be a valid address" - // }); + // }); _toStrAddress = "0x240f9f86b1465bf1b8eb29bc88cbf65573dfdd97"; } @@ -81,7 +81,7 @@ async function step_instance_fromTR(fromTrAddress) { // return web3.utils.isAddress(input); // }, // limitMessage: "Must be a valid address" - // }); + // }); _fromTrAddress = "0xc31714e6759a1ee26db1d06af1ed276340cd4233"; } @@ -192,7 +192,7 @@ async function step_instance_fromSTR(fromStrAddress) { // return web3.utils.isAddress(input); // }, // limitMessage: "Must be a valid address" - // }); + // }); _fromStrAddress = "0xef58491224958d978facf55d2120c55a24516b98"; } @@ -216,7 +216,7 @@ async function step_get_deployed_tokens(securityTokenRegistry, singleTicker) { let event = common.getEventFromLogs(securityTokenRegistry._jsonInterface, [log], 'LogNewSecurityToken'); if (typeof singleTicker === 'undefined' || event._ticker == singleTicker) { let tokenAddress = event._securityTokenAddress; - let securityTokenABI = JSON.parse(require('fs').readFileSync('./CLI/data/SecurityToken1-4-0.json').toString()).abi; + let securityTokenABI = JSON.parse(require('fs').readFileSync(`${__dirname}/../data/SecurityToken1-4-0.json`).toString()).abi; console.log(`Creating SecurityToken contract instance of address: ${tokenAddress}...`); let token = new web3.eth.Contract(securityTokenABI, tokenAddress); token.setProvider(web3.currentProvider); @@ -230,9 +230,9 @@ async function step_get_deployed_tokens(securityTokenRegistry, singleTicker) { let gmtAddress = (await token.methods.getModule(2, 0).call())[1]; - let gtmABI = JSON.parse(require('fs').readFileSync('./CLI/data/GeneralTransferManager1-4-0.json').toString()).abi; + let gtmABI = JSON.parse(require('fs').readFileSync(`${__dirname}/../data/GeneralTransferManager1-4-0.json`).toString()).abi; let gmt = new web3.eth.Contract(gtmABI, gmtAddress); - //let gtmEvents = await gmt.getPastEvents('LogModifyWhitelist', { fromBlock: event.blockNumber}); + //let gtmEvents = await gmt.getPastEvents('LogModifyWhitelist', { fromBlock: event.blockNumber}); let gtmLogs = await getLogsFromEtherscan(gmt.options.address, 0, 'latest', 'LogModifyWhitelist(address,uint256,address,uint256,uint256,uint256,bool)'); let gtmEvents = common.getMultipleEventsFromLogs(gmt._jsonInterface, gtmLogs, 'LogModifyWhitelist'); @@ -284,7 +284,7 @@ async function step_launch_STs(tokens, securityTokenRegistry, tokenAddress) { let failed = []; let totalGas = new web3.utils.BN(0); let polymathRegistryAddress = await contracts.polymathRegistry(); - let STFactoryABI = JSON.parse(require('fs').readFileSync('./build/contracts/STFactory.json').toString()).abi; + let STFactoryABI = JSON.parse(require('fs').readFileSync(`${__dirname}/../../build/contracts/STFactory.json`).toString()).abi; let STFactoryAddress = await securityTokenRegistry.methods.getSTFactoryAddress().call(); let STFactory = new web3.eth.Contract(STFactoryABI, STFactoryAddress); for (const t of tokens) { diff --git a/CLI/commands/token_manager.js b/CLI/commands/token_manager.js index b74505257..fbe89b38f 100644 --- a/CLI/commands/token_manager.js +++ b/CLI/commands/token_manager.js @@ -388,7 +388,7 @@ async function multiMint(_csvFilePath, _batchSize) { for (let batch = 0; batch < batches.length; batch++) { console.log(`Batch ${batch + 1} - Attempting to mint tokens to accounts: \n\n`, investorArray[batch], '\n'); amountArray[batch] = amountArray[batch].map(a => web3.utils.toWei(a.toString())); - let action = await securityToken.methods.mintMulti(investorArray[batch], amountArray[batch]); + let action = securityToken.methods.mintMulti(investorArray[batch], amountArray[batch]); let receipt = await common.sendTransaction(action); console.log(chalk.green('Multi mint transaction was successful.')); console.log(`${receipt.gasUsed} gas used.Spent: ${web3.utils.fromWei((new web3.utils.BN(receipt.gasUsed)).mul(new web3.utils.BN(defaultGasPrice)))} ETH`); diff --git a/CLI/commands/transfer_manager.js b/CLI/commands/transfer_manager.js index 16af0e34e..39af92b78 100644 --- a/CLI/commands/transfer_manager.js +++ b/CLI/commands/transfer_manager.js @@ -6,12 +6,45 @@ const contracts = require('./helpers/contract_addresses'); const abis = require('./helpers/contract_abis'); const gbl = require('./common/global'); const csvParse = require('./helpers/csv'); -const { table } = require('table') +const { table } = require('table'); /////////////////// // Constants -const WHITELIST_DATA_CSV = './CLI/data/Transfer/GTM/whitelist_data.csv'; -const PERCENTAGE_WHITELIST_DATA_CSV = './CLI/data/Transfer/PercentageTM/whitelist_data.csv'; +const WHITELIST_DATA_CSV = `${__dirname}/../data/Transfer/GTM/whitelist_data.csv`; +const ADD_BLACKLIST_DATA_CSV = `${__dirname}/../data/Transfer/BlacklistTM/add_blacklist_data.csv`; +const MODIFY_BLACKLIST_DATA_CSV = `${__dirname}/../data/Transfer/BlacklistTM/modify_blacklist_data.csv`; +const DELETE_BLACKLIST_DATA_CSV = `${__dirname}/../data/Transfer/BlacklistTM/delete_blacklist_data.csv`; +const ADD_INVESTOR_BLACKLIST_DATA_CSV = `${__dirname}/../data/Transfer/BlacklistTM/add_investor_blacklist_data.csv`; +const REMOVE_INVESTOR_BLACKLIST_DATA_CSV = `${__dirname}/../data/Transfer/BlacklistTM/remove_investor_blacklist_data.csv`; +const PERCENTAGE_WHITELIST_DATA_CSV = `${__dirname}/../data/Transfer/PercentageTM/whitelist_data.csv`; +const ADD_MANUAL_APPROVAL_DATA_CSV = `${__dirname}/../data/Transfer/MATM/add_manualapproval_data.csv`; +const MODIFY_MANUAL_APPROVAL_DATA_CSV = `${__dirname}/../data/Transfer/MATM/modify_manualapproval_data.csv`; +const REVOKE_MANUAL_APPROVAL_DATA_CSV = `${__dirname}/../data/Transfer/MATM/revoke_manualapproval_data.csv`; +const ADD_DAILY_RESTRICTIONS_DATA_CSV = `${__dirname}/../data/Transfer/VRTM/add_daily_restriction_data.csv`; +const MODIFY_DAILY_RESTRICTIONS_DATA_CSV = `${__dirname}/../data/Transfer/VRTM/modify_daily_restriction_data.csv`; +const REMOVE_DAILY_RESTRICTIONS_DATA_CSV = `${__dirname}/../data/Transfer/VRTM/remove_daily_restriction_data.csv`; +const ADD_CUSTOM_RESTRICTIONS_DATA_CSV = `${__dirname}/../data/Transfer/VRTM/add_custom_restriction_data.csv`; +const MODIFY_CUSTOM_RESTRICTIONS_DATA_CSV = `${__dirname}/../data/Transfer/VRTM/modify_custom_restriction_data.csv`; +const REMOVE_CUSTOM_RESTRICTIONS_DATA_CSV = `${__dirname}/../data/Transfer/VRTM/remove_custom_restriction_data.csv`; +const ADD_LOCKUP_DATA_CSV = `${__dirname}/../data/Transfer/LockupTM/add_lockup_data.csv`; +const MODIFY_LOCKUP_DATA_CSV = `${__dirname}/../data/Transfer/LockupTM/modify_lockup_data.csv`; +const DELETE_LOCKUP_DATA_CSV = `${__dirname}/../data/Transfer/LockupTM/delete_lockup_data.csv`; +const ADD_LOCKUP_INVESTOR_DATA_CSV = `${__dirname}/../data/Transfer/LockupTM/add_lockup_investor_data.csv`; +const REMOVE_LOCKUP_INVESTOR_DATA_CSV = `${__dirname}/../data/Transfer/LockupTM/remove_lockup_investor_data.csv`; + +const RESTRICTION_TYPES = ['Fixed', 'Percentage']; + +const MATM_MENU_ADD = 'Add new manual approval'; +const MATM_MENU_MANAGE = 'Manage existing approvals'; +const MATM_MENU_EXPLORE = 'Explore account'; +const MATM_MENU_OPERATE = 'Operate with multiple approvals'; +const MATM_MENU_MANAGE_INCRESE = 'Increase allowance'; +const MATM_MENU_MANAGE_DECREASE = 'Decrease allowance'; +const MATM_MENU_MANAGE_TIME = 'Modify expiry time and/or description'; +const MATM_MENU_MANAGE_REVOKE = 'Revoke this approval'; +const MATM_MENU_OPERATE_ADD = 'Add multiple approvals in batch'; +const MATM_MENU_OPERATE_MODIFY = 'Modify multiple approvals in batch'; +const MATM_MENU_OPERATE_REVOKE = 'Revoke multiple approvals in batch'; // App flow let tokenSymbol; @@ -21,98 +54,96 @@ let moduleRegistry; let currentTransferManager; async function executeApp() { - let exit = false; - while (!exit) { - console.log('\n', chalk.blue('Transfer Manager - Main Menu', '\n')); - - let tmModules = await getAllModulesByType(gbl.constants.MODULES_TYPES.TRANSFER); - let nonArchivedModules = tmModules.filter(m => !m.archived); - if (nonArchivedModules.length > 0) { - console.log(`Transfer Manager modules attached:`); - nonArchivedModules.map(m => console.log(`- ${m.name} at ${m.address}`)) - } else { - console.log(`There are no Transfer Manager modules attached`); - } + console.log('\n', chalk.blue('Transfer Manager - Main Menu', '\n')); - let options = ['Verify transfer', 'Transfer']; - let forcedTransferDisabled = await securityToken.methods.controllerDisabled().call(); - if (!forcedTransferDisabled) { - options.push('Forced transfers'); - } - if (nonArchivedModules.length > 0) { - options.push('Config existing modules'); - } - options.push('Add new Transfer Manager module'); + let tmModules = await getAllModulesByType(gbl.constants.MODULES_TYPES.TRANSFER); + let nonArchivedModules = tmModules.filter(m => !m.archived); + if (nonArchivedModules.length > 0) { + console.log(`Transfer Manager modules attached:`); + nonArchivedModules.map(m => console.log(`- ${m.name} at ${m.address}`)) + } else { + console.log(`There are no Transfer Manager modules attached`); + } - let index = readlineSync.keyInSelect(options, 'What do you want to do?', { cancel: 'Exit' }); - let optionSelected = index != -1 ? options[index] : 'Exit'; - console.log('Selected:', optionSelected, '\n'); - switch (optionSelected) { - case 'Verify transfer': - let verifyTotalSupply = web3.utils.fromWei(await securityToken.methods.totalSupply().call()); - await logTotalInvestors(); - let verifyTransferFrom = readlineSync.question(`Enter the sender account (${Issuer.address}): `, { - limit: function (input) { - return web3.utils.isAddress(input); - }, - limitMessage: "Must be a valid address", - defaultInput: Issuer.address - }); - await logBalance(verifyTransferFrom, verifyTotalSupply); - let verifyTransferTo = readlineSync.question('Enter the receiver account: ', { - limit: function (input) { - return web3.utils.isAddress(input); - }, - limitMessage: "Must be a valid address", - }); - await logBalance(verifyTransferTo, verifyTotalSupply); - let verifyTransferAmount = readlineSync.question('Enter amount of tokens to verify: '); - let isVerified = await securityToken.methods.verifyTransfer(verifyTransferFrom, verifyTransferTo, web3.utils.toWei(verifyTransferAmount), web3.utils.fromAscii("")).call(); - if (isVerified) { - console.log(chalk.green(`\n${verifyTransferAmount} ${tokenSymbol} can be transferred from ${verifyTransferFrom} to ${verifyTransferTo}!`)); - } else { - console.log(chalk.red(`\n${verifyTransferAmount} ${tokenSymbol} can't be transferred from ${verifyTransferFrom} to ${verifyTransferTo}!`)); - } - break; - case 'Transfer': - let totalSupply = web3.utils.fromWei(await securityToken.methods.totalSupply().call()); + let options = ['Verify transfer', 'Transfer']; + let forcedTransferDisabled = await securityToken.methods.controllerDisabled().call(); + if (!forcedTransferDisabled) { + options.push('Forced transfers'); + } + if (nonArchivedModules.length > 0) { + options.push('Config existing modules'); + } + options.push('Add new Transfer Manager module'); + + let index = readlineSync.keyInSelect(options, 'What do you want to do?', { cancel: 'EXIT' }); + let optionSelected = index != -1 ? options[index] : 'EXIT'; + console.log('Selected:', optionSelected, '\n'); + switch (optionSelected) { + case 'Verify transfer': + let verifyTotalSupply = web3.utils.fromWei(await securityToken.methods.totalSupply().call()); + await logTotalInvestors(); + let verifyTransferFrom = readlineSync.question(`Enter the sender account (${Issuer.address}): `, { + limit: function (input) { + return web3.utils.isAddress(input); + }, + limitMessage: "Must be a valid address", + defaultInput: Issuer.address + }); + await logBalance(verifyTransferFrom, verifyTotalSupply); + let verifyTransferTo = readlineSync.question('Enter the receiver account: ', { + limit: function (input) { + return web3.utils.isAddress(input); + }, + limitMessage: "Must be a valid address", + }); + await logBalance(verifyTransferTo, verifyTotalSupply); + let verifyTransferAmount = readlineSync.question('Enter amount of tokens to verify: '); + let isVerified = await securityToken.methods.verifyTransfer(verifyTransferFrom, verifyTransferTo, web3.utils.toWei(verifyTransferAmount), web3.utils.fromAscii("")).call(); + if (isVerified) { + console.log(chalk.green(`\n${verifyTransferAmount} ${tokenSymbol} can be transferred from ${verifyTransferFrom} to ${verifyTransferTo}!`)); + } else { + console.log(chalk.red(`\n${verifyTransferAmount} ${tokenSymbol} can't be transferred from ${verifyTransferFrom} to ${verifyTransferTo}!`)); + } + break; + case 'Transfer': + let totalSupply = web3.utils.fromWei(await securityToken.methods.totalSupply().call()); + await logTotalInvestors(); + await logBalance(Issuer.address, totalSupply); + let transferTo = readlineSync.question('Enter beneficiary of tranfer: ', { + limit: function (input) { + return web3.utils.isAddress(input); + }, + limitMessage: "Must be a valid address" + }); + await logBalance(transferTo, totalSupply); + let transferAmount = readlineSync.question('Enter amount of tokens to transfer: '); + let isTranferVerified = await securityToken.methods.verifyTransfer(Issuer.address, transferTo, web3.utils.toWei(transferAmount), web3.utils.fromAscii("")).call(); + if (isTranferVerified) { + let transferAction = securityToken.methods.transfer(transferTo, web3.utils.toWei(transferAmount)); + let receipt = await common.sendTransaction(transferAction); + let event = common.getEventFromLogs(securityToken._jsonInterface, receipt.logs, 'Transfer'); + console.log(chalk.green(`${event.from} transferred ${web3.utils.fromWei(event.value)} ${tokenSymbol} to ${event.to} successfully!`)); await logTotalInvestors(); await logBalance(Issuer.address, totalSupply); - let transferTo = readlineSync.question('Enter beneficiary of tranfer: ', { - limit: function (input) { - return web3.utils.isAddress(input); - }, - limitMessage: "Must be a valid address" - }); await logBalance(transferTo, totalSupply); - let transferAmount = readlineSync.question('Enter amount of tokens to transfer: '); - let isTranferVerified = await securityToken.methods.verifyTransfer(Issuer.address, transferTo, web3.utils.toWei(transferAmount), web3.utils.fromAscii("")).call(); - if (isTranferVerified) { - let transferAction = securityToken.methods.transfer(transferTo, web3.utils.toWei(transferAmount)); - let receipt = await common.sendTransaction(transferAction); - let event = common.getEventFromLogs(securityToken._jsonInterface, receipt.logs, 'Transfer'); - console.log(chalk.green(`${event.from} transferred ${web3.utils.fromWei(event.value)} ${tokenSymbol} to ${event.to} successfully!`)); - await logTotalInvestors(); - await logBalance(Issuer.address, totalSupply); - await logBalance(transferTo, totalSupply); - } else { - console.log(chalk.red(`Transfer failed at verification. Please review the transfer restrictions.`)); - } - break; - case 'Forced transfers': - await forcedTransfers(); - break; - case 'Config existing modules': - await configExistingModules(nonArchivedModules); - break; - case 'Add new Transfer Manager module': - await addTransferManagerModule(); - break; - case 'Exit': - exit = true; - break - } + } else { + console.log(chalk.red(`Transfer failed at verification. Please review the transfer restrictions.`)); + } + break; + case 'Forced transfers': + await forcedTransfers(); + break; + case 'Config existing modules': + await configExistingModules(nonArchivedModules); + break; + case 'Add new Transfer Manager module': + await addTransferManagerModule(); + break; + case 'EXIT': + return; } + + await executeApp(); } async function forcedTransfers() { @@ -121,8 +152,8 @@ async function forcedTransfers() { if (controller == Issuer.address) { options.push('Force Transfer'); } - let index = readlineSync.keyInSelect(options, 'What do you want to do?', { cancel: 'Return' }); - let optionSelected = index != -1 ? options[index] : 'Return'; + let index = readlineSync.keyInSelect(options, 'What do you want to do?', { cancel: 'RETURN' }); + let optionSelected = index !== -1 ? options[index] : 'RETURN'; console.log('Selected:', optionSelected, '\n'); switch (optionSelected) { case 'Disable controller': @@ -181,14 +212,18 @@ async function forcedTransfers() { console.log(`Balance of ${from} after transfer: ${web3.utils.fromWei(await securityToken.methods.balanceOf(from).call())} ${tokenSymbol}`); console.log(`Balance of ${to} after transfer: ${web3.utils.fromWei(await securityToken.methods.balanceOf(to).call())} ${tokenSymbol}`); break; + case 'RETURN': + return; } + + await forcedTransfers(); } async function configExistingModules(tmModules) { let options = tmModules.map(m => `${m.name} at ${m.address}`); - let index = readlineSync.keyInSelect(options, 'Which module do you want to config? ', { cancel: 'Return' }); - console.log('Selected:', index != -1 ? options[index] : 'Return', '\n'); - let moduleNameSelected = index != -1 ? tmModules[index].name : 'Return'; + let index = readlineSync.keyInSelect(options, 'Which module do you want to config? ', { cancel: 'RETURN' }); + console.log('Selected:', index !== -1 ? options[index] : 'RETURN', '\n'); + let moduleNameSelected = index !== -1 ? tmModules[index].name : 'RETURN'; switch (moduleNameSelected) { case 'GeneralTransferManager': @@ -211,23 +246,20 @@ async function configExistingModules(tmModules) { currentTransferManager.setProvider(web3.currentProvider); await percentageTransferManager(); break; - case 'SingleTradeVolumeRestrictionTM': - //currentTransferManager = new web3.eth.Contract(abis.singleTradeVolumeRestrictionTM(), tmModules[index].address); - //currentTransferManager.setProvider(web3.currentProvider); - //await singleTradeVolumeRestrictionTM(); - console.log(chalk.red(` - ********************************* - This option is not yet available. - *********************************` - )); + case 'LockUpTransferManager': + currentTransferManager = new web3.eth.Contract(abis.lockUpTransferManager(), tmModules[index].address); + currentTransferManager.setProvider(web3.currentProvider); + await lockUpTransferManager(); + break; + case 'BlacklistTransferManager': + currentTransferManager = new web3.eth.Contract(abis.blacklistTransferManager(), tmModules[index].address); + currentTransferManager.setProvider(web3.currentProvider); + await blacklistTransferManager(); break; - case 'LookupVolumeRestrictionTM': - //await lookupVolumeRestrictionTM(); - console.log(chalk.red(` - ********************************* - This option is not yet available. - *********************************` - )); + case 'VolumeRestrictionTM': + currentTransferManager = new web3.eth.Contract(abis.volumeRestrictionTM(), tmModules[index].address); + currentTransferManager.setProvider(web3.currentProvider); + await volumeRestrictionTM(); break; } } @@ -240,7 +272,7 @@ async function addTransferManagerModule() { return web3.utils.hexToUtf8(await moduleFactory.methods.name().call()); })); - let index = readlineSync.keyInSelect(options, 'Which Transfer Manager module do you want to add? ', { cancel: 'Return' }); + let index = readlineSync.keyInSelect(options, 'Which Transfer Manager module do you want to add? ', { cancel: 'RETURN' }); if (index != -1 && readlineSync.keyInYNStrict(`Are you sure you want to add ${options[index]} module?`)) { let bytes = web3.utils.fromAscii('', 16); switch (options[index]) { @@ -260,56 +292,6 @@ async function addTransferManagerModule() { let configurePercentageTM = abis.percentageTransferManager().find(o => o.name === 'configure' && o.type === 'function'); bytes = web3.eth.abi.encodeFunctionCall(configurePercentageTM, [maxHolderPercentage, allowPercentagePrimaryIssuance]); break; - case 'SingleTradeVolumeRestrictionTM': - /* - let isTransferLimitInPercentage = !!readlineSync.keyInSelect(['In tokens', 'In percentage'], 'How do you want to set the transfer limit? ', {cancel: false}); - let globalTransferLimitInPercentageOrToken; - if (isTransferLimitInPercentage) { - globalTransferLimitInPercentageOrToken = toWeiPercentage(readlineSync.question('Enter the percentage for default limit: ', { - limit: function(input) { - return (parseInt(input) > 0 && parseInt(input) <= 100); - }, - limitMessage: "Must be greater than 0 and less than 100" - })); - } else { - globalTransferLimitInPercentageOrToken = web3.utils.toWei(readlineSync.question('Enter the amount of tokens for default limit: ', { - limit: function(input) { - return parseInt(input) > 0; - }, - limitMessage: "Must be greater than 0" - })); - } - let allowPrimaryIssuance = readlineSync.keyInYNStrict(`Do you want to allow all primary issuance transfers? `); - bytes = web3.eth.abi.encodeFunctionCall( { - name: 'configure', - type: 'function', - inputs: [ - { - type: 'bool', - name: '_isTransferLimitInPercentage' - },{ - type: 'uint256', - name: '_globalTransferLimitInPercentageOrToken' - },{ - type: 'bool', - name: '_isTransferLimitInPercentage' - } - ] - }, [isTransferLimitInPercentage, globalTransferLimitInPercentageOrToken, allowPrimaryIssuance]); - */ - console.log(chalk.red(` - ********************************* - This option is not yet available. - *********************************` - )); - break; - case 'LookupVolumeRestrictionTM': - console.log(chalk.red(` - ********************************* - This option is not yet available. - *********************************` - )); - break; } let selectedTMFactoryAddress = await contracts.getModuleFactoryAddressByName(securityToken.options.address, gbl.constants.MODULES_TYPES.TRANSFER, options[index]); let addModuleAction = securityToken.methods.addModule(selectedTMFactoryAddress, bytes, 0, 0); @@ -320,7 +302,7 @@ async function addTransferManagerModule() { } async function generalTransferManager() { - console.log(chalk.blue(`General Transfer Manager at ${currentTransferManager.options.address}`), '\n'); + console.log('\n', chalk.blue(`General Transfer Manager at ${currentTransferManager.options.address}`), '\n'); // Show current data let displayIssuanceAddress = await currentTransferManager.methods.issuanceAddress().call(); @@ -372,9 +354,9 @@ async function generalTransferManager() { options.push('Allow all burn transfers'); } - let index = readlineSync.keyInSelect(options, 'What do you want to do?', { cancel: 'Return' }); - let optionSelected = options[index]; - console.log('Selected:', index != -1 ? optionSelected : 'Return', '\n'); + let index = readlineSync.keyInSelect(options, 'What do you want to do?', { cancel: 'RETURN' }); + let optionSelected = index !== -1 ? options[index] : 'RETURN'; + console.log('Selected:', optionSelected, '\n'); switch (optionSelected) { case `Show investors`: console.log('***** List of investors on whitelist *****'); @@ -412,10 +394,10 @@ async function generalTransferManager() { limitMessage: "Must be a valid address" }); let now = Math.floor(Date.now() / 1000); - let fromTime = readlineSync.questionInt(`Enter the time(Unix Epoch time) when the sale lockup period ends and the investor can freely sell his tokens(now = ${now}): `, { defaultInput: now }); - let toTime = readlineSync.questionInt(`Enter the time(Unix Epoch time) when the purchase lockup period ends and the investor can freely purchase tokens from others(now = ${now}): `, { defaultInput: now }); + let fromTime = readlineSync.questionInt(`Enter the time (Unix Epoch time) when the sale lockup period ends and the investor can freely sell his tokens (now = ${now}): `, { defaultInput: now }); + let toTime = readlineSync.questionInt(`Enter the time (Unix Epoch time) when the purchase lockup period ends and the investor can freely purchase tokens from others (now = ${now}): `, { defaultInput: now }); let oneHourFromNow = Math.floor(Date.now() / 1000 + 3600); - let expiryTime = readlineSync.questionInt(`Enter the time till investors KYC will be validated(after that investor need to do re - KYC) (1 hour from now = ${oneHourFromNow}): `, { defaultInput: oneHourFromNow }); + let expiryTime = readlineSync.questionInt(`Enter the time till investors KYC will be validated (after that investor need to do re - KYC) (1 hour from now = ${oneHourFromNow}): `, { defaultInput: oneHourFromNow }); let canBuyFromSTO = readlineSync.keyInYNStrict('Is the investor a restricted investor?'); let modifyWhitelistAction = currentTransferManager.methods.modifyWhitelist(investor, fromTime, toTime, expiryTime, canBuyFromSTO); let modifyWhitelistReceipt = await common.sendTransaction(modifyWhitelistAction); @@ -514,7 +496,11 @@ async function generalTransferManager() { console.log(chalk.green(`The burning mechanism is deactivated!`)); } break; + case 'RETURN': + return; } + + await generalTransferManager(); } function showWhitelistTable(investorsArray, fromTimeArray, toTimeArray, expiryTimeArray, canBuyFromSTOArray) { @@ -532,17 +518,27 @@ function showWhitelistTable(investorsArray, fromTimeArray, toTimeArray, expiryTi console.log(table(dataTable)); } -async function modifyWhitelistInBatch() { - let csvFilePath = readlineSync.question(`Enter the path for csv data file (${WHITELIST_DATA_CSV}): `, { - defaultInput: WHITELIST_DATA_CSV - }); - let batchSize = readlineSync.question(`Enter the max number of records per transaction or batch size (${gbl.constants.DEFAULT_BATCH_SIZE}): `, { - limit: function (input) { - return parseInt(input) > 0; - }, - limitMessage: 'Must be greater than 0', - defaultInput: gbl.constants.DEFAULT_BATCH_SIZE - }); +async function modifyWhitelistInBatch(_csvFilePath, _batchSize) { + let csvFilePath; + if (typeof _csvFilePath === 'undefined') { + csvFilePath = readlineSync.question(`Enter the path for csv data file (${WHITELIST_DATA_CSV}): `, { + defaultInput: WHITELIST_DATA_CSV + }); + } else { + csvFilePath = _csvFilePath; + } + let batchSize; + if (typeof _batchSize === 'undefined') { + batchSize = readlineSync.question(`Enter the max number of records per transaction or batch size (${gbl.constants.DEFAULT_BATCH_SIZE}): `, { + limit: function (input) { + return parseInt(input) > 0; + }, + limitMessage: 'Must be greater than 0', + defaultInput: gbl.constants.DEFAULT_BATCH_SIZE + }); + } else { + batchSize = _batchSize; + } let parsedData = csvParse(csvFilePath); let validData = parsedData.filter(row => web3.utils.isAddress(row[0]) && @@ -559,7 +555,7 @@ async function modifyWhitelistInBatch() { let [investorArray, fromTimesArray, toTimesArray, expiryTimeArray, canBuyFromSTOArray] = common.transposeBatches(batches); for (let batch = 0; batch < batches.length; batch++) { console.log(`Batch ${batch + 1} - Attempting to modify whitelist to accounts: \n\n`, investorArray[batch], '\n'); - let action = await currentTransferManager.methods.modifyWhitelistMulti(investorArray[batch], fromTimesArray[batch], toTimesArray[batch], expiryTimeArray[batch], canBuyFromSTOArray[batch]); + let action = currentTransferManager.methods.modifyWhitelistMulti(investorArray[batch], fromTimesArray[batch], toTimesArray[batch], expiryTimeArray[batch], canBuyFromSTOArray[batch]); let receipt = await common.sendTransaction(action); console.log(chalk.green('Modify whitelist transaction was successful.')); console.log(`${receipt.gasUsed} gas used.Spent: ${web3.utils.fromWei((new web3.utils.BN(receipt.gasUsed)).mul(new web3.utils.BN(defaultGasPrice)))} ETH`); @@ -567,228 +563,446 @@ async function modifyWhitelistInBatch() { } async function manualApprovalTransferManager() { - console.log(chalk.blue(`Manual Approval Transfer Manager at ${currentTransferManager.options.address} `), '\n'); + console.log('\n', chalk.blue(`Manual Approval Transfer Manager at ${currentTransferManager.options.address} `), '\n'); + + let totalApprovals = await currentTransferManager.methods.getTotalApprovalsLength().call(); + console.log(`- Current active approvals: ${totalApprovals}`); - let options = ['Check manual approval', 'Add manual approval', 'Revoke manual approval', - 'Check manual blocking', 'Add manual blocking', 'Revoke manual blocking']; + let matmOptions = [ + MATM_MENU_ADD, + MATM_MENU_MANAGE, + MATM_MENU_EXPLORE, + MATM_MENU_OPERATE + ]; + + let index = readlineSync.keyInSelect(matmOptions, 'What do you want to do?', { + cancel: 'RETURN' + }); + let optionSelected = index != -1 ? matmOptions[index] : 'RETURN'; + console.log('Selected:', optionSelected, '\n'); - let index = readlineSync.keyInSelect(options, 'What do you want to do?', { cancel: 'Return' }); - let optionSelected = options[index]; - console.log('Selected:', index != -1 ? optionSelected : 'Return', '\n'); - let from; - let to; switch (optionSelected) { - case 'Check manual approval': - from = readlineSync.question('Enter the address from which transfers would be approved: ', { - limit: function (input) { - return web3.utils.isAddress(input); - }, - limitMessage: "Must be a valid address" - }); - to = readlineSync.question('Enter the address to which transfers would be approved: ', { - limit: function (input) { - return web3.utils.isAddress(input); - }, - limitMessage: "Must be a valid address" - }); - console.log(); - let manualApproval = await getManualApproval(from, to); - if (manualApproval) { - console.log(`Manual approval found!`); - console.log(`Allowance: ${web3.utils.fromWei(manualApproval.allowance)}`); - console.log(`Expiry time: ${moment.unix(manualApproval.expiryTime).format('MMMM Do YYYY, HH:mm:ss')}`); - } else { - console.log(chalk.yellow(`There are no manual approvals from ${from} to ${to}.`)); - } + case MATM_MENU_ADD: + await matmAdd(); break; - case 'Add manual approval': - from = readlineSync.question('Enter the address from which transfers will be approved: ', { - limit: function (input) { - return web3.utils.isAddress(input); - }, - limitMessage: "Must be a valid address" - }); - to = readlineSync.question('Enter the address to which transfers will be approved: ', { - limit: function (input) { - return web3.utils.isAddress(input); - }, - limitMessage: "Must be a valid address" - }); - if (!await getManualApproval(from, to)) { - let allowance = readlineSync.question('Enter the amount of tokens which will be approved: '); - let oneHourFromNow = Math.floor(Date.now() / 1000 + 3600); - let expiryTime = readlineSync.questionInt(`Enter the time(Unix Epoch time) until which the transfer is allowed(1 hour from now = ${oneHourFromNow}): `, { defaultInput: oneHourFromNow }); - let addManualApprovalAction = currentTransferManager.methods.addManualApproval(from, to, web3.utils.toWei(allowance), expiryTime); - let addManualApprovalReceipt = await common.sendTransaction(addManualApprovalAction); - let addManualApprovalEvent = common.getEventFromLogs(currentTransferManager._jsonInterface, addManualApprovalReceipt.logs, 'AddManualApproval'); - console.log(chalk.green(`Manual approval has been added successfully!`)); - } else { - console.log(chalk.red(`A manual approval already exists from ${from} to ${to}.Revoke it first if you want to add a new one.`)); - } + case MATM_MENU_MANAGE: + await matmManage(); break; - case 'Revoke manual approval': - from = readlineSync.question('Enter the address from which transfers were approved: ', { - limit: function (input) { - return web3.utils.isAddress(input); - }, - limitMessage: "Must be a valid address" - }); - to = readlineSync.question('Enter the address to which transfers were approved: ', { - limit: function (input) { - return web3.utils.isAddress(input); - }, - limitMessage: "Must be a valid address" - }); - if (await getManualApproval(from, to)) { - let revokeManualApprovalAction = currentTransferManager.methods.revokeManualApproval(from, to); - let revokeManualApprovalReceipt = await common.sendTransaction(revokeManualApprovalAction); - let revokeManualApprovalEvent = common.getEventFromLogs(currentTransferManager._jsonInterface, revokeManualApprovalReceipt.logs, 'RevokeManualApproval'); - console.log(chalk.green(`Manual approval has been revoked successfully!`)); - } else { - console.log(chalk.red(`Manual approval from ${from} to ${to} does not exist.`)); - } + case MATM_MENU_EXPLORE: + await matmExplore(); break; - case 'Check manual blocking': - from = readlineSync.question('Enter the address from which transfers would be blocked: ', { - limit: function (input) { - return web3.utils.isAddress(input); - }, - limitMessage: "Must be a valid address" - }); - to = readlineSync.question('Enter the address to which transfers would be blocked: ', { - limit: function (input) { - return web3.utils.isAddress(input); - }, - limitMessage: "Must be a valid address" - }); - console.log(); - let manualBlocking = await getManualBlocking(from, to); - if (manualBlocking) { - console.log(`Manual blocking found!`); - console.log(`Expiry time: ${moment.unix(manualBlocking).format('MMMM Do YYYY, HH:mm:ss')}; `) - } else { - console.log(chalk.yellow(`There are no manual blockings from ${from} to ${to}.`)); - } + case MATM_MENU_OPERATE: + await matmOperate(); break; - case 'Add manual blocking': - from = readlineSync.question('Enter the address from which transfers will be blocked: ', { - limit: function (input) { - return web3.utils.isAddress(input); - }, - limitMessage: "Must be a valid address" - }); - to = readlineSync.question('Enter the address to which transfers will be blocked: ', { - limit: function (input) { - return web3.utils.isAddress(input); - }, - limitMessage: "Must be a valid address" + case 'RETURN': + return; + } + + await manualApprovalTransferManager(); +} + +async function matmAdd() { + let from = readlineSync.question('Enter the address from which transfers will be approved: ', { + limit: function (input) { + return web3.utils.isAddress(input); + }, + limitMessage: "Must be a valid address" + }); + let to = readlineSync.question('Enter the address to which transfers will be approved: ', { + limit: function (input) { + return web3.utils.isAddress(input); + }, + limitMessage: "Must be a valid address" + }); + if (!await getManualApproval(from, to)) { + let description = readlineSync.question('Enter the description for the manual approval: ', { + limit: function (input) { + return input != "" && getBinarySize(input) < 33 + }, + limitMessage: "Description is required" + }); + let allowance = readlineSync.question('Enter the amount of tokens which will be approved: '); + let oneHourFromNow = Math.floor(Date.now() / 1000 + 3600); + let expiryTime = readlineSync.questionInt(`Enter the time (Unix Epoch time) until which the transfer is allowed (1 hour from now = ${oneHourFromNow}): `, { defaultInput: oneHourFromNow }); + let addManualApprovalAction = currentTransferManager.methods.addManualApproval(from, to, web3.utils.toWei(allowance), expiryTime, web3.utils.fromAscii(description)); + let addManualApprovalReceipt = await common.sendTransaction(addManualApprovalAction); + let addManualApprovalEvent = common.getEventFromLogs(currentTransferManager._jsonInterface, addManualApprovalReceipt.logs, 'AddManualApproval'); + console.log(chalk.green(`Manual approval has been added successfully!`)); + } else { + console.log(chalk.red(`A manual approval already exists from ${from} to ${to}. Revoke it first if you want to add a new one or modify the existing one.`)); + } +} + +async function matmManage() { + + let manageOptions = [ + MATM_MENU_MANAGE_INCRESE, + MATM_MENU_MANAGE_DECREASE, + MATM_MENU_MANAGE_TIME, + MATM_MENU_MANAGE_REVOKE + ]; + + let getApprovals = await getApprovalsArray(); + + if (getApprovals.length > 0) { + let options = [] + getApprovals.forEach((item) => { + options.push(`${web3.utils.toAscii(item.description)}\n From: ${item.from}\n To: ${item.to}\n Amount: ${web3.utils.fromWei(item.allowance)} ${tokenSymbol}\n Expiry date: ${moment.unix(item.expiryTime).format('MM/DD/YYYY HH:mm')}\n`) + }) + + let index = readlineSync.keyInSelect(options, 'Select an existing approval: ', { + cancel: 'RETURN' + }); + let optionSelected = index != -1 ? options[index] : 'RETURN'; + console.log('Selected:', optionSelected, '\n'); + + if (optionSelected !== 'RETURN') { + let selectedApproval = getApprovals[index]; + + let index2 = readlineSync.keyInSelect(manageOptions, 'What do you want to do?', { + cancel: 'RETURN' }); - if (!await getManualBlocking(from, to)) { - let oneHourFromNow = Math.floor(Date.now() / 1000 + 3600); - let expiryTime = readlineSync.questionInt(`Enter the time(Unix Epoch time) until which the transfer is blocked(1 hour from now = ${oneHourFromNow}): `, { defaultInput: oneHourFromNow }); - let addManualBlockingAction = currentTransferManager.methods.addManualBlocking(from, to, expiryTime); - let addManualBlockingReceipt = await common.sendTransaction(addManualBlockingAction); - let addManualBlockingEvent = common.getEventFromLogs(currentTransferManager._jsonInterface, addManualBlockingReceipt.logs, 'AddManualBlocking'); - console.log(chalk.green(`Manual blocking has been added successfully!`)); - } else { - console.log(chalk.red(`A manual blocking already exists from ${from} to ${to}.Revoke it first if you want to add a new one.`)); + let optionSelected2 = index2 != -1 ? manageOptions[index2] : 'RETURN'; + console.log('Selected:', optionSelected2, '\n'); + + if (optionSelected2 !== 'RETURN') { + switch (optionSelected2) { + case MATM_MENU_MANAGE_INCRESE: + await matmManageIncrese(selectedApproval); + break; + case MATM_MENU_MANAGE_DECREASE: + await matmManageDecrease(selectedApproval); + break; + case MATM_MENU_MANAGE_TIME: + await matmManageTimeOrDescription(selectedApproval); + break; + case MATM_MENU_MANAGE_REVOKE: + await matmManageRevoke(selectedApproval); + break; + } } + } + } else { + console.log(chalk.yellow(`There are no existing approvals to show`)); + } +} + +async function matmExplore() { + let getApprovals = await getApprovalsArray(); + getApprovals.forEach((item) => { + printMatmRow(item.from, item.to, item.allowance, item.expiryTime, item.description); + }) +} + +async function matmOperate() { + let operateOptions = [ + MATM_MENU_OPERATE_ADD, + MATM_MENU_OPERATE_MODIFY, + MATM_MENU_OPERATE_REVOKE + ]; + + let index = readlineSync.keyInSelect(operateOptions, 'What do you want to do?', { + cancel: 'RETURN' + }); + let optionSelected = index != -1 ? operateOptions[index] : 'RETURN'; + console.log('Selected:', optionSelected, '\n'); + + switch (optionSelected) { + case MATM_MENU_OPERATE_ADD: + await addManualApproveInBatch(); break; - case 'Revoke manual blocking': - from = readlineSync.question('Enter the address from which transfers were blocked: ', { - limit: function (input) { - return web3.utils.isAddress(input); - }, - limitMessage: "Must be a valid address" - }); - to = readlineSync.question('Enter the address to which transfers were blocked: ', { - limit: function (input) { - return web3.utils.isAddress(input); - }, - limitMessage: "Must be a valid address" - }); - if (await getManualBlocking(from, to)) { - let revokeManualBlockingAction = currentTransferManager.methods.revokeManualBlocking(from, to); - let revokeManualBlockingReceipt = await common.sendTransaction(revokeManualBlockingAction); - let revokeManualBlockingEvent = common.getEventFromLogs(currentTransferManager._jsonInterface, revokeManualBlockingReceipt.logs, 'RevokeManualBlocking'); - console.log(chalk.green(`Manual blocking has been revoked successfully!`)); - } else { - console.log(chalk.red(`Manual blocking from ${from} to ${to} does not exist.`)); - } + case MATM_MENU_OPERATE_MODIFY: + await modifyManualApproveInBatch(); + break; + case MATM_MENU_OPERATE_REVOKE: + await revokeManualApproveInBatch(); break; } } -async function getManualApproval(_from, _to) { - let result = null; +async function matmManageIncrese(selectedApproval) { + let allowance = readlineSync.question(`Enter a value to increase allowance (current allowance = ${web3.utils.fromWei(selectedApproval.allowance)}): `, { + limit: function (input) { + return parseFloat(input) > 0 + }, + limitMessage: "Amount must be bigger than 0" + }); - let manualApproval = await currentTransferManager.methods.manualApprovals(_from, _to).call(); - if (manualApproval.expiryTime !== "0") { - result = manualApproval; + if (readlineSync.keyInYNStrict(`Do you want to modify expiry time or description?`)) { + let { expiryTime, description } = readExpiryTimeAndDescription(selectedApproval); + selectedApproval.expiryTime = expiryTime; + selectedApproval.description = web3.utils.fromAscii(description); } - return result; + let modifyManualApprovalAction = currentTransferManager.methods.modifyManualApproval(selectedApproval.from, selectedApproval.to, parseInt(selectedApproval.expiryTime), web3.utils.toWei(allowance), selectedApproval.description, 1); + await common.sendTransaction(modifyManualApprovalAction); + console.log(chalk.green(`The approval allowance has been increased successfully!`)); } -async function getManualBlocking(_from, _to) { - let result = null; +async function matmManageDecrease(selectedApproval) { + let allowance = readlineSync.question(`Enter a value to decrease allowance (current allowance = ${web3.utils.fromWei(selectedApproval.allowance)}): `, { + limit: function (input) { + return parseFloat(input) > 0 + }, + limitMessage: "Amount must be bigger than 0" + }); - let manualBlocking = await currentTransferManager.methods.manualBlockings(_from, _to).call(); - if (manualBlocking !== "0") { - result = manualBlocking; + if (readlineSync.keyInYNStrict(`Do you want to modify expiry time or description?`)) { + let { expiryTime, description } = readExpiryTimeAndDescription(selectedApproval); + selectedApproval.expiryTime = expiryTime; + selectedApproval.description = web3.utils.fromAscii(description); } - return result; + let modifyManualApprovalAction = currentTransferManager.methods.modifyManualApproval(selectedApproval.from, selectedApproval.to, parseInt(selectedApproval.expiryTime), web3.utils.toWei(allowance), selectedApproval.description, 0); + await common.sendTransaction(modifyManualApprovalAction); + console.log(chalk.green(`The approval allowance has been decreased successfully!`)); } -async function countTransferManager() { - console.log(chalk.blue(`Count Transfer Manager at ${currentTransferManager.options.address}`), '\n'); +async function matmManageTimeOrDescription(selectedApproval) { + let { expiryTime, description } = readExpiryTimeAndDescription(selectedApproval); - // Show current data - let displayMaxHolderCount = await currentTransferManager.methods.maxHolderCount().call(); + let modifyManualApprovalAction = currentTransferManager.methods.modifyManualApproval(selectedApproval.from, selectedApproval.to, parseInt(expiryTime), selectedApproval.allowance, web3.utils.fromAscii(description), 2); + await common.sendTransaction(modifyManualApprovalAction); + console.log(chalk.green(`The approval expiry time has been modified successfully!`)); +} - console.log(`- Max holder count: ${displayMaxHolderCount}`); +function readExpiryTimeAndDescription(selectedApproval) { + let expiryTime = readlineSync.questionInt(`Enter the new expiry time (Unix Epoch time) until which the transfer is allowed or leave empty to keep the current (${selectedApproval.expiryTime}): `, { + limit: function (input) { + return parseFloat(input) > 0; + }, + limitMessage: "Enter Unix Epoch time", + defaultInput: selectedApproval.expiryTime + }); + let description = readlineSync.question(`Enter the new description for the manual approval or leave empty to keep the current (${web3.utils.toAscii(selectedApproval.description)}): `, { + limit: function (input) { + return input != "" && getBinarySize(input) < 33; + }, + limitMessage: "Description is required" + }); + return { expiryTime, description }; +} - let options = ['Change max holder count'] - let index = readlineSync.keyInSelect(options, 'What do you want to do?', { cancel: 'Return' }); - let optionSelected = options[index]; - console.log('Selected:', index != -1 ? optionSelected : 'Return', '\n'); - switch (optionSelected) { - case 'Change max holder count': - let maxHolderCount = readlineSync.question('Enter the maximum no. of holders the SecurityToken is allowed to have: '); - let changeHolderCountAction = currentTransferManager.methods.changeHolderCount(maxHolderCount); - let changeHolderCountReceipt = await common.sendTransaction(changeHolderCountAction); - let changeHolderCountEvent = common.getEventFromLogs(currentTransferManager._jsonInterface, changeHolderCountReceipt.logs, 'ModifyHolderCount'); - console.log(chalk.green(`Max holder count has been set to ${changeHolderCountEvent._newHolderCount} sucessfully!`)); - break; +async function matmManageRevoke(selectedApproval) { + let modifyManualApprovalAction = currentTransferManager.methods.revokeManualApproval(selectedApproval.from, selectedApproval.to); + await common.sendTransaction(modifyManualApprovalAction); + console.log(chalk.green(`The approval has been revoked successfully!`)); +} + +async function getApprovalsArray() { + let address = readlineSync.question('Enter an address to filter or leave empty to get all the approvals: ', { + limit: function (input) { + return web3.utils.isAddress(input); + }, + limitMessage: "Must be a valid address", + defaultInput: gbl.constants.ADDRESS_ZERO + }); + if (address == gbl.constants.ADDRESS_ZERO) { + return await getApprovals(); + } else { + let approvals = await getApprovalsToAnAddress(address); + if (!approvals.length) { + console.log(chalk.red(`\nThe address is not listed\n`)) + } + return approvals; } } -async function percentageTransferManager() { - console.log(chalk.blue(`Percentage Transfer Manager at ${currentTransferManager.options.address}`), '\n'); +function printMatmRow(from, to, allowance, time, description) { + console.log(`\nDescription: ${web3.utils.toAscii(description)}\nFrom ${from} to ${to}\nAllowance: ${web3.utils.fromWei(allowance)}\nExpiry time: ${moment.unix(time).format('MMMM Do YYYY HH:mm')}\n`); +} - // Show current data - let displayMaxHolderPercentage = await currentTransferManager.methods.maxHolderPercentage().call(); - let displayAllowPrimaryIssuance = await currentTransferManager.methods.allowPrimaryIssuance().call(); +async function getApprovals() { + function ApprovalDetail(_from, _to, _allowance, _expiryTime, _description) { + this.from = _from; + this.to = _to; + this.allowance = _allowance; + this.expiryTime = _expiryTime; + this.description = _description; + } - console.log(`- Max holder percentage: ${fromWeiPercentage(displayMaxHolderPercentage)}%`); - console.log(`- Allow primary issuance: ${displayAllowPrimaryIssuance ? `YES` : `NO`}`); + let results = []; + let approvalDetails = await currentTransferManager.methods.getAllApprovals().call(); + for (let i = 0; i < approvalDetails[0].length; i++) { + results.push(new ApprovalDetail(approvalDetails[0][i], approvalDetails[1][i], approvalDetails[2][i], approvalDetails[3][i], approvalDetails[4][i])); + } + return results; +} - let options = ['Change max holder percentage', 'Check if investor is whitelisted', 'Modify whitelist', 'Modify whitelist from CSV']; - if (displayAllowPrimaryIssuance) { - options.push('Disallow primary issuance'); - } else { - options.push('Allow primary issuance'); +async function getApprovalsToAnAddress(address) { + function ApprovalDetail(_from, _to, _allowance, _expiryTime, _description) { + this.from = _from; + this.to = _to; + this.allowance = _allowance; + this.expiryTime = _expiryTime; + this.description = _description; } - let index = readlineSync.keyInSelect(options, 'What do you want to do?', { cancel: 'Return' }); - let optionSelected = options[index]; - console.log('Selected:', index != -1 ? optionSelected : 'Return', '\n'); - switch (optionSelected) { - case 'Change max holder percentage': - let maxHolderPercentage = toWeiPercentage(readlineSync.question('Enter the maximum amount of tokens in percentage that an investor can hold: ', { - limit: function (input) { - return (parseInt(input) > 0 && parseInt(input) <= 100); - }, + + let results = []; + let approvals = await currentTransferManager.methods.getActiveApprovalsToUser(address).call(); + for (let i = 0; i < approvals[0].length; i++) { + results.push(new ApprovalDetail(approvals[0][i], approvals[1][i], approvals[2][i], approvals[3][i], approvals[4][i])); + } + return results; +} + +async function getManualApproval(_from, _to) { + let result = null; + + let manualApproval = await currentTransferManager.methods.getApprovalDetails(_from, _to).call(); + if ((manualApproval[0] >= new Date()) && (manualApproval[1] != 0)) { + result = manualApproval; + } + return result; +} + +async function matmGenericCsv(path, f) { + let csvFilePath = readlineSync.question(`Enter the path for csv data file (${path}): `, { + defaultInput: path + }); + let batchSize = readlineSync.question(`Enter the max number of records per transaction or batch size (${gbl.constants.DEFAULT_BATCH_SIZE}): `, { + limit: function (input) { + return parseInt(input) > 0; + }, + limitMessage: 'Must be greater than 0', + defaultInput: gbl.constants.DEFAULT_BATCH_SIZE + }); + let parsedData = csvParse(csvFilePath); + let validData = parsedData.filter(row => f(row)); + let invalidRows = parsedData.filter(row => !validData.includes(row)); + if (invalidRows.length > 0) { + console.log(chalk.red(`The following lines from csv file are not valid: ${invalidRows.map(r => parsedData.indexOf(r) + 1).join(',')} `)); + } + return common.splitIntoBatches(validData, batchSize); +} + +async function addManualApproveInBatch() { + + var f = (row) => { + return (web3.utils.isAddress(row[0]) && + web3.utils.isAddress(row[1]) && + parseFloat(row[2]) > 0 && + moment.unix(row[3]).isValid() && + typeof row[4] === 'string' && + getBinarySize(row[4]) < 33) + } + + let batches = await matmGenericCsv(ADD_MANUAL_APPROVAL_DATA_CSV, f) + + let [fromArray, toArray, allowanceArray, expiryArray, descriptionArray] = common.transposeBatches(batches); + for (let batch = 0; batch < batches.length; batch++) { + console.log(`Batch ${batch + 1} - Attempting to add manual approvals: \n\n`, descriptionArray[batch], '\n'); + descriptionArray[batch] = descriptionArray[batch].map(d => web3.utils.fromAscii(d)); + allowanceArray[batch] = allowanceArray[batch].map(a => web3.utils.toWei(new web3.utils.BN(a))); + let action = await currentTransferManager.methods.addManualApprovalMulti(fromArray[batch], toArray[batch], allowanceArray[batch], expiryArray[batch], descriptionArray[batch]); + let receipt = await common.sendTransaction(action); + console.log(chalk.green('Add multiple manual approvals transaction was successful.')); + console.log(`${receipt.gasUsed} gas used.Spent: ${web3.utils.fromWei((new web3.utils.BN(receipt.gasUsed)).mul(new web3.utils.BN(defaultGasPrice)))} ETH`); + } +} + +async function revokeManualApproveInBatch() { + + var f = (row) => { + return (web3.utils.isAddress(row[0]) && + web3.utils.isAddress(row[1])) + } + + let batches = await matmGenericCsv(REVOKE_MANUAL_APPROVAL_DATA_CSV, f) + + let [fromArray, toArray] = common.transposeBatches(batches); + for (let batch = 0; batch < batches.length; batch++) { + console.log(`Batch ${batch + 1} - Attempting to revoke manual approvals`, '\n'); + let action = await currentTransferManager.methods.revokeManualApprovalMulti(fromArray[batch], toArray[batch]); + let receipt = await common.sendTransaction(action); + console.log(chalk.green('Revoke multip;e manual approvals transaction was successful.')); + console.log(`${receipt.gasUsed} gas used.Spent: ${web3.utils.fromWei((new web3.utils.BN(receipt.gasUsed)).mul(new web3.utils.BN(defaultGasPrice)))} ETH`); + } +} + +async function modifyManualApproveInBatch() { + + var f = (row) => { + return (web3.utils.isAddress(row[0]) && + web3.utils.isAddress(row[1]) && + moment.unix(row[2]).isValid() && + parseFloat(row[3]) > 0 && + typeof row[4] === 'string' && + getBinarySize(row[4]) < 33 && + typeof parseInt(row[5])) === 'number' + } + + let batches = await matmGenericCsv(MODIFY_MANUAL_APPROVAL_DATA_CSV, f) + + let [fromArray, toArray, expiryArray, allowanceArray, descriptionArray, changesArray] = common.transposeBatches(batches); + for (let batch = 0; batch < batches.length; batch++) { + console.log(`Batch ${batch + 1} - Attempting to modify manual approvals: \n\n`, descriptionArray[batch], '\n'); + descriptionArray[batch] = descriptionArray[batch].map(d => web3.utils.fromAscii(d)); + allowanceArray[batch] = allowanceArray[batch].map(a => web3.utils.toWei(new web3.utils.BN(a))); + changesArray[batch] = changesArray[batch].map(c => parseInt(c)); + let action = await currentTransferManager.methods.modifyManualApprovalMulti(fromArray[batch], toArray[batch], expiryArray[batch], allowanceArray[batch], descriptionArray[batch], changesArray[batch]); + let receipt = await common.sendTransaction(action); + console.log(chalk.green('Modify multiple manual approvals transaction was successful.')); + console.log(`${receipt.gasUsed} gas used.Spent: ${web3.utils.fromWei((new web3.utils.BN(receipt.gasUsed)).mul(new web3.utils.BN(defaultGasPrice)))} ETH`); + } +} + +function getBinarySize(string) { + return Buffer.byteLength(string, 'utf8'); +} + +async function countTransferManager() { + console.log('\n', chalk.blue(`Count Transfer Manager at ${currentTransferManager.options.address}`), '\n'); + + // Show current data + let displayMaxHolderCount = await currentTransferManager.methods.maxHolderCount().call(); + + console.log(`- Max holder count: ${displayMaxHolderCount}`); + + let options = ['Change max holder count'] + let index = readlineSync.keyInSelect(options, 'What do you want to do?', { cancel: 'RETURN' }); + let optionSelected = index !== -1 ? options[index] : 'RETURN'; + console.log('Selected:', optionSelected, '\n'); + switch (optionSelected) { + case 'Change max holder count': + let maxHolderCount = readlineSync.question('Enter the maximum no. of holders the SecurityToken is allowed to have: '); + let changeHolderCountAction = currentTransferManager.methods.changeHolderCount(maxHolderCount); + let changeHolderCountReceipt = await common.sendTransaction(changeHolderCountAction); + let changeHolderCountEvent = common.getEventFromLogs(currentTransferManager._jsonInterface, changeHolderCountReceipt.logs, 'ModifyHolderCount'); + console.log(chalk.green(`Max holder count has been set to ${changeHolderCountEvent._newHolderCount} sucessfully!`)); + break; + case 'RETURN': + return; + } + + await countTransferManager(); +} + +async function percentageTransferManager() { + console.log('\n', chalk.blue(`Percentage Transfer Manager at ${currentTransferManager.options.address}`), '\n'); + + // Show current data + let displayMaxHolderPercentage = await currentTransferManager.methods.maxHolderPercentage().call(); + let displayAllowPrimaryIssuance = await currentTransferManager.methods.allowPrimaryIssuance().call(); + + console.log(`- Max holder percentage: ${fromWeiPercentage(displayMaxHolderPercentage)}%`); + console.log(`- Allow primary issuance: ${displayAllowPrimaryIssuance ? `YES` : `NO`}`); + + let options = ['Change max holder percentage', 'Check if investor is whitelisted', 'Modify whitelist', 'Modify whitelist from CSV']; + if (displayAllowPrimaryIssuance) { + options.push('Disallow primary issuance'); + } else { + options.push('Allow primary issuance'); + } + let index = readlineSync.keyInSelect(options, 'What do you want to do?', { cancel: 'RETURN' }); + let optionSelected = index !== -1 ? options[index] : 'RETURN'; + console.log('Selected:', optionSelected, '\n'); + switch (optionSelected) { + case 'Change max holder percentage': + let maxHolderPercentage = toWeiPercentage(readlineSync.question('Enter the maximum amount of tokens in percentage that an investor can hold: ', { + limit: function (input) { + return (parseInt(input) > 0 && parseInt(input) <= 100); + }, limitMessage: "Must be greater than 0 and less than 100" })); let changeHolderPercentageAction = currentTransferManager.methods.changeHolderPercentage(maxHolderPercentage); @@ -799,256 +1013,1589 @@ async function percentageTransferManager() { case 'Check if investor is whitelisted': let investorToCheck = readlineSync.question('Enter the address of the investor: ', { limit: function (input) { - return web3.utils.isAddress(input); + return web3.utils.isAddress(input); + }, + limitMessage: "Must be a valid address" + }); + let isWhitelisted = await currentTransferManager.methods.whitelist(investorToCheck).call(); + if (isWhitelisted) { + console.log(chalk.green(`${investorToCheck} is whitelisted!`)); + } else { + console.log(chalk.yellow(`${investorToCheck} is not whitelisted!`)); + } + break; + case 'Modify whitelist': + let valid = !!readlineSync.keyInSelect(['Remove investor from whitelist', 'Add investor to whitelist'], 'How do you want to do? ', { cancel: false }); + let investorToWhitelist = readlineSync.question('Enter the address of the investor: ', { + limit: function (input) { + return web3.utils.isAddress(input); + }, + limitMessage: "Must be a valid address" + }); + let modifyWhitelistAction = currentTransferManager.methods.modifyWhitelist(investorToWhitelist, valid); + let modifyWhitelistReceipt = await common.sendTransaction(modifyWhitelistAction); + let modifyWhitelistEvent = common.getEventFromLogs(currentTransferManager._jsonInterface, modifyWhitelistReceipt.logs, 'ModifyWhitelist'); + if (modifyWhitelistEvent._valid) { + console.log(chalk.green(`${modifyWhitelistEvent._investor} has been added to the whitelist sucessfully!`)); + } else { + console.log(chalk.green(`${modifyWhitelistEvent._investor} has been removed from the whitelist sucessfully!`)); + } + break; + case 'Modify whitelist from CSV': + let csvFilePath = readlineSync.question(`Enter the path for csv data file (${PERCENTAGE_WHITELIST_DATA_CSV}): `, { + defaultInput: PERCENTAGE_WHITELIST_DATA_CSV + }); + let batchSize = readlineSync.question(`Enter the max number of records per transaction or batch size (${gbl.constants.DEFAULT_BATCH_SIZE}): `, { + limit: function (input) { + return parseInt(input) > 0; + }, + limitMessage: 'Must be greater than 0', + defaultInput: gbl.constants.DEFAULT_BATCH_SIZE + }); + let parsedData = csvParse(csvFilePath); + let validData = parsedData.filter(row => web3.utils.isAddress(row[0]) && typeof row[1] === 'boolean'); + let invalidRows = parsedData.filter(row => !validData.includes(row)); + if (invalidRows.length > 0) { + console.log(chalk.red(`The following lines from csv file are not valid: ${invalidRows.map(r => parsedData.indexOf(r) + 1).join(',')}`)); + } + let batches = common.splitIntoBatches(validData, batchSize); + let [investorArray, isWhitelistedArray] = common.transposeBatches(batches); + for (let batch = 0; batch < batches.length; batch++) { + console.log(`Batch ${batch + 1} - Attempting to modify whitelist accounts:\n\n`, investorArray[batch], '\n'); + let action = await currentTransferManager.methods.modifyWhitelistMulti(investorArray[batch], isWhitelistedArray[batch]); + let receipt = await common.sendTransaction(action); + console.log(chalk.green('Modify whitelist transaction was successful.')); + console.log(`${receipt.gasUsed} gas used. Spent: ${web3.utils.fromWei((new web3.utils.BN(receipt.gasUsed)).mul(new web3.utils.BN(defaultGasPrice)))} ETH`); + } + break; + case 'Allow primary issuance': + case 'Disallow primary issuance': + let setAllowPrimaryIssuanceAction = currentTransferManager.methods.setAllowPrimaryIssuance(!displayAllowPrimaryIssuance); + let setAllowPrimaryIssuanceReceipt = await common.sendTransaction(setAllowPrimaryIssuanceAction); + let setAllowPrimaryIssuanceEvent = common.getEventFromLogs(currentTransferManager._jsonInterface, setAllowPrimaryIssuanceReceipt.logs, 'SetAllowPrimaryIssuance'); + if (setAllowPrimaryIssuanceEvent._allowPrimaryIssuance) { + console.log(chalk.green(`Transactions which are part of the primary issuance will be ignored!`)); + } else { + console.log(chalk.green(`Transactions which are part of the primary issuance will NOT be ignored!`)); + } + break; + } + + await percentageTransferManager(); +} + +async function blacklistTransferManager() { + console.log('\n', chalk.blue(`Blacklist Transfer Manager at ${currentTransferManager.options.address}`), '\n'); + + let currentBlacklists = await currentTransferManager.methods.getAllBlacklists().call(); + console.log(`- Blacklists: ${currentBlacklists.length}`); + + let options = ['Add new blacklist']; + if (currentBlacklists.length > 0) { + options.push('Manage existing blacklist', 'Explore account'); + } + options.push('Delete investors from all blacklists', 'Operate with multiple blacklists'); + + let index = readlineSync.keyInSelect(options, 'What do you want to do?', { cancel: "RETURN" }); + let optionSelected = index !== -1 ? options[index] : 'RETURN'; + console.log('Selected:', optionSelected, '\n'); + switch (optionSelected) { + case 'Add new blacklist': + let name = readlineSync.question(`Enter the name of the blacklist type: `, { + limit: function (input) { + return input !== ""; + }, + limitMessage: `Invalid blacklist name` + }); + let minuteFromNow = Math.floor(Date.now() / 1000) + 60; + let startTime = readlineSync.questionInt(`Enter the start date (Unix Epoch time) of the blacklist type (a minute from now = ${minuteFromNow}): `, { defaultInput: minuteFromNow }); + let oneDayFromStartTime = startTime + 24 * 60 * 60; + let endTime = readlineSync.questionInt(`Enter the end date (Unix Epoch time) of the blacklist type (1 day from start time = ${oneDayFromStartTime}): `, { defaultInput: oneDayFromStartTime }); + let repeatPeriodTime = readlineSync.questionInt(`Enter the repeat period (days) of the blacklist type, 0 to disable (90 days): `, { defaultInput: 90 }); + if (readlineSync.keyInYNStrict(`Do you want to add an investor to this blacklist type? `)) { + let investor = readlineSync.question(`Enter the address of the investor: `, { + limit: function (input) { + return web3.utils.isAddress(input); + }, + limitMessage: `Must be a valid address` + }); + let addInvestorToNewBlacklistAction = currentTransferManager.methods.addInvestorToNewBlacklist( + startTime, + endTime, + web3.utils.toHex(name), + repeatPeriodTime, + investor + ); + let addInvestorToNewBlacklistReceipt = await common.sendTransaction(addInvestorToNewBlacklistAction); + let addNewBlacklistEvent = common.getEventFromLogs(currentTransferManager._jsonInterface, addInvestorToNewBlacklistReceipt.logs, 'AddBlacklistType'); + console.log(chalk.green(`${web3.utils.hexToUtf8(addNewBlacklistEvent._blacklistName)} blacklist type has been added successfully!`)); + let addInvestorToNewBlacklistEvent = common.getEventFromLogs(currentTransferManager._jsonInterface, addInvestorToNewBlacklistReceipt.logs, 'AddInvestorToBlacklist'); + console.log(chalk.green(`${addInvestorToNewBlacklistEvent._investor} has been added to ${web3.utils.hexToUtf8(addInvestorToNewBlacklistEvent._blacklistName)} successfully!`)); + } else { + let addBlacklistTypeAction = currentTransferManager.methods.addBlacklistType(startTime, endTime, web3.utils.toHex(name), repeatPeriodTime); + let addBlacklistTypeReceipt = await common.sendTransaction(addBlacklistTypeAction); + let addBlacklistTypeEvent = common.getEventFromLogs(currentTransferManager._jsonInterface, addBlacklistTypeReceipt.logs, 'AddBlacklistType'); + console.log(chalk.green(`${web3.utils.hexToUtf8(addBlacklistTypeEvent._blacklistName)} blacklist type has been added successfully!`)); + } + break; + case 'Manage existing blacklist': + let options = currentBlacklists.map(b => web3.utils.hexToUtf8(b)); + let index = readlineSync.keyInSelect(options, 'Which blacklist type do you want to manage? ', { cancel: "RETURN" }); + let optionSelected = index !== -1 ? options[index] : 'RETURN'; + console.log('Selected:', optionSelected, '\n'); + if (index !== -1) { + await manageExistingBlacklist(currentBlacklists[index]); + } + break; + case 'Explore account': + let account = readlineSync.question(`Enter the address of the investor: `, { + limit: function (input) { + return web3.utils.isAddress(input); + }, + limitMessage: `Must be a valid address` + }); + let blacklistNamesToUser = await currentTransferManager.methods.getBlacklistNamesToUser(account).call(); + if (blacklistNamesToUser.length > 0) { + console.log(); + console.log(`**** Blacklists inlcuding ${account} ****`); + blacklistNamesToUser.map(n => console.log(web3.utils.hexToUtf8(n))); + } else { + console.log(chalk.yellow(`No blacklist includes ${account}`)); + } + console.log(); + break; + case 'Delete investors from all blacklists': + let investorsToRemove = readlineSync.question(`Enter the addresses of the investors separated by comma (i.e. addr1,addr2,addr3): `, { + limit: function (input) { + return (input !== '' && input.split(",").every(a => web3.utils.isAddress(a))); + }, + limitMessage: `All addresses must be valid` + }).split(','); + let deleteInvestorFromAllBlacklistAction; + if (investorsToRemove.length === 1) { + deleteInvestorFromAllBlacklistAction = currentTransferManager.methods.deleteInvestorFromAllBlacklist(investorsToRemove[0]); + } else { + deleteInvestorFromAllBlacklistAction = currentTransferManager.methods.deleteInvestorFromAllBlacklistMulti(investorsToRemove); + } + let deleteInvestorFromAllBlacklistReceipt = await common.sendTransaction(deleteInvestorFromAllBlacklistAction); + let deleteInvestorFromAllBlacklistEvents = common.getMultipleEventsFromLogs(currentTransferManager._jsonInterface, deleteInvestorFromAllBlacklistReceipt.logs, 'DeleteInvestorFromBlacklist'); + deleteInvestorFromAllBlacklistEvents.map(e => console.log(chalk.green(`${e._investor} has been removed from ${web3.utils.hexToUtf8(e._blacklistName)} successfully!`))); + break; + case 'Operate with multiple blacklists': + await operateWithMultipleBlacklists(currentBlacklists); + break; + case 'RETURN': + return; + } + + await blacklistTransferManager(); +} + +async function manageExistingBlacklist(blacklistName) { + // Show current data + let currentBlacklist = await currentTransferManager.methods.blacklists(blacklistName).call(); + let investors = await currentTransferManager.methods.getListOfAddresses(blacklistName).call(); + + console.log(); + console.log(`- Name: ${web3.utils.hexToUtf8(blacklistName)}`); + console.log(`- Start time: ${moment.unix(currentBlacklist.startTime).format('MMMM Do YYYY, HH:mm:ss')}`); + console.log(`- End time: ${moment.unix(currentBlacklist.endTime).format('MMMM Do YYYY, HH:mm:ss')}`); + console.log(`- Span: ${(currentBlacklist.endTime - currentBlacklist.startTime) / 60 / 60 / 24} days`); + console.log(`- Repeat period time: ${currentBlacklist.repeatPeriodTime} days`); + console.log(`- Investors: ${investors.length}`); + // ------------------ + + let options = [ + "Modify properties", + "Show investors", + "Add investors", + "Remove investor", + "Delete this blacklist type" + ]; + + let index = readlineSync.keyInSelect(options, 'What do you want to do?', { cancel: 'RETURN' }); + let optionSelected = index !== -1 ? options[index] : 'RETURN'; + console.log('Selected:', optionSelected, '\n'); + switch (optionSelected) { + case 'Modify properties': + let minuteFromNow = Math.floor(Date.now() / 1000) + 60; + let startTime = readlineSync.questionInt(`Enter the start date (Unix Epoch time) of the blacklist type (a minute from now = ${minuteFromNow}): `, { defaultInput: minuteFromNow }); + let oneDayFromStartTime = startTime + 24 * 60 * 60; + let endTime = readlineSync.questionInt(`Enter the end date (Unix Epoch time) of the blacklist type (1 day from start time = ${oneDayFromStartTime}): `, { defaultInput: oneDayFromStartTime }); + let repeatPeriodTime = readlineSync.questionInt(`Enter the repeat period (days) of the blacklist type, 0 to disable (90 days): `, { defaultInput: 90 }); + let modifyBlacklistTypeAction = currentTransferManager.methods.modifyBlacklistType( + startTime, + endTime, + blacklistName, + repeatPeriodTime + ); + let modifyBlacklistTypeReceipt = await common.sendTransaction(modifyBlacklistTypeAction); + let modifyBlacklistTypeEvent = common.getEventFromLogs(currentTransferManager._jsonInterface, modifyBlacklistTypeReceipt.logs, 'ModifyBlacklistType'); + console.log(chalk.green(`${web3.utils.hexToUtf8(modifyBlacklistTypeEvent._blacklistName)} blacklist type has been modified successfully!`)); + break; + case 'Show investors': + if (investors.length > 0) { + console.log("************ List of investors ************"); + investors.map(i => console.log(i)); + } else { + console.log(chalk.yellow("There are no investors yet")); + } + break; + case 'Add investors': + let investorsToAdd = readlineSync.question(`Enter the addresses of the investors separated by comma (i.e. addr1,addr2,addr3): `, { + limit: function (input) { + return (input !== '' && input.split(",").every(a => web3.utils.isAddress(a))); + }, + limitMessage: `All addresses must be valid` + }).split(","); + let addInvestorToBlacklistAction; + if (investorsToAdd.length === 1) { + addInvestorToBlacklistAction = currentTransferManager.methods.addInvestorToBlacklist(investorsToAdd[0], blacklistName); + } else { + addInvestorToBlacklistAction = currentTransferManager.methods.addInvestorToBlacklistMulti(investorsToAdd, blacklistName); + } + let addInvestorToBlacklistReceipt = await common.sendTransaction(addInvestorToBlacklistAction); + let addInvestorToBlacklistEvents = common.getMultipleEventsFromLogs(currentTransferManager._jsonInterface, addInvestorToBlacklistReceipt.logs, 'AddInvestorToBlacklist'); + addInvestorToBlacklistEvents.map(e => console.log(chalk.green(`${e._investor} has been added to ${web3.utils.hexToUtf8(e._blacklistName)} successfully!`))); + break; + case "Remove investor": + let investorsToRemove = readlineSync.question(`Enter the address of the investor: `, { + limit: function (input) { + return web3.utils.isAddress(input); + }, + limitMessage: `Must be a valid address` + }); + let deleteInvestorFromBlacklistAction = currentTransferManager.methods.deleteInvestorFromBlacklist(investorsToRemove, blacklistName); + let deleteInvestorFromBlacklistReceipt = await common.sendTransaction(deleteInvestorFromBlacklistAction); + let deleteInvestorFromBlacklistEvent = common.getEventFromLogs(currentTransferManager._jsonInterface, deleteInvestorFromBlacklistReceipt.logs, 'DeleteInvestorFromBlacklist'); + console.log(chalk.green(`${deleteInvestorFromBlacklistEvent._investor} has been removed from ${web3.utils.hexToUtf8(deleteInvestorFromBlacklistEvent._blacklistName)} successfully!`)); + break; + case "Delete this blacklist type": + let isEmpty = investors.length === 0; + if (!isEmpty) { + console.log(chalk.yellow(`This blacklist have investors added on it. To delete it you must remove them first.`)); + if (readlineSync.keyInYNStrict(`Do you want to remove them? `)) { + let data = investors.map(i => [i, blacklistName]) + let batches = common.splitIntoBatches(data, gbl.constants.DEFAULT_BATCH_SIZE); + let [investorArray, blacklistNameArray] = common.transposeBatches(batches); + for (let batch = 0; batch < batches.length; batch++) { + console.log(`Batch ${batch + 1} - Attempting to remove the following investors:\n\n`, investorArray[batch], '\n'); + let action = currentTransferManager.methods.deleteMultiInvestorsFromBlacklistMulti(investorArray[batch], blacklistNameArray[batch]); + let receipt = await common.sendTransaction(action); + console.log(chalk.green('Remove investors from multiple blacklists transaction was successful.')); + console.log(`${receipt.gasUsed} gas used.Spent: ${web3.utils.fromWei((new web3.utils.BN(receipt.gasUsed)).mul(new web3.utils.BN(defaultGasPrice)))} ETH`); + } + isEmpty = true; + } + } + if (isEmpty) { + let deleteBlacklistTypeAction = currentTransferManager.methods.deleteBlacklistType(blacklistName); + let deleteBlacklistTypeReceipt = await common.sendTransaction(deleteBlacklistTypeAction); + let deleteBlacklistTypeEvent = common.getEventFromLogs(currentTransferManager._jsonInterface, deleteBlacklistTypeReceipt.logs, 'DeleteBlacklistType'); + console.log(chalk.green(`${web3.utils.hexToUtf8(deleteBlacklistTypeEvent._blacklistName)} blacklist type has been deleted successfully!`)); + } + return; + case 'RETURN': + return; + } + + await manageExistingBlacklist(blacklistName); +} + +async function operateWithMultipleBlacklists(currentBlacklists) { + let options = ['Add multiple blacklists']; + if (currentBlacklists.length > 0) { + options.push('Modify multiple blacklists'); + } + options.push( + 'Delete multiple blacklists', + 'Add investors to multiple blacklists', + 'Remove investors from multiple blacklists' + ); + + let index = readlineSync.keyInSelect(options, 'What do you want to do?', { cancel: 'RETURN' }); + let optionSelected = index !== -1 ? options[index] : 'RETURN'; + console.log('Selected:', optionSelected, '\n'); + switch (optionSelected) { + case 'Add multiple blacklists': + await addBlacklistsInBatch(); + break; + case 'Modify multiple blacklists': + await modifyBlacklistsInBatch(); + break; + case 'Delete multiple blacklists': + await deleteBlacklistsInBatch(); + break; + case 'Add investors to multiple blacklists': + await addInvestorsToBlacklistsInBatch(); + break; + case 'Remove investors from multiple blacklists': + await removeInvestorsFromBlacklistsInBatch(); + break; + } +} + +async function addBlacklistsInBatch() { + let csvFilePath = readlineSync.question(`Enter the path for csv data file (${ADD_BLACKLIST_DATA_CSV}): `, { + defaultInput: ADD_BLACKLIST_DATA_CSV + }); + let batchSize = readlineSync.question(`Enter the max number of records per transaction or batch size (${gbl.constants.DEFAULT_BATCH_SIZE}): `, { + limit: function (input) { + return parseInt(input) > 0; + }, + limitMessage: 'Must be greater than 0', + defaultInput: gbl.constants.DEFAULT_BATCH_SIZE + }); + let parsedData = csvParse(csvFilePath); + let validData = parsedData.filter( + row => moment.unix(row[0]).isValid() && + moment.unix(row[1]).isValid() && + typeof row[2] === 'string' && + (!isNaN(row[3] && (parseFloat(row[3]) % 1 === 0)))); + let invalidRows = parsedData.filter(row => !validData.includes(row)); + if (invalidRows.length > 0) { + console.log(chalk.red(`The following lines from csv file are not valid: ${invalidRows.map(r => parsedData.indexOf(r) + 1).join(',')} `)); + } + let batches = common.splitIntoBatches(validData, batchSize); + let [startTimeArray, endTimeArray, blacklistNameArray, repeatPeriodTimeArray] = common.transposeBatches(batches); + for (let batch = 0; batch < batches.length; batch++) { + console.log(`Batch ${batch + 1} - Attempting to add the following blacklists:\n\n`, blacklistNameArray[batch], '\n'); + blacklistNameArray[batch] = blacklistNameArray[batch].map(n => web3.utils.toHex(n)); + let action = currentTransferManager.methods.addBlacklistTypeMulti(startTimeArray[batch], endTimeArray[batch], blacklistNameArray[batch], repeatPeriodTimeArray[batch]); + let receipt = await common.sendTransaction(action); + console.log(chalk.green('Add multiple blacklists transaction was successful.')); + console.log(`${receipt.gasUsed} gas used.Spent: ${web3.utils.fromWei((new web3.utils.BN(receipt.gasUsed)).mul(new web3.utils.BN(defaultGasPrice)))} ETH`); + } +} + +async function modifyBlacklistsInBatch() { + let csvFilePath = readlineSync.question(`Enter the path for csv data file (${MODIFY_BLACKLIST_DATA_CSV}): `, { + defaultInput: MODIFY_BLACKLIST_DATA_CSV + }); + let batchSize = readlineSync.question(`Enter the max number of records per transaction or batch size (${gbl.constants.DEFAULT_BATCH_SIZE}): `, { + limit: function (input) { + return parseInt(input) > 0; + }, + limitMessage: 'Must be greater than 0', + defaultInput: gbl.constants.DEFAULT_BATCH_SIZE + }); + let parsedData = csvParse(csvFilePath); + let validData = parsedData.filter( + row => moment.unix(row[0]).isValid() && + moment.unix(row[1]).isValid() && + typeof row[2] === 'string' && + (!isNaN(row[3] && (parseFloat(row[3]) % 1 === 0)))); + let invalidRows = parsedData.filter(row => !validData.includes(row)); + if (invalidRows.length > 0) { + console.log(chalk.red(`The following lines from csv file are not valid: ${invalidRows.map(r => parsedData.indexOf(r) + 1).join(',')} `)); + } + let batches = common.splitIntoBatches(validData, batchSize); + let [startTimeArray, endTimeArray, blacklistNameArray, repeatPeriodTimeArray] = common.transposeBatches(batches); + for (let batch = 0; batch < batches.length; batch++) { + console.log(`Batch ${batch + 1} - Attempting to modify the following blacklists:\n\n`, blacklistNameArray[batch], '\n'); + blacklistNameArray[batch] = blacklistNameArray[batch].map(n => web3.utils.toHex(n)); + let action = currentTransferManager.methods.modifyBlacklistTypeMulti(startTimeArray[batch], endTimeArray[batch], blacklistNameArray[batch], repeatPeriodTimeArray[batch]); + let receipt = await common.sendTransaction(action); + console.log(chalk.green('Modify multiple blacklists transaction was successful.')); + console.log(`${receipt.gasUsed} gas used.Spent: ${web3.utils.fromWei((new web3.utils.BN(receipt.gasUsed)).mul(new web3.utils.BN(defaultGasPrice)))} ETH`); + } +} + +async function deleteBlacklistsInBatch() { + let csvFilePath = readlineSync.question(`Enter the path for csv data file (${DELETE_BLACKLIST_DATA_CSV}): `, { + defaultInput: DELETE_BLACKLIST_DATA_CSV + }); + let batchSize = readlineSync.question(`Enter the max number of records per transaction or batch size (${gbl.constants.DEFAULT_BATCH_SIZE}): `, { + limit: function (input) { + return parseInt(input) > 0; + }, + limitMessage: 'Must be greater than 0', + defaultInput: gbl.constants.DEFAULT_BATCH_SIZE + }); + let parsedData = csvParse(csvFilePath); + let validData = parsedData.filter(row => typeof row[0] === 'string'); + let invalidRows = parsedData.filter(row => !validData.includes(row)); + if (invalidRows.length > 0) { + console.log(chalk.red(`The following lines from csv file are not valid: ${invalidRows.map(r => parsedData.indexOf(r) + 1).join(',')} `)); + } + + let verifiedData = []; + let unverifiedData = []; + for (const row of validData) { + let blacklistName = row[0]; + let verifiedTransaction = (await currentTransferManager.methods.getListOfAddresses(web3.utils.toHex(blacklistName)).call()).length === 0; + if (verifiedTransaction) { + verifiedData.push(row); + } else { + unverifiedData.push(row); + } + } + + let batches = common.splitIntoBatches(verifiedData, batchSize); + let [blacklistNameArray] = common.transposeBatches(batches); + for (let batch = 0; batch < batches.length; batch++) { + console.log(`Batch ${batch + 1} - Attempting to delete the following blacklists:\n\n`, blacklistNameArray[batch], '\n'); + blacklistNameArray[batch] = blacklistNameArray[batch].map(n => web3.utils.toHex(n)); + let action = currentTransferManager.methods.deleteBlacklistTypeMulti(blacklistNameArray[batch]); + let receipt = await common.sendTransaction(action); + console.log(chalk.green('Delete multiple blacklists transaction was successful.')); + console.log(`${receipt.gasUsed} gas used.Spent: ${web3.utils.fromWei((new web3.utils.BN(receipt.gasUsed)).mul(new web3.utils.BN(defaultGasPrice)))} ETH`); + } + + if (unverifiedData.length > 0) { + console.log("*****************************************************************************************************************"); + console.log('The following data would failed as these blacklists have investors. They must be empty to be able to delete them.\n'); + console.log(chalk.red(unverifiedData.map(d => `${d[0]}`).join('\n'))); + console.log("*****************************************************************************************************************"); + } +} + +async function addInvestorsToBlacklistsInBatch() { + let csvFilePath = readlineSync.question(`Enter the path for csv data file (${ADD_INVESTOR_BLACKLIST_DATA_CSV}): `, { + defaultInput: ADD_INVESTOR_BLACKLIST_DATA_CSV + }); + let batchSize = readlineSync.question(`Enter the max number of records per transaction or batch size (${gbl.constants.DEFAULT_BATCH_SIZE}): `, { + limit: function (input) { + return parseInt(input) > 0; + }, + limitMessage: 'Must be greater than 0', + defaultInput: gbl.constants.DEFAULT_BATCH_SIZE + }); + let parsedData = csvParse(csvFilePath); + let validData = parsedData.filter( + row => web3.utils.isAddress(row[0]) && + typeof row[1] === 'string'); + let invalidRows = parsedData.filter(row => !validData.includes(row)); + if (invalidRows.length > 0) { + console.log(chalk.red(`The following lines from csv file are not valid: ${invalidRows.map(r => parsedData.indexOf(r) + 1).join(',')} `)); + } + let batches = common.splitIntoBatches(validData, batchSize); + let [investorArray, blacklistNameArray] = common.transposeBatches(batches); + for (let batch = 0; batch < batches.length; batch++) { + console.log(`Batch ${batch + 1} - Attempting to add the following investors:\n\n`, investorArray[batch], '\n'); + blacklistNameArray[batch] = blacklistNameArray[batch].map(n => web3.utils.toHex(n)); + let action = currentTransferManager.methods.addMultiInvestorToBlacklistMulti(investorArray[batch], blacklistNameArray[batch]); + let receipt = await common.sendTransaction(action); + console.log(chalk.green('Add investors to multiple blacklists transaction was successful.')); + console.log(`${receipt.gasUsed} gas used.Spent: ${web3.utils.fromWei((new web3.utils.BN(receipt.gasUsed)).mul(new web3.utils.BN(defaultGasPrice)))} ETH`); + } +} + +function makeBatchRequest(calls) { + let batch = new web3.BatchRequest(); + + let promises = calls.map(call => { + return new Promise((res, rej) => { + let req = call.request({ from: Issuer.address }, (err, data) => { + if (err) rej(err); + else res(data) + }); + batch.add(req) + }) + }) + batch.execute() + + return Promise.all(promises) +} + +async function removeInvestorsFromBlacklistsInBatch() { + let csvFilePath = readlineSync.question(`Enter the path for csv data file (${REMOVE_INVESTOR_BLACKLIST_DATA_CSV}): `, { + defaultInput: REMOVE_INVESTOR_BLACKLIST_DATA_CSV + }); + let batchSize = readlineSync.question(`Enter the max number of records per transaction or batch size (${gbl.constants.DEFAULT_BATCH_SIZE}): `, { + limit: function (input) { + return parseInt(input) > 0; + }, + limitMessage: 'Must be greater than 0', + defaultInput: gbl.constants.DEFAULT_BATCH_SIZE + }); + let parsedData = csvParse(csvFilePath); + let validData = parsedData.filter( + row => web3.utils.isAddress(row[0]) && + typeof row[1] === 'string'); + let invalidRows = parsedData.filter(row => !validData.includes(row)); + if (invalidRows.length > 0) { + console.log(chalk.red(`The following lines from csv file are not valid: ${invalidRows.map(r => parsedData.indexOf(r) + 1).join(',')} `)); + } + let batches = common.splitIntoBatches(validData, batchSize); + let [investorArray, blacklistNameArray] = common.transposeBatches(batches); + for (let batch = 0; batch < batches.length; batch++) { + console.log(`Batch ${batch + 1} - Attempting to remove the following investors:\n\n`, investorArray[batch], '\n'); + blacklistNameArray[batch] = blacklistNameArray[batch].map(n => web3.utils.toHex(n)); + let action = currentTransferManager.methods.deleteMultiInvestorsFromBlacklistMulti(investorArray[batch], blacklistNameArray[batch]); + let receipt = await common.sendTransaction(action); + console.log(chalk.green('Remove investors from multiple blacklists transaction was successful.')); + console.log(`${receipt.gasUsed} gas used.Spent: ${web3.utils.fromWei((new web3.utils.BN(receipt.gasUsed)).mul(new web3.utils.BN(defaultGasPrice)))} ETH`); + } +} + +async function volumeRestrictionTM() { + console.log('\n', chalk.blue(`Volume Restriction Transfer Manager at ${currentTransferManager.options.address}`, '\n')); + + let globalDailyRestriction = await currentTransferManager.methods.defaultDailyRestriction().call(); + let hasGlobalDailyRestriction = parseInt(globalDailyRestriction.startTime) !== 0; + let globalCustomRestriction = await currentTransferManager.methods.defaultRestriction().call(); + let hasGlobalCustomRestriction = parseInt(globalCustomRestriction.startTime) !== 0; + + console.log(`- Default daily restriction: ${hasGlobalDailyRestriction ? '' : 'None'}`); + if (hasGlobalDailyRestriction) { + console.log(` Type: ${RESTRICTION_TYPES[globalDailyRestriction.typeOfRestriction]}`); + console.log(` Allowed tokens: ${globalDailyRestriction.typeOfRestriction === "0" ? `${web3.utils.fromWei(globalDailyRestriction.allowedTokens)} ${tokenSymbol}` : `${fromWeiPercentage(globalDailyRestriction.allowedTokens)}%`}`); + console.log(` Start time: ${moment.unix(globalDailyRestriction.startTime).format('MMMM Do YYYY, HH:mm:ss')}`); + console.log(` Rolling period: ${globalDailyRestriction.rollingPeriodInDays} days`); + console.log(` End time: ${moment.unix(globalDailyRestriction.endTime).format('MMMM Do YYYY, HH:mm:ss')} `); + } + console.log(`- Default custom restriction: ${hasGlobalCustomRestriction ? '' : 'None'}`); + if (hasGlobalCustomRestriction) { + console.log(` Type: ${RESTRICTION_TYPES[globalCustomRestriction.typeOfRestriction]}`); + console.log(` Allowed tokens: ${globalCustomRestriction.typeOfRestriction === "0" ? `${web3.utils.fromWei(globalCustomRestriction.allowedTokens)} ${tokenSymbol}` : `${fromWeiPercentage(globalCustomRestriction.allowedTokens)}%`}`); + console.log(` Start time: ${moment.unix(globalCustomRestriction.startTime).format('MMMM Do YYYY, HH:mm:ss')}`); + console.log(` Rolling period: ${globalCustomRestriction.rollingPeriodInDays} days`); + console.log(` End time: ${moment.unix(globalCustomRestriction.endTime).format('MMMM Do YYYY, HH:mm:ss')} `); + } + + let addressesAndRestrictions = await currentTransferManager.methods.getRestrictedData().call(); + console.log(`- Individual restrictions: ${addressesAndRestrictions.allAddresses.length}`); + let exemptedAddresses = await currentTransferManager.methods.getExemptAddress().call(); + console.log(`- Exempted addresses: ${exemptedAddresses.length}`); + + let options = []; + if (addressesAndRestrictions.allAddresses.length > 0) { + options.push('Show restrictions'); + } + if (exemptedAddresses.length > 0) { + options.push('Show exempted addresses'); + } + options.push( + 'Change exempt wallet', + 'Change default restrictions', + 'Change individual restrictions', + 'Explore account', + 'Operate with multiple restrictions' + ); + + let index = readlineSync.keyInSelect(options, 'What do you want to do?', { cancel: 'RETURN' }); + let optionSelected = index !== -1 ? options[index] : 'RETURN'; + console.log('Selected:', optionSelected, '\n'); + switch (optionSelected) { + case 'Show restrictions': + showRestrictionTable( + addressesAndRestrictions.allAddresses, + addressesAndRestrictions.allowedTokens, + addressesAndRestrictions.typeOfRestriction, + addressesAndRestrictions.rollingPeriodInDays, + addressesAndRestrictions.startTime, + addressesAndRestrictions.endTime, + ); + break; + case 'Show exempted addresses': + showExemptedAddresses(exemptedAddresses); + break; + case 'Change exempt wallet': + await changeExemptWallet(); + break; + case 'Change default restrictions': + await changeDefaultRestrictions(hasGlobalDailyRestriction, hasGlobalCustomRestriction); + break; + case 'Change individual restrictions': + await changeIndividualRestrictions(); + break; + case 'Explore account': + await exploreAccount(); + break; + case 'Operate with multiple restrictions': + await operateWithMultipleRestrictions(); + break; + case 'RETURN': + return; + } + + await volumeRestrictionTM(); +} + +function showRestrictionTable(investorArray, amountArray, typeArray, rollingPeriodArray, startTimeArray, endTimeTimeArray) { + let dataTable = [['Investor', 'Maximum transfer (# or %)', 'Rolling period (days)', 'Start date', 'End date']]; + for (let i = 0; i < investorArray.length; i++) { + dataTable.push([ + investorArray[i], + typeArray[i] === "0" ? `${web3.utils.fromWei(amountArray[i])} ${tokenSymbol}` : `${fromWeiPercentage(amountArray[i])}%`, + rollingPeriodArray[i], + moment.unix(startTimeArray[i]).format('MM/DD/YYYY HH:mm'), + moment.unix(endTimeTimeArray[i]).format('MM/DD/YYYY HH:mm') + ]); + } + console.log(); + console.log(table(dataTable)); +} + +function showExemptedAddresses(addresses) { + console.log("*********** Exepmpted addresses ***********"); + addresses.map(i => console.log(i)); +} + +async function changeExemptWallet() { + let options = [ + 'Add exempt wallet', + 'Remove exempt wallet' + ]; + + let change; + let index = readlineSync.keyInSelect(options, 'What do you want to do?', { cancel: 'RETURN' }); + let optionSelected = index !== -1 ? options[index] : 'RETURN'; + console.log('Selected:', optionSelected, '\n'); + switch (optionSelected) { + case 'Add exempt wallet': + change = true; + break; + case 'Remove exempt wallet': + change = false; + break; + case 'RETURN': + return; + } + + let wallet = readlineSync.question('Enter the wallet to change: ', { + limit: function (input) { + return web3.utils.isAddress(input); + }, + limitMessage: "Must be a valid address" + }); + let changeExemptWalletAction = currentTransferManager.methods.changeExemptWalletList(wallet, change); + let changeExemptWalletReceipt = await common.sendTransaction(changeExemptWalletAction); + let changeExemptWalletEvent = common.getEventFromLogs(currentTransferManager._jsonInterface, changeExemptWalletReceipt.logs, 'ChangedExemptWalletList'); + console.log(chalk.green(`${changeExemptWalletEvent._wallet} has been ${changeExemptWalletEvent._change ? `added to` : `removed from`} exempt wallets successfully!`)); +} + +async function changeDefaultRestrictions(hasGlobalDailyRestriction, hasGlobalCustomRestriction) { + let options = []; + if (!hasGlobalDailyRestriction) { + options.push('Add global daily restriction'); + } else { + options.push('Modify global daily restriction', 'Remove global daily restriction'); + } + + if (!hasGlobalCustomRestriction) { + options.push('Add global custom restriction'); + } else { + options.push('Modify global custom restriction', 'Remove global custom restriction'); + } + + let index = readlineSync.keyInSelect(options, 'What do you want to do?', { cancel: 'RETURN' }); + let optionSelected = index !== -1 ? options[index] : 'RETURN'; + console.log('Selected:', optionSelected, '\n'); + switch (optionSelected) { + case 'Add global daily restriction': + let globalDailyRestrictoToAdd = inputRestrictionData(true); + let addGlobalDailyRestrictionAction = currentTransferManager.methods.addDefaultDailyRestriction( + globalDailyRestrictoToAdd.allowedTokens, + globalDailyRestrictoToAdd.startTime, + globalDailyRestrictoToAdd.endTime, + globalDailyRestrictoToAdd.restrictionType + ); + let addGlobalDailyRestrictionReceipt = await common.sendTransaction(addGlobalDailyRestrictionAction); + let addGlobalDailyRestrictionEvent = common.getEventFromLogs(currentTransferManager._jsonInterface, addGlobalDailyRestrictionReceipt.logs, 'AddDefaultDailyRestriction'); + console.log(chalk.green(`Global daily restriction has been added successfully!`)); + break; + case 'Modify global daily restriction': + let globalDailyRestrictoToModify = inputRestrictionData(true); + let modifyGlobalDailyRestrictionAction = currentTransferManager.methods.modifyDefaultDailyRestriction( + globalDailyRestrictoToModify.allowedTokens, + globalDailyRestrictoToModify.startTime, + globalDailyRestrictoToModify.endTime, + globalDailyRestrictoToModify.restrictionType + ); + let modifyGlobalDailyRestrictionReceipt = await common.sendTransaction(modifyGlobalDailyRestrictionAction); + let modifyGlobalDailyRestrictionEvent = common.getEventFromLogs(currentTransferManager._jsonInterface, modifyGlobalDailyRestrictionReceipt.logs, 'ModifyDefaultDailyRestriction'); + console.log(chalk.green(`Global daily restriction has been modified successfully!`)); + break; + case 'Remove global daily restriction': + let removeGlobalDailyRestrictionAction = currentTransferManager.methods.removeDefaultDailyRestriction(); + let removeGlobalDailyRestrictionReceipt = await common.sendTransaction(removeGlobalDailyRestrictionAction); + let removeGlobalDailyRestrictionEvent = common.getEventFromLogs(currentTransferManager._jsonInterface, removeGlobalDailyRestrictionReceipt.logs, 'DefaultDailyRestrictionRemoved'); + console.log(chalk.green(`Global daily restriction has been removed successfully!`)); + break; + case 'Add global custom restriction': + let globalCustomRestrictoToAdd = inputRestrictionData(false); + let addGlobalCustomRestrictionAction = currentTransferManager.methods.addDefaultRestriction( + globalCustomRestrictoToAdd.allowedTokens, + globalCustomRestrictoToAdd.startTime, + globalCustomRestrictoToAdd.rollingPeriodInDays, + globalCustomRestrictoToAdd.endTime, + globalCustomRestrictoToAdd.restrictionType + ); + let addGlobalCustomRestrictionReceipt = await common.sendTransaction(addGlobalCustomRestrictionAction); + let addGlobalCustomRestrictionEvent = common.getEventFromLogs(currentTransferManager._jsonInterface, addGlobalCustomRestrictionReceipt.logs, 'AddDefaultRestriction'); + console.log(chalk.green(`Global custom restriction has been added successfully!`)); + break; + case 'Modify global custom restriction': + let globalCustomRestrictoToModify = inputRestrictionData(false); + let modifiyGlobalCustomRestrictionAction = currentTransferManager.methods.modifyDefaultRestriction( + globalCustomRestrictoToModify.allowedTokens, + globalCustomRestrictoToModify.startTime, + globalCustomRestrictoToModify.rollingPeriodInDays, + globalCustomRestrictoToModify.endTime, + globalCustomRestrictoToModify.restrictionType + ); + let modifyGlobalCustomRestrictionReceipt = await common.sendTransaction(modifiyGlobalCustomRestrictionAction); + let modifyGlobalCustomRestrictionEvent = common.getEventFromLogs(currentTransferManager._jsonInterface, modifyGlobalCustomRestrictionReceipt.logs, 'ModifyDefaultRestriction'); + console.log(chalk.green(`Global custom restriction has been modified successfully!`)); + break; + case 'Remove global custom restriction': + let removeGlobalCustomRestrictionAction = currentTransferManager.methods.removeDefaultRestriction(); + let removeGlobalCustomRestrictionReceipt = await common.sendTransaction(removeGlobalCustomRestrictionAction); + let removeGlobalCustomRestrictionEvent = common.getEventFromLogs(currentTransferManager._jsonInterface, removeGlobalCustomRestrictionReceipt.logs, 'DefaultRestrictionRemoved'); + console.log(chalk.green(`Global custom restriction has been removed successfully!`)); + break; + } +} + +async function changeIndividualRestrictions() { + let holder = readlineSync.question('Enter the address of the token holder, whom restriction will be implied: ', { + limit: function (input) { + return web3.utils.isAddress(input); + }, + limitMessage: "Must be a valid address" + }); + + let currentDailyRestriction = await currentTransferManager.methods.individualDailyRestriction(holder).call(); + let hasDailyRestriction = parseInt(currentDailyRestriction.startTime) !== 0; + let currentCustomRestriction = await currentTransferManager.methods.individualRestriction(holder).call(); + let hasCustomRestriction = parseInt(currentCustomRestriction.startTime) !== 0; + + console.log(`*** Current individual restrictions for ${holder} ***`, '\n'); + + console.log(`- Daily restriction: ${hasDailyRestriction ? '' : 'None'}`); + if (hasDailyRestriction) { + console.log(` Type: ${RESTRICTION_TYPES[currentDailyRestriction.typeOfRestriction]}`); + console.log(` Allowed tokens: ${currentDailyRestriction.typeOfRestriction === "0" ? `${web3.utils.fromWei(currentDailyRestriction.allowedTokens)} ${tokenSymbol}` : `${fromWeiPercentage(currentDailyRestriction.allowedTokens)}%`}`); + console.log(` Start time: ${moment.unix(currentDailyRestriction.startTime).format('MMMM Do YYYY, HH:mm:ss')}`); + console.log(` Rolling period: ${currentDailyRestriction.rollingPeriodInDays} days`); + console.log(` End time: ${moment.unix(currentDailyRestriction.endTime).format('MMMM Do YYYY, HH:mm:ss')} `); + } + console.log(`- Custom restriction: ${hasCustomRestriction ? '' : 'None'} `); + if (hasCustomRestriction) { + console.log(` Type: ${RESTRICTION_TYPES[currentCustomRestriction.typeOfRestriction]}`); + console.log(` Allowed tokens: ${currentCustomRestriction.typeOfRestriction === "0" ? `${web3.utils.fromWei(currentCustomRestriction.allowedTokens)} ${tokenSymbol}` : `${fromWeiPercentage(currentCustomRestriction.allowedTokens)}%`}`); + console.log(` Start time: ${moment.unix(currentCustomRestriction.startTime).format('MMMM Do YYYY, HH:mm:ss')}`); + console.log(` Rolling period: ${currentCustomRestriction.rollingPeriodInDays} days`); + console.log(` End time: ${moment.unix(currentCustomRestriction.endTime).format('MMMM Do YYYY, HH:mm:ss')} `); + } + + let options = []; + if (!hasDailyRestriction) { + options.push('Add individual daily restriction'); + } else { + options.push('Modify individual daily restriction', 'Remove individual daily restriction'); + } + + if (!hasCustomRestriction) { + options.push('Add individual custom restriction'); + } else { + options.push('Modify individual custom restriction', 'Remove individual custom restriction'); + } + + let index = readlineSync.keyInSelect(options, 'What do you want to do?', { cancel: 'RETURN' }); + let optionSelected = index !== -1 ? options[index] : 'RETURN'; + console.log('Selected:', optionSelected, '\n'); + switch (optionSelected) { + case 'Add individual daily restriction': + let dailyRestrictonToAdd = inputRestrictionData(true); + let addDailyRestrictionAction = currentTransferManager.methods.addIndividualDailyRestriction( + holder, + dailyRestrictonToAdd.allowedTokens, + dailyRestrictonToAdd.startTime, + dailyRestrictonToAdd.endTime, + dailyRestrictonToAdd.restrictionType + ); + let addDailyRestrictionReceipt = await common.sendTransaction(addDailyRestrictionAction); + let addDailyRestrictionEvent = common.getEventFromLogs(currentTransferManager._jsonInterface, addDailyRestrictionReceipt.logs, 'AddIndividualDailyRestriction'); + console.log(chalk.green(`Daily restriction for ${addDailyRestrictionEvent._holder} has been added successfully!`)); + break; + case 'Modify individual daily restriction': + let dailyRestrictonToModify = inputRestrictionData(true); + let modifyDailyRestrictionAction = currentTransferManager.methods.modifyIndividualDailyRestriction( + holder, + dailyRestrictonToModify.allowedTokens, + dailyRestrictonToModify.startTime, + dailyRestrictonToModify.endTime, + dailyRestrictonToModify.restrictionType + ); + let modifyDailyRestrictionReceipt = await common.sendTransaction(modifyDailyRestrictionAction); + let modifyDailyRestrictionEvent = common.getEventFromLogs(currentTransferManager._jsonInterface, modifyDailyRestrictionReceipt.logs, 'ModifyIndividualDailyRestriction'); + console.log(chalk.green(`Daily restriction for ${modifyDailyRestrictionEvent._holder} has been modified successfully!`)); + break; + case 'Remove individual daily restriction': + let removeDailyRestrictionAction = currentTransferManager.methods.removeIndividualDailyRestriction(holder); + let removeDailyRestrictionReceipt = await common.sendTransaction(removeDailyRestrictionAction); + let removeDailyRestrictionEvent = common.getEventFromLogs(currentTransferManager._jsonInterface, removeDailyRestrictionReceipt.logs, 'IndividualDailyRestrictionRemoved'); + console.log(chalk.green(`Daily restriction for ${removeDailyRestrictionEvent._holder} has been removed successfully!`)); + break; + case 'Add individual custom restriction': + let restrictonToAdd = inputRestrictionData(false); + let addCustomRestrictionAction = currentTransferManager.methods.addIndividualRestriction( + holder, + restrictonToAdd.allowedTokens, + restrictonToAdd.startTime, + restrictonToAdd.rollingPeriodInDays, + restrictonToAdd.endTime, + restrictonToAdd.restrictionType + ); + let addCustomRestrictionReceipt = await common.sendTransaction(addCustomRestrictionAction); + let addCustomRestrictionEvent = common.getEventFromLogs(currentTransferManager._jsonInterface, addCustomRestrictionReceipt.logs, 'AddIndividualRestriction'); + console.log(chalk.green(`Custom restriction for ${addCustomRestrictionEvent._holder} has been added successfully!`)); + break; + case 'Modify individual custom restriction': + let restrictonToModify = inputRestrictionData(false); + let modifyCustomRestrictionAction = currentTransferManager.methods.modifyIndividualRestriction( + holder, + restrictonToModify.allowedTokens, + restrictonToModify.startTime, + restrictonToModify.rollingPeriodInDays, + restrictonToModify.endTime, + restrictonToModify.restrictionType + ); + let modifyCustomRestrictionReceipt = await common.sendTransaction(modifyCustomRestrictionAction); + let modifyCustomRestrictionEvent = common.getEventFromLogs(currentTransferManager._jsonInterface, modifyCustomRestrictionReceipt.logs, 'ModifyIndividualRestriction'); + console.log(chalk.green(`Custom restriction for ${modifyCustomRestrictionEvent._holder} has been modified successfully!`)); + break; + case 'Remove individual custom restriction': + let removeCustomRestrictionAction = currentTransferManager.methods.removeIndividualRestriction(holder); + let removeCustomRestrictionReceipt = await common.sendTransaction(removeCustomRestrictionAction); + let removeCustomRestrictionEvent = common.getEventFromLogs(currentTransferManager._jsonInterface, removeCustomRestrictionReceipt.logs, 'IndividualRestrictionRemoved'); + console.log(chalk.green(`Custom restriction for ${removeCustomRestrictionEvent._holder} has been removed successfully!`)); + break; + case 'RETURN': + return; + } +} + +async function exploreAccount() { + let account = readlineSync.question('Enter the account to explore: ', { + limit: function (input) { + return web3.utils.isAddress(input); + }, + limitMessage: "Must be a valid address" + }); + + let appliyngDailyRestriction = null; + let applyingCustomRestriction = null; + let hasIndividualRestrictions = false; + let isExempted = await currentTransferManager.methods.exemptList(account).call(); + if (!isExempted) { + let individuallDailyRestriction = await currentTransferManager.methods.individualDailyRestriction(account).call(); + if (parseInt(individuallDailyRestriction.endTime) !== 0) { + appliyngDailyRestriction = individuallDailyRestriction; + } + let customRestriction = await currentTransferManager.methods.individualRestriction(account).call(); + if (parseInt(customRestriction.endTime) !== 0) { + applyingCustomRestriction = customRestriction; + } + + hasIndividualRestrictions = applyingCustomRestriction || appliyngDailyRestriction; + + if (!hasIndividualRestrictions) { + let globalDailyRestriction = await currentTransferManager.methods.defaultDailyRestriction().call(); + if (parseInt(globalDailyRestriction.endTime) !== 0) { + appliyngDailyRestriction = globalDailyRestriction; + } + let globalCustomRestriction = await currentTransferManager.methods.defaultRestriction().call(); + if (parseInt(globalCustomRestriction.endTime) === 0) { + applyingCustomRestriction = globalCustomRestriction; + } + } + } + + console.log(`*** Applying restrictions for ${account} ***`, '\n'); + + console.log(`- Daily restriction: ${appliyngDailyRestriction ? (!hasIndividualRestrictions ? 'global' : '') : 'None'}`); + if (appliyngDailyRestriction) { + console.log(` Type: ${RESTRICTION_TYPES[appliyngDailyRestriction.typeOfRestriction]}`); + console.log(` Allowed tokens: ${appliyngDailyRestriction.typeOfRestriction === "0" ? `${web3.utils.fromWei(appliyngDailyRestriction.allowedTokens)} ${tokenSymbol}` : `${fromWeiPercentage(appliyngDailyRestriction.allowedTokens)}%`}`); + console.log(` Start time: ${moment.unix(appliyngDailyRestriction.startTime).format('MMMM Do YYYY, HH:mm:ss')}`); + console.log(` Rolling period: ${appliyngDailyRestriction.rollingPeriodInDays} days`); + console.log(` End time: ${moment.unix(appliyngDailyRestriction.endTime).format('MMMM Do YYYY, HH:mm:ss')} `); + } + console.log(`- Other restriction: ${applyingCustomRestriction ? (!hasIndividualRestrictions ? 'global' : '') : 'None'} `); + if (applyingCustomRestriction) { + console.log(` Type: ${RESTRICTION_TYPES[applyingCustomRestriction.typeOfRestriction]}`); + console.log(` Allowed tokens: ${applyingCustomRestriction.typeOfRestriction === "0" ? `${web3.utils.fromWei(applyingCustomRestriction.allowedTokens)} ${tokenSymbol}` : `${fromWeiPercentage(applyingCustomRestriction.allowedTokens)}%`}`); + console.log(` Start time: ${moment.unix(applyingCustomRestriction.startTime).format('MMMM Do YYYY, HH:mm:ss')}`); + console.log(` Rolling period: ${applyingCustomRestriction.rollingPeriodInDays} days`); + console.log(` End time: ${moment.unix(applyingCustomRestriction.endTime).format('MMMM Do YYYY, HH:mm:ss')} `); + } + + if (applyingCustomRestriction || appliyngDailyRestriction) { + let bucketDetails; + if (hasIndividualRestrictions) { + bucketDetails = await currentTransferManager.methods.getIndividualBucketDetailsToUser(account).call(); + } else { + bucketDetails = await currentTransferManager.methods.getDefaultBucketDetailsToUser(account).call(); + } + let now = Math.floor(Date.now() / 1000) - gbl.constants.DURATION.days(1); + let tradedByUserLastDay = await currentTransferManager.methods.getTotalTradedByUser(account, now).call(); + console.log(); + console.log(`Last trade: ${bucketDetails[0]}`); + console.log(`Last daily trade: ${bucketDetails[3]}`); + console.log(`Days since rolling period started: ${bucketDetails[2]}`); + console.log(`Transacted amount since rolling period started: ${web3.utils.fromWei(bucketDetails[1])}`); + console.log(`Transacted amount within last 24 hours: ${web3.utils.fromWei(tradedByUserLastDay)}`); + console.log(); + } +} + +async function operateWithMultipleRestrictions() { + let options = [ + 'Add multiple individual daily restrictions', + 'Modify multiple individual daily restrictions', + 'Remove multiple individual daily restrictions', + 'Add multiple individual restrictions', + 'Modify multiple individual restrictions', + 'Remove multiple individual restrictions' + ]; + + let index = readlineSync.keyInSelect(options, 'What do you want to do?', { cancel: 'RETURN' }); + let optionSelected = index !== -1 ? options[index] : 'RETURN'; + console.log('Selected:', optionSelected, '\n'); + switch (optionSelected) { + case 'Add multiple individual daily restrictions': + await addDailyRestrictionsInBatch(); + break; + case 'Modify multiple individual daily restrictions': + await modifyDailyRestrictionsInBatch(); + break; + case 'Remove multiple individual daily restrictions': + await removeDailyRestrictionsInBatch(); + break; + case 'Add multiple individual restrictions': + await addCustomRestrictionsInBatch(); + break; + case 'Modify multiple individual restrictions': + await modifyCustomRestrictionsInBatch(); + break; + case 'Remove multiple individual restrictions': + await removeCustomRestrictionsInBatch(); + break; + } +} + +async function addDailyRestrictionsInBatch() { + let csvFilePath = readlineSync.question(`Enter the path for csv data file (${ADD_DAILY_RESTRICTIONS_DATA_CSV}): `, { + defaultInput: ADD_DAILY_RESTRICTIONS_DATA_CSV + }); + let batchSize = readlineSync.question(`Enter the max number of records per transaction or batch size (${gbl.constants.DEFAULT_BATCH_SIZE}): `, { + limit: function (input) { + return parseInt(input) > 0; + }, + limitMessage: 'Must be greater than 0', + defaultInput: gbl.constants.DEFAULT_BATCH_SIZE + }); + let parsedData = csvParse(csvFilePath); + let validData = parsedData.filter( + row => web3.utils.isAddress(row[0]) && + !isNaN(row[1]) && + moment.unix(row[2]).isValid() && + moment.unix(row[3]).isValid() && + typeof row[4] === 'string' && RESTRICTION_TYPES.includes(row[4])); + let invalidRows = parsedData.filter(row => !validData.includes(row)); + if (invalidRows.length > 0) { + console.log(chalk.red(`The following lines from csv file are not valid: ${invalidRows.map(r => parsedData.indexOf(r) + 1).join(',')} `)); + } + let batches = common.splitIntoBatches(validData, batchSize); + let [holderArray, allowanceArray, startTimeArray, endTimeArray, restrictionTypeArray] = common.transposeBatches(batches); + for (let batch = 0; batch < batches.length; batch++) { + console.log(`Batch ${batch + 1} - Attempting to add daily restrictions to the following accounts: \n\n`, holderArray[batch], '\n'); + allowanceArray[batch] = allowanceArray[batch].map(n => web3.utils.toWei(n.toString())); + restrictionTypeArray[batch] = restrictionTypeArray[batch].map(n => RESTRICTION_TYPES.indexOf(n)); + let action = currentTransferManager.methods.addIndividualDailyRestrictionMulti(holderArray[batch], allowanceArray[batch], startTimeArray[batch], endTimeArray[batch], restrictionTypeArray[batch]); + let receipt = await common.sendTransaction(action); + console.log(chalk.green('Add multiple daily restrictions transaction was successful.')); + console.log(`${receipt.gasUsed} gas used.Spent: ${web3.utils.fromWei((new web3.utils.BN(receipt.gasUsed)).mul(new web3.utils.BN(defaultGasPrice)))} ETH`); + } +} + +async function modifyDailyRestrictionsInBatch() { + let csvFilePath = readlineSync.question(`Enter the path for csv data file (${MODIFY_DAILY_RESTRICTIONS_DATA_CSV}): `, { + defaultInput: MODIFY_DAILY_RESTRICTIONS_DATA_CSV + }); + let batchSize = readlineSync.question(`Enter the max number of records per transaction or batch size (${gbl.constants.DEFAULT_BATCH_SIZE}): `, { + limit: function (input) { + return parseInt(input) > 0; + }, + limitMessage: 'Must be greater than 0', + defaultInput: gbl.constants.DEFAULT_BATCH_SIZE + }); + let parsedData = csvParse(csvFilePath); + let validData = parsedData.filter( + row => web3.utils.isAddress(row[0]) && + !isNaN(row[1]) && + moment.unix(row[2]).isValid() && + moment.unix(row[3]).isValid() && + typeof row[4] === 'string' && RESTRICTION_TYPES.includes(row[4])); + let invalidRows = parsedData.filter(row => !validData.includes(row)); + if (invalidRows.length > 0) { + console.log(chalk.red(`The following lines from csv file are not valid: ${invalidRows.map(r => parsedData.indexOf(r) + 1).join(',')} `)); + } + let batches = common.splitIntoBatches(validData, batchSize); + let [holderArray, allowanceArray, startTimeArray, endTimeArray, restrictionTypeArray] = common.transposeBatches(batches); + for (let batch = 0; batch < batches.length; batch++) { + console.log(`Batch ${batch + 1} - Attempting to modify daily restrictions to the following accounts: \n\n`, holderArray[batch], '\n'); + allowanceArray[batch] = allowanceArray[batch].map(n => web3.utils.toWei(n.toString())); + restrictionTypeArray[batch] = restrictionTypeArray[batch].map(n => RESTRICTION_TYPES.indexOf(n)); + let action = currentTransferManager.methods.modifyIndividualDailyRestrictionMulti(holderArray[batch], allowanceArray[batch], startTimeArray[batch], endTimeArray[batch], restrictionTypeArray[batch]); + let receipt = await common.sendTransaction(action); + console.log(chalk.green('Modify multiple daily restrictions transaction was successful.')); + console.log(`${receipt.gasUsed} gas used.Spent: ${web3.utils.fromWei((new web3.utils.BN(receipt.gasUsed)).mul(new web3.utils.BN(defaultGasPrice)))} ETH`); + } +} + +async function removeDailyRestrictionsInBatch() { + let csvFilePath = readlineSync.question(`Enter the path for csv data file (${REMOVE_DAILY_RESTRICTIONS_DATA_CSV}): `, { + defaultInput: REMOVE_DAILY_RESTRICTIONS_DATA_CSV + }); + let batchSize = readlineSync.question(`Enter the max number of records per transaction or batch size (${gbl.constants.DEFAULT_BATCH_SIZE}): `, { + limit: function (input) { + return parseInt(input) > 0; + }, + limitMessage: 'Must be greater than 0', + defaultInput: gbl.constants.DEFAULT_BATCH_SIZE + }); + let parsedData = csvParse(csvFilePath); + let validData = parsedData.filter(row => web3.utils.isAddress(row[0])); + let invalidRows = parsedData.filter(row => !validData.includes(row)); + if (invalidRows.length > 0) { + console.log(chalk.red(`The following lines from csv file are not valid: ${invalidRows.map(r => parsedData.indexOf(r) + 1).join(',')} `)); + } + let batches = common.splitIntoBatches(validData, batchSize); + let [holderArray] = common.transposeBatches(batches); + for (let batch = 0; batch < batches.length; batch++) { + console.log(`Batch ${batch + 1} - Attempting to remove daily restrictions to the following accounts: \n\n`, holderArray[batch], '\n'); + let action = currentTransferManager.methods.removeIndividualDailyRestrictionMulti(holderArray[batch]); + let receipt = await common.sendTransaction(action); + console.log(chalk.green('Remove multiple daily restrictions transaction was successful.')); + console.log(`${receipt.gasUsed} gas used.Spent: ${web3.utils.fromWei((new web3.utils.BN(receipt.gasUsed)).mul(new web3.utils.BN(defaultGasPrice)))} ETH`); + } +} + +async function addCustomRestrictionsInBatch() { + let csvFilePath = readlineSync.question(`Enter the path for csv data file (${ADD_CUSTOM_RESTRICTIONS_DATA_CSV}): `, { + defaultInput: ADD_CUSTOM_RESTRICTIONS_DATA_CSV + }); + let batchSize = readlineSync.question(`Enter the max number of records per transaction or batch size (${gbl.constants.DEFAULT_BATCH_SIZE}): `, { + limit: function (input) { + return parseInt(input) > 0; + }, + limitMessage: 'Must be greater than 0', + defaultInput: gbl.constants.DEFAULT_BATCH_SIZE + }); + let parsedData = csvParse(csvFilePath); + let validData = parsedData.filter( + row => web3.utils.isAddress(row[0]) && + !isNaN(row[1]) && + moment.unix(row[2]).isValid() && + (!isNaN(row[3]) && (parseFloat(row[3]) % 1 === 0)) && + moment.unix(row[4]).isValid() && + typeof row[5] === 'string' && RESTRICTION_TYPES.includes(row[5])); + let invalidRows = parsedData.filter(row => !validData.includes(row)); + if (invalidRows.length > 0) { + console.log(chalk.red(`The following lines from csv file are not valid: ${invalidRows.map(r => parsedData.indexOf(r) + 1).join(',')} `)); + } + let batches = common.splitIntoBatches(validData, batchSize); + let [holderArray, allowanceArray, startTimeArray, rollingPeriodArray, endTimeArray, restrictionTypeArray] = common.transposeBatches(batches); + for (let batch = 0; batch < batches.length; batch++) { + console.log(`Batch ${batch + 1} - Attempting to add custom restrictions to the following accounts: \n\n`, holderArray[batch], '\n'); + allowanceArray[batch] = allowanceArray[batch].map(n => web3.utils.toWei(n.toString())); + restrictionTypeArray[batch] = restrictionTypeArray[batch].map(n => RESTRICTION_TYPES.indexOf(n)); + let action = currentTransferManager.methods.addIndividualRestrictionMulti(holderArray[batch], allowanceArray[batch], startTimeArray[batch], rollingPeriodArray[batch], endTimeArray[batch], restrictionTypeArray[batch]); + let receipt = await common.sendTransaction(action); + console.log(chalk.green('Add multiple custom restrictions transaction was successful.')); + console.log(`${receipt.gasUsed} gas used.Spent: ${web3.utils.fromWei((new web3.utils.BN(receipt.gasUsed)).mul(new web3.utils.BN(defaultGasPrice)))} ETH`); + } +} + +async function modifyCustomRestrictionsInBatch() { + let csvFilePath = readlineSync.question(`Enter the path for csv data file (${MODIFY_CUSTOM_RESTRICTIONS_DATA_CSV}): `, { + defaultInput: MODIFY_CUSTOM_RESTRICTIONS_DATA_CSV + }); + let batchSize = readlineSync.question(`Enter the max number of records per transaction or batch size (${gbl.constants.DEFAULT_BATCH_SIZE}): `, { + limit: function (input) { + return parseInt(input) > 0; + }, + limitMessage: 'Must be greater than 0', + defaultInput: gbl.constants.DEFAULT_BATCH_SIZE + }); + let parsedData = csvParse(csvFilePath); + let validData = parsedData.filter( + row => web3.utils.isAddress(row[0]) && + !isNaN(row[1]) && + moment.unix(row[2]).isValid() && + (!isNaN(row[3]) && (parseFloat(row[3]) % 1 === 0)) && + moment.unix(row[4]).isValid() && + typeof row[5] === 'string' && RESTRICTION_TYPES.includes(row[5])); + let invalidRows = parsedData.filter(row => !validData.includes(row)); + if (invalidRows.length > 0) { + console.log(chalk.red(`The following lines from csv file are not valid: ${invalidRows.map(r => parsedData.indexOf(r) + 1).join(',')} `)); + } + let batches = common.splitIntoBatches(validData, batchSize); + let [holderArray, allowanceArray, startTimeArray, rollingPeriodArray, endTimeArray, restrictionTypeArray] = common.transposeBatches(batches); + for (let batch = 0; batch < batches.length; batch++) { + console.log(`Batch ${batch + 1} - Attempting to modify custom restrictions to the following accounts: \n\n`, holderArray[batch], '\n'); + allowanceArray[batch] = allowanceArray[batch].map(n => web3.utils.toWei(n.toString())); + restrictionTypeArray[batch] = restrictionTypeArray[batch].map(n => RESTRICTION_TYPES.indexOf(n)); + let action = currentTransferManager.methods.modifyIndividualRestrictionMulti(holderArray[batch], allowanceArray[batch], startTimeArray[batch], rollingPeriodArray[batch], endTimeArray[batch], restrictionTypeArray[batch]); + let receipt = await common.sendTransaction(action); + console.log(chalk.green('Modify multiple custom restrictions transaction was successful.')); + console.log(`${receipt.gasUsed} gas used.Spent: ${web3.utils.fromWei((new web3.utils.BN(receipt.gasUsed)).mul(new web3.utils.BN(defaultGasPrice)))} ETH`); + } +} + +async function removeCustomRestrictionsInBatch() { + let csvFilePath = readlineSync.question(`Enter the path for csv data file (${REMOVE_CUSTOM_RESTRICTIONS_DATA_CSV}): `, { + defaultInput: REMOVE_CUSTOM_RESTRICTIONS_DATA_CSV + }); + let batchSize = readlineSync.question(`Enter the max number of records per transaction or batch size (${gbl.constants.DEFAULT_BATCH_SIZE}): `, { + limit: function (input) { + return parseInt(input) > 0; + }, + limitMessage: 'Must be greater than 0', + defaultInput: gbl.constants.DEFAULT_BATCH_SIZE + }); + let parsedData = csvParse(csvFilePath); + let validData = parsedData.filter(row => web3.utils.isAddress(row[0])); + let invalidRows = parsedData.filter(row => !validData.includes(row)); + if (invalidRows.length > 0) { + console.log(chalk.red(`The following lines from csv file are not valid: ${invalidRows.map(r => parsedData.indexOf(r) + 1).join(',')} `)); + } + let batches = common.splitIntoBatches(validData, batchSize); + let [holderArray] = common.transposeBatches(batches); + for (let batch = 0; batch < batches.length; batch++) { + console.log(`Batch ${batch + 1} - Attempting to remove custom restrictions to the following accounts: \n\n`, holderArray[batch], '\n'); + let action = currentTransferManager.methods.removeIndividualRestrictionMulti(holderArray[batch]); + let receipt = await common.sendTransaction(action); + console.log(chalk.green('Remove multiple custom restrictions transaction was successful.')); + console.log(`${receipt.gasUsed} gas used.Spent: ${web3.utils.fromWei((new web3.utils.BN(receipt.gasUsed)).mul(new web3.utils.BN(defaultGasPrice)))} ETH`); + } +} + +function inputRestrictionData(isDaily) { + let restriction = {}; + restriction.restrictionType = readlineSync.keyInSelect(RESTRICTION_TYPES, 'How do you want to set the allowance? ', { cancel: false }); + if (restriction.restrictionType == RESTRICTION_TYPES.indexOf('Fixed')) { + restriction.allowedTokens = web3.utils.toWei(readlineSync.question(`Enter the maximum amount of tokens allowed to be traded every ${isDaily ? 'day' : 'rolling period'}: `).toString()); + } else { + restriction.allowedTokens = toWeiPercentage(readlineSync.question(`Enter the maximum percentage of total supply allowed to be traded every ${isDaily ? 'day' : 'rolling period'}: `).toString()); + } + if (isDaily) { + restriction.rollingPeriodInDays = 1; + } else { + restriction.rollingPeriodInDays = readlineSync.questionInt(`Enter the rolling period in days (10 days): `, { defaultInput: 10 }); + } + restriction.startTime = readlineSync.questionInt(`Enter the time (Unix Epoch time) at which restriction get into effect (now = 0): `, { defaultInput: 0 }); + let oneMonthFromNow = Math.floor(Date.now() / 1000) + gbl.constants.DURATION.days(30); + restriction.endTime = readlineSync.question(`Enter the time (Unix Epoch time) when the purchase lockup period ends and the investor can freely purchase tokens from others (1 month from now = ${oneMonthFromNow}): `, { + limit: function (input) { + return input > restriction.startTime + gbl.constants.DURATION.days(restriction.rollingPeriodInDays); + }, + limitMessage: 'Must be greater than startTime + rolling period', + defaultInput: oneMonthFromNow + }); + return restriction; +} + +async function lockUpTransferManager() { + console.log('\n', chalk.blue(`Lockup Transfer Manager at ${currentTransferManager.options.address}`), '\n'); + + let currentLockups = await currentTransferManager.methods.getAllLockups().call(); + console.log(`- Lockups: ${currentLockups.length}`); + + let options = ['Add new lockup']; + if (currentLockups.length > 0) { + options.push('Manage existing lockups', 'Explore investor'); + } + options.push('Operate with multiple lockups'); + + let index = readlineSync.keyInSelect(options, 'What do you want to do?', { cancel: 'RETURN' }); + let optionSelected = index !== -1 ? options[index] : 'RETURN'; + console.log('Selected:', optionSelected, '\n'); + switch (optionSelected) { + case 'Add new lockup': + let name = readlineSync.question(`Enter the name of the lockup type: `, { + limit: function (input) { + return input !== ""; }, - limitMessage: "Must be a valid address" + limitMessage: `Invalid lockup name` }); - let isWhitelisted = await currentTransferManager.methods.whitelist(investorToCheck).call(); - if (isWhitelisted) { - console.log(chalk.green(`${investorToCheck} is whitelisted!`)); + let lockupAmount = readlineSync.questionInt(`Enter the amount of tokens that will be locked: `); + let minuteFromNow = Math.floor(Date.now() / 1000) + 60; + let startTime = readlineSync.questionInt(`Enter the start time (Unix Epoch time) of the lockup type (a minute from now = ${minuteFromNow}): `, { defaultInput: minuteFromNow }); + let lockUpPeriodSeconds = readlineSync.questionInt(`Enter the total period (seconds) of the lockup type (ten minutes = 600): `, { defaultInput: 600 }); + let releaseFrequencySeconds = readlineSync.questionInt(`Enter how often to release a tranche of tokens in seconds (one minute = 60): `, { defaultInput: 60 }); + if (readlineSync.keyInYNStrict(`Do you want to add an investor to this lockup type? `)) { + let investor = readlineSync.question(`Enter the address of the investor: `, { + limit: function (input) { + return web3.utils.isAddress(input); + }, + limitMessage: `Must be a valid address` + }); + let addNewLockUpToUserAction = currentTransferManager.methods.addNewLockUpToUser( + investor, + web3.utils.toWei(lockupAmount.toString()), + startTime, + lockUpPeriodSeconds, + releaseFrequencySeconds, + web3.utils.toHex(name) + ); + let addNewLockUpToUserReceipt = await common.sendTransaction(addNewLockUpToUserAction); + let addNewLockUpToUserEvent = common.getEventFromLogs(currentTransferManager._jsonInterface, addNewLockUpToUserReceipt.logs, 'AddNewLockUpType'); + console.log(chalk.green(`${web3.utils.hexToUtf8(addNewLockUpToUserEvent._lockupName)} lockup type has been added successfully!`)); + let addLockUpToUserEvent = common.getEventFromLogs(currentTransferManager._jsonInterface, addNewLockUpToUserReceipt.logs, 'AddLockUpToUser'); + console.log(chalk.green(`${addLockUpToUserEvent._userAddress} has been added to ${web3.utils.hexToUtf8(addLockUpToUserEvent._lockupName)} successfully!`)); } else { - console.log(chalk.yellow(`${investorToCheck} is not whitelisted!`)); + let addLockupTypeAction = currentTransferManager.methods.addNewLockUpType(web3.utils.toWei(lockupAmount.toString()), startTime, lockUpPeriodSeconds, releaseFrequencySeconds, web3.utils.toHex(name)); + let addLockupTypeReceipt = await common.sendTransaction(addLockupTypeAction); + let addLockupTypeEvent = common.getEventFromLogs(currentTransferManager._jsonInterface, addLockupTypeReceipt.logs, 'AddNewLockUpType'); + console.log(chalk.green(`${web3.utils.hexToUtf8(addLockupTypeEvent._lockupName)} lockup type has been added successfully!`)); } break; - case 'Modify whitelist': - let valid = !!readlineSync.keyInSelect(['Remove investor from whitelist', 'Add investor to whitelist'], 'How do you want to do? ', { cancel: false }); - let investorToWhitelist = readlineSync.question('Enter the address of the investor: ', { + case 'Manage existing lockups': + let options = currentLockups.map(b => web3.utils.hexToUtf8(b)); + let index = readlineSync.keyInSelect(options, 'Which lockup type do you want to manage? ', { cancel: 'RETURN' }); + let optionSelected = index !== -1 ? options[index] : 'RETURN'; + console.log('Selected:', optionSelected, '\n'); + if (index !== -1) { + await manageExistingLockups(currentLockups[index]); + } + break; + case 'Explore investor': + let investorToExplore = readlineSync.question('Enter the address you want to explore: ', { limit: function (input) { return web3.utils.isAddress(input); }, limitMessage: "Must be a valid address" }); - let modifyWhitelistAction = currentTransferManager.methods.modifyWhitelist(investorToWhitelist, valid); - let modifyWhitelistReceipt = await common.sendTransaction(modifyWhitelistAction); - let modifyWhitelistEvent = common.getEventFromLogs(currentTransferManager._jsonInterface, modifyWhitelistReceipt.logs, 'ModifyWhitelist'); - if (modifyWhitelistEvent._valid) { - console.log(chalk.green(`${modifyWhitelistEvent._investor} has been added to the whitelist sucessfully!`)); + let lockupsToInvestor = await currentTransferManager.methods.getLockupsNamesToUser(investorToExplore).call(); + if (lockupsToInvestor.length > 0) { + let lockedTokenToInvestor = await currentTransferManager.methods.getLockedTokenToUser(investorToExplore).call(); + console.log(chalk.green(`The address ${investorToExplore} has ${web3.utils.fromWei(lockedTokenToInvestor)} ${tokenSymbol} locked across the following ${lockupsToInvestor.length} lockups: `)); + lockupsToInvestor.map(l => console.log(chalk.green(`- ${web3.utils.hexToUtf8(l)}`))); } else { - console.log(chalk.green(`${modifyWhitelistEvent._investor} has been removed from the whitelist sucessfully!`)); - } - break; - case 'Modify whitelist from CSV': - let csvFilePath = readlineSync.question(`Enter the path for csv data file (${PERCENTAGE_WHITELIST_DATA_CSV}): `, { - defaultInput: PERCENTAGE_WHITELIST_DATA_CSV - }); - let batchSize = readlineSync.question(`Enter the max number of records per transaction or batch size (${gbl.constants.DEFAULT_BATCH_SIZE}): `, { - limit: function (input) { - return parseInt(input) > 0; - }, - limitMessage: 'Must be greater than 0', - defaultInput: gbl.constants.DEFAULT_BATCH_SIZE - }); - let parsedData = csvParse(csvFilePath); - let validData = parsedData.filter(row => web3.utils.isAddress(row[0]) && typeof row[1] === 'boolean'); - let invalidRows = parsedData.filter(row => !validData.includes(row)); - if (invalidRows.length > 0) { - console.log(chalk.red(`The following lines from csv file are not valid: ${invalidRows.map(r => parsedData.indexOf(r) + 1).join(',')}`)); - } - let batches = common.splitIntoBatches(validData, batchSize); - let [investorArray, isWhitelistedArray] = common.transposeBatches(batches); - for (let batch = 0; batch < batches.length; batch++) { - console.log(`Batch ${batch + 1} - Attempting to modify whitelist accounts:\n\n`, investorArray[batch], '\n'); - let action = await currentTransferManager.methods.modifyWhitelistMulti(investorArray[batch], isWhitelistedArray[batch]); - let receipt = await common.sendTransaction(action); - console.log(chalk.green('Modify whitelist transaction was successful.')); - console.log(`${receipt.gasUsed} gas used. Spent: ${web3.utils.fromWei((new web3.utils.BN(receipt.gasUsed)).mul(new web3.utils.BN(defaultGasPrice)))} ETH`); + console.log(chalk.yellow(`The address ${investorToExplore} has no lockups`)); } break; - case 'Allow primary issuance': - case 'Disallow primary issuance': - let setAllowPrimaryIssuanceAction = currentTransferManager.methods.setAllowPrimaryIssuance(!displayAllowPrimaryIssuance); - let setAllowPrimaryIssuanceReceipt = await common.sendTransaction(setAllowPrimaryIssuanceAction); - let setAllowPrimaryIssuanceEvent = common.getEventFromLogs(currentTransferManager._jsonInterface, setAllowPrimaryIssuanceReceipt.logs, 'SetAllowPrimaryIssuance'); - if (setAllowPrimaryIssuanceEvent._allowPrimaryIssuance) { - console.log(chalk.green(`Transactions which are part of the primary issuance will be ignored!`)); - } else { - console.log(chalk.green(`Transactions which are part of the primary issuance will NOT be ignored!`)); - } + case 'Operate with multiple lockups': + await operateWithMultipleLockups(currentLockups); break; - + case 'RETURN': + return; } + + await lockUpTransferManager(); } -async function singleTradeVolumeRestrictionTM() { - console.log(chalk.blue(`Single Trade Volume Restriction Transfer Manager at ${currentTransferManager.options.address} `)); - console.log(); +async function manageExistingLockups(lockupName) { + console.log('\n', chalk.blue(`Lockup ${web3.utils.hexToUtf8(lockupName)}`), '\n'); // Show current data - let displayIsInPercentage = await currentTransferManager.methods.isTransferLimitInPercentage().call(); - let displayGlobalTransferLimit; - if (displayIsInPercentage) { - displayGlobalTransferLimit = fromWeiPercentage(await currentTransferManager.methods.globalTransferLimitInPercentage().call()); - } else { - displayGlobalTransferLimit = web3.utils.fromWei(await currentTransferManager.methods.globalTransferLimitInTokens().call()); - } - let displayAllowPrimaryIssuance = await currentTransferManager.methods.allowPrimaryIssuance().call(); + let currentLockup = await currentTransferManager.methods.getLockUp(lockupName).call(); + let investors = await currentTransferManager.methods.getListOfAddresses(lockupName).call(); - console.log(`- Limit type: ${displayIsInPercentage ? `Percentage` : `Tokens`} `); - console.log(`- Default transfer limit: ${displayGlobalTransferLimit} ${displayIsInPercentage ? `%` : `${tokenSymbol}`} `); - console.log(`- Allow primary issuance: ${displayAllowPrimaryIssuance ? `YES` : `NO`} `); + console.log(`- Amount: ${web3.utils.fromWei(currentLockup.lockupAmount)} ${tokenSymbol}`); + console.log(`- Currently unlocked: ${web3.utils.fromWei(currentLockup.unlockedAmount)} ${tokenSymbol}`); + console.log(`- Start time: ${moment.unix(currentLockup.startTime).format('MMMM Do YYYY, HH:mm:ss')}`); + console.log(`- Lockup period: ${currentLockup.lockUpPeriodSeconds} seconds`); + console.log(`- End time: ${moment.unix(currentLockup.endTime).add(parseInt(currentLockup.lockUpPeriodSeconds)).format('MMMM Do YYYY, HH:mm:ss')}`); + console.log(`- Release frequency: ${currentLockup.releaseFrequencySeconds} senconds`); + console.log(`- Investors: ${investors.length}`); // ------------------ - let options = []; - if (displayAllowPrimaryIssuance) { - options.push('Disallow primary issuance'); - } else { - options.push('Allow primary issuance'); - } - options.push('Add exempted wallet', 'Remove exempted wallet'); - if (displayIsInPercentage) { - options.push('Change transfer limit to tokens', 'Change default percentage limit', - 'Set percentage transfer limit per account', 'Remove percentage transfer limit per account'); - } else { - options.push('Change transfer limit to percentage', 'Change default tokens limit', - 'Set tokens transfer limit per account', 'Remove tokens transfer limit per account'); - } + let options = [ + 'Modify properties', + 'Show investors', + 'Add this lockup to investors', + 'Remove this lockup from investors', + 'Delete this lockup type' + ]; - let index = readlineSync.keyInSelect(options, 'What do you want to do?', { cancel: 'Return' }); - let optionSelected = options[index]; - console.log('Selected:', index != -1 ? optionSelected : 'Return', '\n'); + let index = readlineSync.keyInSelect(options, 'What do you want to do?', { cancel: 'RETURN' }); + let optionSelected = index !== -1 ? options[index] : 'RETURN'; + console.log('Selected:', optionSelected, '\n'); switch (optionSelected) { - case 'Allow primary issuance': - case 'Disallow primary issuance': - let disallowPrimaryIssuanceAction = currentTransferManager.methods.setAllowPrimaryIssuance(!displayAllowPrimaryIssuance); - await common.sendTransaction(disallowPrimaryIssuanceAction); + case 'Modify properties': + let lockupAmount = readlineSync.questionInt(`Enter the amount of tokens that will be locked: `); + let minuteFromNow = Math.floor(Date.now() / 1000) + 60; + let startTime = readlineSync.questionInt(`Enter the start time (Unix Epoch time) of the lockup type (a minute from now = ${minuteFromNow}): `, { defaultInput: minuteFromNow }); + let lockUpPeriodSeconds = readlineSync.questionInt(`Enter the total period (seconds) of the lockup type (ten minutes = 600): `, { defaultInput: 600 }); + let releaseFrequencySeconds = readlineSync.questionInt(`Enter how often to release a tranche of tokens in seconds (one minute = 60): `, { defaultInput: 60 }); + let modifyLockUpTypeAction = currentTransferManager.methods.modifyLockUpType(lockupAmount, startTime, lockUpPeriodSeconds, releaseFrequencySeconds, lockupName); + let modifyLockUpTypeReceipt = await common.sendTransaction(modifyLockUpTypeAction); + let modifyLockUpTypeEvent = common.getEventFromLogs(currentTransferManager._jsonInterface, modifyLockUpTypeReceipt.logs, 'ModifyLockUpType'); + console.log(chalk.green(`${web3.utils.hexToUtf8(modifyLockUpTypeEvent._lockupName)} lockup type has been modified successfully!`)); break; - case 'Add exempted wallet': - let walletToExempt = readlineSync.question('Enter the wallet to exempt: ', { - limit: function (input) { - return web3.utils.isAddress(input); - }, - limitMessage: "Must be a valid address" - }); - let addExemptWalletAction = currentTransferManager.methods.addExemptWallet(walletToExempt); - let addExemptWalletReceipt = await common.sendTransaction(addExemptWalletAction); - let addExemptWalletEvent = common.getEventFromLogs(currentTransferManager._jsonInterface, addExemptWalletReceipt.logs, 'ExemptWalletAdded'); - console.log(chalk.green(`${addExemptWalletEvent._wallet} has been exempted sucessfully!`)); + case 'Show investors': + if (investors.length > 0) { + console.log("************ List of investors ************"); + investors.map(i => console.log(i)); + } else { + console.log(chalk.yellow("There are no investors yet")); + } break; - case 'Remove exempted wallet': - let exemptedWallet = readlineSync.question('Enter the wallet to remove from exempt: ', { + case 'Add this lockup to investors': + let investorsToAdd = readlineSync.question(`Enter the addresses of the investors separated by comma (i.e.addr1, addr2, addr3): `, { limit: function (input) { - return web3.utils.isAddress(input); + return (input !== '' && input.split(",").every(a => web3.utils.isAddress(a))); }, - limitMessage: "Must be a valid address" - }); - let removeExemptWalletAction = currentTransferManager.methods.removeExemptWallet(exemptedWallet); - let removeExemptWalletReceipt = await common.sendTransaction(removeExemptWalletAction); - let removeExemptWalletEvent = common.getEventFromLogs(currentTransferManager._jsonInterface, removeExemptWalletReceipt.logs, 'ExemptWalletRemoved'); - console.log(chalk.green(`${removeExemptWalletEvent._wallet} has been removed from exempt wallets sucessfully!`)); + limitMessage: `All addresses must be valid` + }).split(","); + let addInvestorToLockupAction; + if (investorsToAdd.length === 1) { + addInvestorToLockupAction = currentTransferManager.methods.addLockUpByName(investorsToAdd[0], lockupName); + } else { + addInvestorToLockupAction = currentTransferManager.methods.addLockUpByNameMulti(investorsToAdd, investorsToAdd.map(i => lockupName)); + } + let addInvestorToLockupReceipt = await common.sendTransaction(addInvestorToLockupAction); + let addInvestorToLockupEvents = common.getMultipleEventsFromLogs(currentTransferManager._jsonInterface, addInvestorToLockupReceipt.logs, 'AddLockUpToUser'); + addInvestorToLockupEvents.map(e => console.log(chalk.green(`${e._userAddress} has been added to ${web3.utils.hexToUtf8(e._lockupName)} successfully!`))); break; - case 'Change transfer limit to tokens': - let newDefaultLimitInTokens = web3.utils.toWei(readlineSync.question('Enter the amount of tokens for default limit: ', { - limit: function (input) { - return parseInt(input) > 0; - }, - limitMessage: "Must be greater than zero" - })); - let changeTransferLimitToTokensAction = currentTransferManager.methods.changeTransferLimitToTokens(newDefaultLimitInTokens); - let changeTransferLimitToTokensReceipt = await common.sendTransaction(changeTransferLimitToTokensAction); - let changeTransferLimitToTokensEvent = common.getEventFromLogs(currentTransferManager._jsonInterface, changeTransferLimitToTokensReceipt.logs, 'GlobalTransferLimitInTokensSet'); - console.log(chalk.green(`Transfer limit has been set to tokens sucessfully!`)); - console.log(chalk.green(`The default transfer limit is ${web3.utils.fromWei(changeTransferLimitToTokensEvent._amount)} ${tokenSymbol} `)); - break; - case 'Change transfer limit to percentage': - let newDefaultLimitInPercentage = toWeiPercentage(readlineSync.question('Enter the percentage for default limit: ', { - limit: function (input) { - return (parseInt(input) > 0 && parseInt(input) <= 100); - }, - limitMessage: "Must be greater than 0 and less than 100" - })); - let changeTransferLimitToPercentageAction = currentTransferManager.methods.changeTransferLimitToPercentage(newDefaultLimitInPercentage); - let changeTransferLimitToPercentageReceipt = await common.sendTransaction(changeTransferLimitToPercentageAction); - let changeTransferLimitToPercentageEvent = common.getEventFromLogs(currentTransferManager._jsonInterface, changeTransferLimitToPercentageReceipt.logs, 'GlobalTransferLimitInPercentageSet'); - console.log(chalk.green(`Transfer limit has been set to tokens sucessfully!`)); - console.log(chalk.green(`The default transfer limit is ${fromWeiPercentage(changeTransferLimitToPercentageEvent._percentage)} % `)); - break; - case 'Change default percentage limit': - let defaultLimitInPercentage = toWeiPercentage(readlineSync.question('Enter the percentage for default limit: ', { + case 'Remove this lockup from investors': + let investorsToRemove = readlineSync.question(`Enter the addresses of the investors separated by comma (i.e.addr1, addr2, addr3): `, { limit: function (input) { - return (parseInt(input) > 0 && parseInt(input) <= 100); + return (input !== '' && input.split(",").every(a => web3.utils.isAddress(a))); }, - limitMessage: "Must be greater than 0 and less than 100" - })); - let changeGlobalLimitInPercentageAction = currentTransferManager.methods.changeGlobalLimitInPercentage(defaultLimitInPercentage); - let changeGlobalLimitInPercentageReceipt = await common.sendTransaction(changeGlobalLimitInPercentageAction); - let changeGlobalLimitInPercentageEvent = common.getEventFromLogs(currentTransferManager._jsonInterface, changeGlobalLimitInPercentageReceipt.logs, 'GlobalTransferLimitInPercentageSet'); - console.log(chalk.green(`The default transfer limit is ${fromWeiPercentage(changeGlobalLimitInPercentageEvent._percentage)} % `)); + limitMessage: `All addresses must be valid` + }).split(","); + let removeLockupFromInvestorAction; + if (investorsToRemove.length === 1) { + removeLockupFromInvestorAction = currentTransferManager.methods.removeLockUpFromUser(investorsToRemove[0], lockupName); + } else { + removeLockupFromInvestorAction = currentTransferManager.methods.removeLockUpFromUserMulti(investorsToRemove, investorsToRemove.map(i => lockupName)); + } + let removeLockUpFromUserReceipt = await common.sendTransaction(removeLockupFromInvestorAction); + let removeLockUpFromUserEvents = common.getMultipleEventsFromLogs(currentTransferManager._jsonInterface, removeLockUpFromUserReceipt.logs, 'RemoveLockUpFromUser'); + removeLockUpFromUserEvents.map(e => console.log(chalk.green(`${e._userAddress} has been removed to ${web3.utils.hexToUtf8(e._lockupName)} successfully!`))); break; - case 'Change default tokens limit': - let defaultLimitInTokens = web3.utils.toWei(readlineSync.question('Enter the amount of tokens for default limit: ', { - limit: function (input) { - return parseInt(input) > 0; - }, - limitMessage: "Must be greater than zero" - })); - let changeGlobalLimitInTokensAction = currentTransferManager.methods.changeGlobalLimitInTokens(defaultLimitInTokens); - let changeGlobalLimitInTokensReceipt = await common.sendTransaction(changeGlobalLimitInTokensAction); - let changeGlobalLimitInTokensEvent = common.getEventFromLogs(currentTransferManager._jsonInterface, changeGlobalLimitInTokensReceipt.logs, 'GlobalTransferLimitInTokensSet'); - console.log(chalk.green(`The default transfer limit is ${web3.utils.fromWei(changeGlobalLimitInTokensEvent._amount)} ${tokenSymbol} `)); + case 'Delete this lockup type': + let isEmpty = investors.length === 0; + if (!isEmpty) { + console.log(chalk.yellow(`This lockup have investors added to it. To delete it you must remove them first.`)); + if (readlineSync.keyInYNStrict(`Do you want to remove them? `)) { + let data = investors.map(i => [i, lockupName]) + let batches = common.splitIntoBatches(data, gbl.constants.DEFAULT_BATCH_SIZE); + let [investorArray, lockupNameArray] = common.transposeBatches(batches); + for (let batch = 0; batch < batches.length; batch++) { + console.log(`Batch ${batch + 1} - Attempting to remove the following investors:\n\n`, investorArray[batch], '\n'); + let action = currentTransferManager.methods.removeLockUpFromUserMulti(investorArray[batch], lockupNameArray[batch]); + let receipt = await common.sendTransaction(action); + console.log(chalk.green('Remove lockups from multiple investors transaction was successful.')); + console.log(`${receipt.gasUsed} gas used.Spent: ${web3.utils.fromWei((new web3.utils.BN(receipt.gasUsed)).mul(new web3.utils.BN(defaultGasPrice)))} ETH`); + } + isEmpty = true; + } + } + if (isEmpty) { + let removeLockupTypeAction = currentTransferManager.methods.removeLockupType(lockupName); + let removeLockupTypeReceipt = await common.sendTransaction(removeLockupTypeAction); + let removeLockupTypeEvent = common.getEventFromLogs(currentTransferManager._jsonInterface, removeLockupTypeReceipt.logs, 'RemoveLockUpType'); + console.log(chalk.green(`${web3.utils.hexToUtf8(removeLockupTypeEvent._lockupName)} lockup type has been deleted successfully!`)); + } + return; + case 'RETURN': + return; + } + + await manageExistingLockups(lockupName); +} + +async function operateWithMultipleLockups(currentLockups) { + let options = ['Add multiple lockups']; + if (currentLockups.length > 0) { + options.push('Modify multiple lockups'); + } + options.push( + 'Delete multiple lockups', + 'Add lockups to multiple investors', + 'Remove lockups from multiple investors' + ); + + let index = readlineSync.keyInSelect(options, 'What do you want to do?', { cancel: 'RETURN' }); + let optionSelected = index !== -1 ? options[index] : 'RETURN'; + console.log('Selected:', optionSelected, '\n'); + switch (optionSelected) { + case 'Add multiple lockups': + await addLockupsInBatch(); break; - case 'Set percentage transfer limit per account': - let percentageAccount = readlineSync.question('Enter the wallet: ', { - limit: function (input) { - return web3.utils.isAddress(input); - }, - limitMessage: "Must be a valid address" - }); - let accountLimitInPercentage = toWeiPercentage(readlineSync.question(`Enter the transfer limit for ${percentageAccount} in percentage: `, { - limit: function (input) { - return (parseInt(input) > 0 && parseInt(input) <= 100); - }, - limitMessage: "Must be greater than 0 and less than 100" - })); - let setTransferLimitInPercentageAction = currentTransferManager.methods.setTransferLimitInPercentage(percentageAccount, accountLimitInPercentage); - let setTransferLimitInPercentageReceipt = await common.sendTransaction(setTransferLimitInPercentageAction); - let setTransferLimitInPercentageEvent = common.getEventFromLogs(currentTransferManager._jsonInterface, setTransferLimitInPercentageReceipt.logs, 'TransferLimitInPercentageSet'); - console.log(chalk.green(`The transfer limit for ${setTransferLimitInPercentageEvent._wallet} is ${fromWeiPercentage(setTransferLimitInPercentageEvent._percentage)} % `)); + case 'Modify multiple lockups': + await modifyLockupsInBatch(); break; - case 'Set tokens transfer limit per account': - let tokensAccount = readlineSync.question('Enter the wallet: ', { - limit: function (input) { - return web3.utils.isAddress(input); - }, - limitMessage: "Must be a valid address" - }); - let accountLimitInTokens = web3.utils.toWei(readlineSync.question(`Enter the transfer limit for ${tokensAccount} in amount of tokens: `, { - limit: function (input) { - return parseInt(input) > 0; - }, - limitMessage: "Must be greater than zero" - })); - let setTransferLimitInTokensAction = currentTransferManager.methods.setTransferLimitInTokens(tokensAccount, accountLimitInTokens); - let setTransferLimitInTokensReceipt = await common.sendTransaction(setTransferLimitInTokensAction); - let setTransferLimitInTokensEvent = common.getEventFromLogs(currentTransferManager._jsonInterface, setTransferLimitInTokensReceipt.logs, 'TransferLimitInTokensSet'); - console.log(chalk.green(`The transfer limit for ${setTransferLimitInTokensEvent._wallet} is ${web3.utils.fromWei(setTransferLimitInTokensEvent._amount)} ${tokenSymbol} `)); + case 'Delete multiple lockups': + await deleteLockupsInBatch(); break; - case 'Remove percentage transfer limit per account': - let percentageAccountToRemove = readlineSync.question('Enter the wallet to remove: ', { - limit: function (input) { - return web3.utils.isAddress(input); - }, - limitMessage: "Must be a valid address" - }); - let removeTransferLimitInPercentageAction = currentTransferManager.methods.removeTransferLimitInPercentage(percentageAccountToRemove); - let removeTransferLimitInPercentageReceipt = await common.sendTransaction(removeTransferLimitInPercentageAction); - let removeTransferLimitInPercentageEvent = common.getEventFromLogs(currentTransferManager._jsonInterface, removeTransferLimitInPercentageReceipt.logs, 'TransferLimitInPercentageRemoved'); - console.log(chalk.green(`The transfer limit for ${removeTransferLimitInPercentageEvent._wallet} is the default limit`)); + case 'Add lockups to multiple investors': + await addLockupsToInvestorsInBatch(); break; - case 'Remove tokens transfer limit per account': - let tokensAccountToRemove = readlineSync.question('Enter the wallet to remove: ', { - limit: function (input) { - return web3.utils.isAddress(input); - }, - limitMessage: "Must be a valid address" - }); - let removeTransferLimitInTokensAction = currentTransferManager.methods.removeTransferLimitInTokens(tokensAccountToRemove); - let removeTransferLimitInTokensReceipt = await common.sendTransaction(removeTransferLimitInTokensAction); - let removeTransferLimitInTokensEvent = common.getEventFromLogs(currentTransferManager._jsonInterface, removeTransferLimitInTokensReceipt.logs, 'TransferLimitInTokensRemoved'); - console.log(chalk.green(`The transfer limit for ${removeTransferLimitInTokensEvent._wallet} is the default limit`)); + case 'Remove lockups from multiple investors': + await removeLockupsFromInvestorsInBatch(); break; } } +async function addLockupsInBatch() { + let csvFilePath = readlineSync.question(`Enter the path for csv data file (${ADD_LOCKUP_DATA_CSV}): `, { + defaultInput: ADD_LOCKUP_DATA_CSV + }); + let batchSize = readlineSync.question(`Enter the max number of records per transaction or batch size (${gbl.constants.DEFAULT_BATCH_SIZE}): `, { + limit: function (input) { + return parseInt(input) > 0; + }, + limitMessage: 'Must be greater than 0', + defaultInput: gbl.constants.DEFAULT_BATCH_SIZE + }); + let parsedData = csvParse(csvFilePath); + let validData = parsedData.filter( + row => !isNaN(row[0]) && + moment.unix(row[1]).isValid() && + (!isNaN(row[2] && (parseFloat(row[2]) % 1 === 0))) && + (!isNaN(row[3] && (parseFloat(row[3]) % 1 === 0))) && + typeof row[4] === 'string'); + let invalidRows = parsedData.filter(row => !validData.includes(row)); + if (invalidRows.length > 0) { + console.log(chalk.red(`The following lines from csv file are not valid: ${invalidRows.map(r => parsedData.indexOf(r) + 1).join(',')} `)); + } + let batches = common.splitIntoBatches(validData, batchSize); + let [amountArray, startTimeArray, lockUpPeriodArray, releaseFrequencyArray, lockupNameArray] = common.transposeBatches(batches); + for (let batch = 0; batch < batches.length; batch++) { + console.log(`Batch ${batch + 1} - Attempting to add the following lockups: \n\n`, lockupNameArray[batch], '\n'); + lockupNameArray[batch] = lockupNameArray[batch].map(n => web3.utils.toHex(n)); + let action = currentTransferManager.methods.addNewLockUpTypeMulti(amountArray[batch], startTimeArray[batch], lockUpPeriodArray[batch], releaseFrequencyArray[batch], lockupNameArray[batch]); + let receipt = await common.sendTransaction(action); + console.log(chalk.green('Add multiple lockups transaction was successful.')); + console.log(`${receipt.gasUsed} gas used.Spent: ${web3.utils.fromWei((new web3.utils.BN(receipt.gasUsed)).mul(new web3.utils.BN(defaultGasPrice)))} ETH`); + } +} + +async function modifyLockupsInBatch() { + let csvFilePath = readlineSync.question(`Enter the path for csv data file (${MODIFY_LOCKUP_DATA_CSV}): `, { + defaultInput: MODIFY_LOCKUP_DATA_CSV + }); + let batchSize = readlineSync.question(`Enter the max number of records per transaction or batch size (${gbl.constants.DEFAULT_BATCH_SIZE}): `, { + limit: function (input) { + return parseInt(input) > 0; + }, + limitMessage: 'Must be greater than 0', + defaultInput: gbl.constants.DEFAULT_BATCH_SIZE + }); + let parsedData = csvParse(csvFilePath); + let validData = parsedData.filter( + row => !isNaN(row[0]) && + moment.unix(row[1]).isValid() && + (!isNaN(row[2] && (parseFloat(row[2]) % 1 === 0))) && + (!isNaN(row[3] && (parseFloat(row[3]) % 1 === 0))) && + typeof row[4] === 'string'); + let invalidRows = parsedData.filter(row => !validData.includes(row)); + if (invalidRows.length > 0) { + console.log(chalk.red(`The following lines from csv file are not valid: ${invalidRows.map(r => parsedData.indexOf(r) + 1).join(',')} `)); + } + let batches = common.splitIntoBatches(validData, batchSize); + let [amountArray, startTimeArray, lockUpPeriodArray, releaseFrequencyArray, lockupNameArray] = common.transposeBatches(batches); + for (let batch = 0; batch < batches.length; batch++) { + console.log(`Batch ${batch + 1} - Attempting to modify the following lockups: \n\n`, lockupNameArray[batch], '\n'); + lockupNameArray[batch] = lockupNameArray[batch].map(n => web3.utils.toHex(n)); + let action = currentTransferManager.methods.modifyLockUpTypeMulti(amountArray[batch], startTimeArray[batch], lockUpPeriodArray[batch], releaseFrequencyArray[batch], lockupNameArray[batch]); + let receipt = await common.sendTransaction(action); + console.log(chalk.green('Modify multiple lockups transaction was successful.')); + console.log(`${receipt.gasUsed} gas used.Spent: ${web3.utils.fromWei((new web3.utils.BN(receipt.gasUsed)).mul(new web3.utils.BN(defaultGasPrice)))} ETH`); + } +} + +async function deleteLockupsInBatch() { + let csvFilePath = readlineSync.question(`Enter the path for csv data file (${DELETE_LOCKUP_DATA_CSV}): `, { + defaultInput: DELETE_LOCKUP_DATA_CSV + }); + let batchSize = readlineSync.question(`Enter the max number of records per transaction or batch size (${gbl.constants.DEFAULT_BATCH_SIZE}): `, { + limit: function (input) { + return parseInt(input) > 0; + }, + limitMessage: 'Must be greater than 0', + defaultInput: gbl.constants.DEFAULT_BATCH_SIZE + }); + let parsedData = csvParse(csvFilePath); + let validData = parsedData.filter(row => typeof row[0] === 'string'); + let invalidRows = parsedData.filter(row => !validData.includes(row)); + if (invalidRows.length > 0) { + console.log(chalk.red(`The following lines from csv file are not valid: ${invalidRows.map(r => parsedData.indexOf(r) + 1).join(',')} `)); + } + let batches = common.splitIntoBatches(validData, batchSize); + let [lockupNameArray] = common.transposeBatches(batches); + for (let batch = 0; batch < batches.length; batch++) { + console.log(`Batch ${batch + 1} - Attempting to delete the following lockups: \n\n`, lockupNameArray[batch], '\n'); + lockupNameArray[batch] = lockupNameArray[batch].map(n => web3.utils.toHex(n)); + let action = currentTransferManager.methods.removeLockupTypeMulti(lockupNameArray[batch]); + let receipt = await common.sendTransaction(action); + console.log(chalk.green('Delete multiple lockups transaction was successful.')); + console.log(`${receipt.gasUsed} gas used.Spent: ${web3.utils.fromWei((new web3.utils.BN(receipt.gasUsed)).mul(new web3.utils.BN(defaultGasPrice)))} ETH`); + } +} + +async function addLockupsToInvestorsInBatch() { + let csvFilePath = readlineSync.question(`Enter the path for csv data file (${ADD_LOCKUP_INVESTOR_DATA_CSV}): `, { + defaultInput: ADD_LOCKUP_INVESTOR_DATA_CSV + }); + let batchSize = readlineSync.question(`Enter the max number of records per transaction or batch size (${gbl.constants.DEFAULT_BATCH_SIZE}): `, { + limit: function (input) { + return parseInt(input) > 0; + }, + limitMessage: 'Must be greater than 0', + defaultInput: gbl.constants.DEFAULT_BATCH_SIZE + }); + let parsedData = csvParse(csvFilePath); + let validData = parsedData.filter( + row => web3.utils.isAddress(row[0]) && + typeof row[1] === 'string'); + let invalidRows = parsedData.filter(row => !validData.includes(row)); + if (invalidRows.length > 0) { + console.log(chalk.red(`The following lines from csv file are not valid: ${invalidRows.map(r => parsedData.indexOf(r) + 1).join(',')} `)); + } + let batches = common.splitIntoBatches(validData, batchSize); + let [investorArray, lockupNameArray] = common.transposeBatches(batches); + for (let batch = 0; batch < batches.length; batch++) { + console.log(`Batch ${batch + 1} - Attempting to add lockups to the following investors: \n\n`, investorArray[batch], '\n'); + lockupNameArray[batch] = lockupNameArray[batch].map(n => web3.utils.toHex(n)); + let action = currentTransferManager.methods.addLockUpByNameMulti(investorArray[batch], lockupNameArray[batch]); + let receipt = await common.sendTransaction(action); + console.log(chalk.green('Add lockups to multiple investors transaction was successful.')); + console.log(`${receipt.gasUsed} gas used.Spent: ${web3.utils.fromWei((new web3.utils.BN(receipt.gasUsed)).mul(new web3.utils.BN(defaultGasPrice)))} ETH`); + } +} + +async function removeLockupsFromInvestorsInBatch() { + let csvFilePath = readlineSync.question(`Enter the path for csv data file (${REMOVE_LOCKUP_INVESTOR_DATA_CSV}): `, { + defaultInput: REMOVE_LOCKUP_INVESTOR_DATA_CSV + }); + let batchSize = readlineSync.question(`Enter the max number of records per transaction or batch size (${gbl.constants.DEFAULT_BATCH_SIZE}): `, { + limit: function (input) { + return parseInt(input) > 0; + }, + limitMessage: 'Must be greater than 0', + defaultInput: gbl.constants.DEFAULT_BATCH_SIZE + }); + let parsedData = csvParse(csvFilePath); + let validData = parsedData.filter( + row => web3.utils.isAddress(row[0]) && + typeof row[1] === 'string'); + let invalidRows = parsedData.filter(row => !validData.includes(row)); + if (invalidRows.length > 0) { + console.log(chalk.red(`The following lines from csv file are not valid: ${invalidRows.map(r => parsedData.indexOf(r) + 1).join(',')} `)); + } + let batches = common.splitIntoBatches(validData, batchSize); + let [investorArray, lockupNameArray] = common.transposeBatches(batches); + for (let batch = 0; batch < batches.length; batch++) { + console.log(`Batch ${batch + 1} - Attempting to remove the following investors: \n\n`, investorArray[batch], '\n'); + lockupNameArray[batch] = lockupNameArray[batch].map(n => web3.utils.toHex(n)); + let action = currentTransferManager.methods.removeLockUpFromUserMulti(investorArray[batch], lockupNameArray[batch]); + let receipt = await common.sendTransaction(action); + console.log(chalk.green('Remove lockups from multiple investors transaction was successful.')); + console.log(`${receipt.gasUsed} gas used.Spent: ${web3.utils.fromWei((new web3.utils.BN(receipt.gasUsed)).mul(new web3.utils.BN(defaultGasPrice)))} ETH`); + } +} + /* // Copied from tests function signData(tmAddress, investorAddress, fromTime, toTime, expiryTime, restricted, validFrom, validTo, pk) { @@ -1066,11 +2613,11 @@ function signData(tmAddress, investorAddress, fromTime, toTime, expiryTime, rest */ function toWeiPercentage(number) { - return new web3.utils.BN(web3.utils.toWei(number)).divn(100); + return web3.utils.toWei((parseFloat(number) / 100).toString()); } function fromWeiPercentage(number) { - return web3.utils.fromWei(new web3.utils.BN(number).muln(100)); + return web3.utils.fromWei(new web3.utils.BN(number).muln(100)).toString(); } async function getAllModulesByType(type) { @@ -1092,7 +2639,7 @@ async function getAllModulesByType(type) { let nameTemp = web3.utils.hexToUtf8(details[0]); let pausedTemp = null; if (type == gbl.constants.MODULES_TYPES.STO || type == gbl.constants.MODULES_TYPES.TRANSFER) { - let abiTemp = JSON.parse(require('fs').readFileSync(`./build/contracts/${nameTemp}.json`).toString()).abi; + let abiTemp = JSON.parse(require('fs').readFileSync(`${__dirname}/../../build/contracts/${nameTemp}.json`).toString()).abi; let contractTemp = new web3.eth.Contract(abiTemp, details[1]); pausedTemp = await contractTemp.methods.paused().call(); } @@ -1159,13 +2706,13 @@ async function selectToken() { }); options.push('Enter token symbol manually'); - let index = readlineSync.keyInSelect(options, 'Select a token:', { cancel: 'Exit' }); - let selected = index != -1 ? options[index] : 'Exit'; + let index = readlineSync.keyInSelect(options, 'Select a token:', { cancel: 'EXIT' }); + let selected = index != -1 ? options[index] : 'EXIT'; switch (selected) { case 'Enter token symbol manually': result = readlineSync.question('Enter the token symbol: '); break; - case 'Exit': + case 'EXIT': process.exit(); break; default: @@ -1178,13 +2725,13 @@ async function selectToken() { async function logTotalInvestors() { let investorsCount = await securityToken.methods.getInvestorCount().call(); - console.log(chalk.yellow(`Total investors at the moment: ${investorsCount}`)); + console.log(chalk.yellow(`Total investors at the moment: ${investorsCount} `)); } async function logBalance(from, totalSupply) { let fromBalance = web3.utils.fromWei(await securityToken.methods.balanceOf(from).call()); let percentage = totalSupply != '0' ? ` - ${parseFloat(fromBalance) / parseFloat(totalSupply) * 100}% of total supply` : ''; - console.log(chalk.yellow(`Balance of ${from}: ${fromBalance} ${tokenSymbol}${percentage}`)); + console.log(chalk.yellow(`Balance of ${from}: ${fromBalance} ${tokenSymbol} ${percentage} `)); } module.exports = { @@ -1194,6 +2741,14 @@ module.exports = { }, addTransferManagerModule: async function (_tokenSymbol) { await initialize(_tokenSymbol); - return addTransferManagerModule() + return addTransferManagerModule(); + }, + modifyWhitelistInBatch: async function (_tokenSymbol, _csvFilePath, _batchSize) { + await initialize(_tokenSymbol); + let gmtModules = await securityToken.methods.getModulesByName(web3.utils.toHex('GeneralTransferManager')).call(); + let generalTransferManagerAddress = gmtModules[0]; + currentTransferManager = new web3.eth.Contract(abis.generalTransferManager(), generalTransferManagerAddress); + currentTransferManager.setProvider(web3.currentProvider); + return modifyWhitelistInBatch(_csvFilePath, _batchSize); } } \ No newline at end of file diff --git a/CLI/data/Checkpoint/exclusions_data.csv b/CLI/data/Checkpoint/exclusions_data.csv new file mode 100644 index 000000000..406cd94cf --- /dev/null +++ b/CLI/data/Checkpoint/exclusions_data.csv @@ -0,0 +1,5 @@ +0xee7ae74d964f2be7d72c1b187b38e2ed3615d4d1 +0x49fc0b78238dab644698a90fa351b4c749e123d2 +0x10223927009b8add0960359dd90d1449415b7ca9 +0x3c65cfe3de848cf38e9d76e9c3e57a2f1140b399 +0xabf60de3265b3017db7a1be66fc8b364ec1dbb98 \ No newline at end of file diff --git a/CLI/data/Checkpoint/tax_withholding_data.csv b/CLI/data/Checkpoint/tax_withholding_data.csv new file mode 100644 index 000000000..10c2928c8 --- /dev/null +++ b/CLI/data/Checkpoint/tax_withholding_data.csv @@ -0,0 +1,10 @@ +0xee7ae74d964f2be7d72c1b187b38e2ed3615d4d1,0.5 +0x2f0fd672bf222413cc69dc1f4f1d7e93ad1763a1,1 +0xac297053173b02b02a737d47f7b4a718e5b170ef,2 +0x49fc0b78238dab644698a90fa351b4c749e123d2,10 +0x10223927009b8add0960359dd90d1449415b7ca9,15 +0x3c65cfe3de848cf38e9d76e9c3e57a2f1140b399,50 +0xabf60de3265b3017db7a1be66fc8b364ec1dbb98,0 +0xb841fe5a89da1bbef2d0805fbd7ffcbbb2fca5e3,23 +0x56be93088141b16ebaa9416122fd1d928da25ecf,45 +0xbb276b6f68f0a41d54b7e0a608fe8eb1ebdee7b0,67 \ No newline at end of file diff --git a/CLI/data/Transfer/BlacklistTM/add_blacklist_data.csv b/CLI/data/Transfer/BlacklistTM/add_blacklist_data.csv new file mode 100644 index 000000000..1d925053d --- /dev/null +++ b/CLI/data/Transfer/BlacklistTM/add_blacklist_data.csv @@ -0,0 +1,4 @@ +1559401200,1560178800,"FirstTenDays",30 +1560535200,1560621600,"NoRepeat",0 +1566734400,1567252800,"Every90",90 +1567296000,1567303200,"TwoHours",2 diff --git a/CLI/data/Transfer/BlacklistTM/add_investor_blacklist_data.csv b/CLI/data/Transfer/BlacklistTM/add_investor_blacklist_data.csv new file mode 100644 index 000000000..cd0fe922c --- /dev/null +++ b/CLI/data/Transfer/BlacklistTM/add_investor_blacklist_data.csv @@ -0,0 +1,12 @@ +0xee7ae74d964f2be7d72c1b187b38e2ed3615d4d1,"FirstTenDays" +0x2f0fd672bf222413cc69dc1f4f1d7e93ad1763a1,"FirstTenDays" +0xac297053173b02b02a737d47f7b4a718e5b170ef,"FirstTenDays" +0x49fc0b78238dab644698a90fa351b4c749e123d2,"FirstTenDays" +0x10223927009b8add0960359dd90d1449415b7ca9,"FirstTenDays" +0x49fc0b78238dab644698a90fa351b4c749e123d2,"NoRepeat" +0x10223927009b8add0960359dd90d1449415b7ca9,"NoRepeat" +0x3c65cfe3de848cf38e9d76e9c3e57a2f1140b399,"NoRepeat" +0xabf60de3265b3017db7a1be66fc8b364ec1dbb98,"NoRepeat" +0x10223927009b8add0960359dd90d1449415b7ca9,"Every90" +0xb841fe5a89da1bbef2d0805fbd7ffcbbb2fca5e3,"Every90" +0x56be93088141b16ebaa9416122fd1d928da25ecf,"Every90" \ No newline at end of file diff --git a/CLI/data/Transfer/BlacklistTM/delete_blacklist_data.csv b/CLI/data/Transfer/BlacklistTM/delete_blacklist_data.csv new file mode 100644 index 000000000..752cb89c1 --- /dev/null +++ b/CLI/data/Transfer/BlacklistTM/delete_blacklist_data.csv @@ -0,0 +1,4 @@ +"FirstTenDays" +"Every90" +"NoRepeat" +"TwoHours" \ No newline at end of file diff --git a/CLI/data/Transfer/BlacklistTM/modify_blacklist_data.csv b/CLI/data/Transfer/BlacklistTM/modify_blacklist_data.csv new file mode 100644 index 000000000..4b5e45b4d --- /dev/null +++ b/CLI/data/Transfer/BlacklistTM/modify_blacklist_data.csv @@ -0,0 +1,4 @@ +1559412000,1560178800,"FirstTenDays",30 +1560535200,1561032000,"NoRepeat",0 +1566734400,1567252800,"Every90",90 +1567296000,1567303200,"TwoHours",3 diff --git a/CLI/data/Transfer/BlacklistTM/remove_investor_blacklist_data.csv b/CLI/data/Transfer/BlacklistTM/remove_investor_blacklist_data.csv new file mode 100644 index 000000000..cd0fe922c --- /dev/null +++ b/CLI/data/Transfer/BlacklistTM/remove_investor_blacklist_data.csv @@ -0,0 +1,12 @@ +0xee7ae74d964f2be7d72c1b187b38e2ed3615d4d1,"FirstTenDays" +0x2f0fd672bf222413cc69dc1f4f1d7e93ad1763a1,"FirstTenDays" +0xac297053173b02b02a737d47f7b4a718e5b170ef,"FirstTenDays" +0x49fc0b78238dab644698a90fa351b4c749e123d2,"FirstTenDays" +0x10223927009b8add0960359dd90d1449415b7ca9,"FirstTenDays" +0x49fc0b78238dab644698a90fa351b4c749e123d2,"NoRepeat" +0x10223927009b8add0960359dd90d1449415b7ca9,"NoRepeat" +0x3c65cfe3de848cf38e9d76e9c3e57a2f1140b399,"NoRepeat" +0xabf60de3265b3017db7a1be66fc8b364ec1dbb98,"NoRepeat" +0x10223927009b8add0960359dd90d1449415b7ca9,"Every90" +0xb841fe5a89da1bbef2d0805fbd7ffcbbb2fca5e3,"Every90" +0x56be93088141b16ebaa9416122fd1d928da25ecf,"Every90" \ No newline at end of file diff --git a/CLI/data/Transfer/GTM/whitelist_data.csv b/CLI/data/Transfer/GTM/whitelist_data.csv index 3276dec64..236cac436 100644 --- a/CLI/data/Transfer/GTM/whitelist_data.csv +++ b/CLI/data/Transfer/GTM/whitelist_data.csv @@ -7,4 +7,4 @@ 0xabf60de3265b3017db7a1be66fc8b364ec1dbb98,5/5/2018,1/8/2018,10/10/2019,false 0xb841fe5a89da1bbef2d0805fbd7ffcbbb2fca5e3,5/5/2018,1/8/2018,10/10/2019,false 0x56be93088141b16ebaa9416122fd1d928da25ecf,5/5/2018,1/8/2018,10/10/2019,false -0xbb276b6f68f0a41d54b7e0a608fe8eb1ebdee7b0,5/5/2018,1/8/2018,10/10/2019,true +0xbb276b6f68f0a41d54b7e0a608fe8eb1ebdee7b0,5/5/2018,1/8/2018,10/10/2019,true \ No newline at end of file diff --git a/CLI/data/Transfer/LockupTM/add_lockup_data.csv b/CLI/data/Transfer/LockupTM/add_lockup_data.csv new file mode 100644 index 000000000..f3d27ab2d --- /dev/null +++ b/CLI/data/Transfer/LockupTM/add_lockup_data.csv @@ -0,0 +1,4 @@ +1000,1560178800,600,1,"TenMinutes" +1000,1560621600,3600,60,"OneHour" +2000,1567252800,7200,3600,"TwoHours" +3000,1567303200,14400,4800,"4Hours" diff --git a/CLI/data/Transfer/LockupTM/add_lockup_investor_data.csv b/CLI/data/Transfer/LockupTM/add_lockup_investor_data.csv new file mode 100644 index 000000000..68c08a7d7 --- /dev/null +++ b/CLI/data/Transfer/LockupTM/add_lockup_investor_data.csv @@ -0,0 +1,12 @@ +0xee7ae74d964f2be7d72c1b187b38e2ed3615d4d1,"TenMinutes" +0x2f0fd672bf222413cc69dc1f4f1d7e93ad1763a1,"TenMinutes" +0xac297053173b02b02a737d47f7b4a718e5b170ef,"TenMinutes" +0x49fc0b78238dab644698a90fa351b4c749e123d2,"TenMinutes" +0x10223927009b8add0960359dd90d1449415b7ca9,"TenMinutes" +0x49fc0b78238dab644698a90fa351b4c749e123d2,"OneHour" +0x10223927009b8add0960359dd90d1449415b7ca9,"OneHour" +0x3c65cfe3de848cf38e9d76e9c3e57a2f1140b399,"OneHour" +0xabf60de3265b3017db7a1be66fc8b364ec1dbb98,"OneHour" +0x10223927009b8add0960359dd90d1449415b7ca9,"OneHour" +0xb841fe5a89da1bbef2d0805fbd7ffcbbb2fca5e3,"OneHour" +0x56be93088141b16ebaa9416122fd1d928da25ecf,"OneHour" \ No newline at end of file diff --git a/CLI/data/Transfer/LockupTM/delete_lockup_data.csv b/CLI/data/Transfer/LockupTM/delete_lockup_data.csv new file mode 100644 index 000000000..0d9203ffe --- /dev/null +++ b/CLI/data/Transfer/LockupTM/delete_lockup_data.csv @@ -0,0 +1,2 @@ +"TwoHours" +"4Hours" \ No newline at end of file diff --git a/CLI/data/Transfer/LockupTM/modify_lockup_data.csv b/CLI/data/Transfer/LockupTM/modify_lockup_data.csv new file mode 100644 index 000000000..2b520c4ff --- /dev/null +++ b/CLI/data/Transfer/LockupTM/modify_lockup_data.csv @@ -0,0 +1,4 @@ +1000,1560178800,600,10,"TenMinutes" +1000,1560623200,3600,60,"OneHour" +2000,1567252800,7200,3600,"TwoHours" +6000,1567303200,14400,4800,"4Hours" diff --git a/CLI/data/Transfer/LockupTM/remove_lockup_investor_data.csv b/CLI/data/Transfer/LockupTM/remove_lockup_investor_data.csv new file mode 100644 index 000000000..68c08a7d7 --- /dev/null +++ b/CLI/data/Transfer/LockupTM/remove_lockup_investor_data.csv @@ -0,0 +1,12 @@ +0xee7ae74d964f2be7d72c1b187b38e2ed3615d4d1,"TenMinutes" +0x2f0fd672bf222413cc69dc1f4f1d7e93ad1763a1,"TenMinutes" +0xac297053173b02b02a737d47f7b4a718e5b170ef,"TenMinutes" +0x49fc0b78238dab644698a90fa351b4c749e123d2,"TenMinutes" +0x10223927009b8add0960359dd90d1449415b7ca9,"TenMinutes" +0x49fc0b78238dab644698a90fa351b4c749e123d2,"OneHour" +0x10223927009b8add0960359dd90d1449415b7ca9,"OneHour" +0x3c65cfe3de848cf38e9d76e9c3e57a2f1140b399,"OneHour" +0xabf60de3265b3017db7a1be66fc8b364ec1dbb98,"OneHour" +0x10223927009b8add0960359dd90d1449415b7ca9,"OneHour" +0xb841fe5a89da1bbef2d0805fbd7ffcbbb2fca5e3,"OneHour" +0x56be93088141b16ebaa9416122fd1d928da25ecf,"OneHour" \ No newline at end of file diff --git a/CLI/data/Transfer/MATM/add_manualapproval_data.csv b/CLI/data/Transfer/MATM/add_manualapproval_data.csv new file mode 100644 index 000000000..48d5f2a54 --- /dev/null +++ b/CLI/data/Transfer/MATM/add_manualapproval_data.csv @@ -0,0 +1,5 @@ +0x127b4F58A825Dfc6d4058AcdB7bB397d1F0411b5,0x1DDF4FDBB8eaDB5dD6EeC6edB1A91a0926940a33,10,12/12/2019,Lorem ipsum dolor sit amet +0x127b4F58A825Dfc6d4058AcdB7bB397d1F0411b5,0xB849AC17d881183800300A31f022d3fe4B82D457,20,12/12/2019,Consectetur adipiscing elit +0x127b4F58A825Dfc6d4058AcdB7bB397d1F0411b5,0xD122e1951cfb337D5CC8bc5aDECC0eb66ffb4B80,25,12/12/2019,Pellentesque ultrices eros +0x127b4F58A825Dfc6d4058AcdB7bB397d1F0411b5,0x1D9127821A244b64852E8Da431bb389B81710985,20,12/12/2019,Non eleifend ante tincidunt eget +0x127b4F58A825Dfc6d4058AcdB7bB397d1F0411b5,0x42dc79375E511Fb7Ee17fF50417141bAE5E5698E,10,12/12/2019,Sed ante arcu \ No newline at end of file diff --git a/CLI/data/Transfer/MATM/modify_manualapproval_data.csv b/CLI/data/Transfer/MATM/modify_manualapproval_data.csv new file mode 100644 index 000000000..7e695f3b2 --- /dev/null +++ b/CLI/data/Transfer/MATM/modify_manualapproval_data.csv @@ -0,0 +1,5 @@ +0x127b4F58A825Dfc6d4058AcdB7bB397d1F0411b5,0x1DDF4FDBB8eaDB5dD6EeC6edB1A91a0926940a33,12/12/2019,1,Lorem ipsum dolor sit amet,0 +0x127b4F58A825Dfc6d4058AcdB7bB397d1F0411b5,0xB849AC17d881183800300A31f022d3fe4B82D457,12/12/2019,2,Consectetur adipiscing elit,0 +0x127b4F58A825Dfc6d4058AcdB7bB397d1F0411b5,0xD122e1951cfb337D5CC8bc5aDECC0eb66ffb4B80,12/12/2019,3,Pellentesque ultrices eros,1 +0x127b4F58A825Dfc6d4058AcdB7bB397d1F0411b5,0x1D9127821A244b64852E8Da431bb389B81710985,12/12/2019,4,Non eleifend ante tincidunt eget,2 +0x127b4F58A825Dfc6d4058AcdB7bB397d1F0411b5,0x42dc79375E511Fb7Ee17fF50417141bAE5E5698E,12/12/2019,5,Sed ante arcu,1 \ No newline at end of file diff --git a/CLI/data/Transfer/MATM/revoke_manualapproval_data.csv b/CLI/data/Transfer/MATM/revoke_manualapproval_data.csv new file mode 100644 index 000000000..b398caf02 --- /dev/null +++ b/CLI/data/Transfer/MATM/revoke_manualapproval_data.csv @@ -0,0 +1,2 @@ +0x127b4F58A825Dfc6d4058AcdB7bB397d1F0411b5,0x1DDF4FDBB8eaDB5dD6EeC6edB1A91a0926940a33 +0x127b4F58A825Dfc6d4058AcdB7bB397d1F0411b5,0xB849AC17d881183800300A31f022d3fe4B82D457 \ No newline at end of file diff --git a/CLI/data/Transfer/VRTM/add_custom_restriction_data.csv b/CLI/data/Transfer/VRTM/add_custom_restriction_data.csv new file mode 100644 index 000000000..1807e90e6 --- /dev/null +++ b/CLI/data/Transfer/VRTM/add_custom_restriction_data.csv @@ -0,0 +1,8 @@ +0xee7ae74d964f2be7d72c1b187b38e2ed3615d4d1,1000,8/1/2019,90,10/10/2019,"Fixed" +0x2f0fd672bf222413cc69dc1f4f1d7e93ad1763a1,2000,8/2/2019,30,10/12/2019,"Fixed" +0xac297053173b02b02a737d47f7b4a718e5b170ef,500,8/1/2019,15,10/1/2019,"Fixed" +0x49fc0b78238dab644698a90fa351b4c749e123d2,0.15,8/5/2019,90,10/10/2019,"Percentage" +0x10223927009b8add0960359dd90d1449415b7ca9,0.25,8/3/2019,30,10/15/2019,"Percentage" +0x3c65cfe3de848cf38e9d76e9c3e57a2f1140b399,0.1,8/10/2019,15,10/10/2019,"Percentage" +0xabf60de3265b3017db7a1be66fc8b364ec1dbb98,1234,8/20/2019,10,10/22/2019,"Fixed" +0xb841fe5a89da1bbef2d0805fbd7ffcbbb2fca5e3,5678,8/1/2019,2,10/10/2019,"Fixed" \ No newline at end of file diff --git a/CLI/data/Transfer/VRTM/add_daily_restriction_data.csv b/CLI/data/Transfer/VRTM/add_daily_restriction_data.csv new file mode 100644 index 000000000..1486579b7 --- /dev/null +++ b/CLI/data/Transfer/VRTM/add_daily_restriction_data.csv @@ -0,0 +1,8 @@ +0xee7ae74d964f2be7d72c1b187b38e2ed3615d4d1,1000,8/1/2019,10/10/2019,"Fixed" +0x2f0fd672bf222413cc69dc1f4f1d7e93ad1763a1,2000,8/2/2019,10/12/2019,"Fixed" +0xac297053173b02b02a737d47f7b4a718e5b170ef,500,8/1/2019,10/10/2019,"Fixed" +0x49fc0b78238dab644698a90fa351b4c749e123d2,0.15,8/3/2019,10/1/2019,"Percentage" +0x10223927009b8add0960359dd90d1449415b7ca9,0.25,8/1/2019,10/10/2019,"Percentage" +0x3c65cfe3de848cf38e9d76e9c3e57a2f1140b399,0.1,8/1/2019,10/5/2019,"Percentage" +0xabf60de3265b3017db7a1be66fc8b364ec1dbb98,1234,8/12/2019,10/10/2019,"Fixed" +0xb841fe5a89da1bbef2d0805fbd7ffcbbb2fca5e3,5678,8/1/2019,10/10/2019,"Fixed" \ No newline at end of file diff --git a/CLI/data/Transfer/VRTM/modify_custom_restriction_data.csv b/CLI/data/Transfer/VRTM/modify_custom_restriction_data.csv new file mode 100644 index 000000000..b69f43361 --- /dev/null +++ b/CLI/data/Transfer/VRTM/modify_custom_restriction_data.csv @@ -0,0 +1,5 @@ +0xee7ae74d964f2be7d72c1b187b38e2ed3615d4d1,2000,8/1/2019,90,10/10/2019,"Fixed" +0x2f0fd672bf222413cc69dc1f4f1d7e93ad1763a1,2000,8/2/2019,30,10/10/2019,"Fixed" +0xac297053173b02b02a737d47f7b4a718e5b170ef,500,8/1/2019,20,10/10/2019,"Fixed" +0x49fc0b78238dab644698a90fa351b4c749e123d2,0.15,8/1/2019,90,20/10/2019,"Percentage" +0x10223927009b8add0960359dd90d1449415b7ca9,25,8/1/2019,30,10/10/2019,"Fixed" \ No newline at end of file diff --git a/CLI/data/Transfer/VRTM/modify_daily_restriction_data.csv b/CLI/data/Transfer/VRTM/modify_daily_restriction_data.csv new file mode 100644 index 000000000..908c164e9 --- /dev/null +++ b/CLI/data/Transfer/VRTM/modify_daily_restriction_data.csv @@ -0,0 +1,5 @@ +0xee7ae74d964f2be7d72c1b187b38e2ed3615d4d1,2000,8/1/2019,10/10/2019,"Fixed" +0x2f0fd672bf222413cc69dc1f4f1d7e93ad1763a1,2000,8/1/2019,10/10/2019,"Fixed" +0xac297053173b02b02a737d47f7b4a718e5b170ef,500,8/1/2019,10/10/2019,"Fixed" +0x49fc0b78238dab644698a90fa351b4c749e123d2,0.15,8/1/2019,20/10/2019,"Percentage" +0x10223927009b8add0960359dd90d1449415b7ca9,25,8/1/2019,10/10/2019,"Fixed" diff --git a/CLI/data/Transfer/VRTM/remove_custom_restriction_data.csv b/CLI/data/Transfer/VRTM/remove_custom_restriction_data.csv new file mode 100644 index 000000000..d927ee57b --- /dev/null +++ b/CLI/data/Transfer/VRTM/remove_custom_restriction_data.csv @@ -0,0 +1,2 @@ +0xabf60de3265b3017db7a1be66fc8b364ec1dbb98 +0xb841fe5a89da1bbef2d0805fbd7ffcbbb2fca5e3 diff --git a/CLI/data/Transfer/VRTM/remove_daily_restriction_data.csv b/CLI/data/Transfer/VRTM/remove_daily_restriction_data.csv new file mode 100644 index 000000000..a7fef30c4 --- /dev/null +++ b/CLI/data/Transfer/VRTM/remove_daily_restriction_data.csv @@ -0,0 +1,6 @@ +0xee7ae74d964f2be7d72c1b187b38e2ed3615d4d1 +0x2f0fd672bf222413cc69dc1f4f1d7e93ad1763a1 +0xac297053173b02b02a737d47f7b4a718e5b170ef +0x49fc0b78238dab644698a90fa351b4c749e123d2 +0x10223927009b8add0960359dd90d1449415b7ca9 +0x3c65cfe3de848cf38e9d76e9c3e57a2f1140b399 diff --git a/CLI/package.json b/CLI/package.json index 4dfcbf3a4..cc2773b74 100644 --- a/CLI/package.json +++ b/CLI/package.json @@ -4,7 +4,7 @@ "description": "CLI for Polymath-core", "main": "polymath-cli.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "stable_coin": "scripts/stable_coin.sh" }, "author": "Polymath Inc", "license": "MIT", diff --git a/CLI/polymath-cli.js b/CLI/polymath-cli.js index d2b4c68fa..c46425716 100644 --- a/CLI/polymath-cli.js +++ b/CLI/polymath-cli.js @@ -12,7 +12,7 @@ const transfer_manager = require('./commands/transfer_manager'); const contract_manager = require('./commands/contract_manager'); const strMigrator = require('./commands/strMigrator'); const permission_manager = require('./commands/permission_manager'); -const time = require('./commands/helpers/time') +const time = require('./commands/helpers/time'); const gbl = require('./commands/common/global'); const program = require('commander'); const moment = require('moment'); @@ -120,10 +120,17 @@ program .command('transfer_manager') .alias('tm') .option('-t, --securityToken ', 'Selects a ST to manage transfer modules') + .option('-w, --whitelist ', 'Whitelists addresses according to a csv file') + .option('-b, --batchSize ', 'Max number of records per transaction') .description('Runs transfer_manager') .action(async function (cmd) { await gbl.initialize(program.remoteNode); - await transfer_manager.executeApp(cmd.securityToken); + if (cmd.whitelist) { + let batchSize = cmd.batchSize ? cmd.batchSize : gbl.constants.DEFAULT_BATCH_SIZE; + await transfer_manager.modifyWhitelistInBatch(cmd.securityToken, cmd.whitelist, batchSize); + } else { + await transfer_manager.executeApp(cmd.securityToken); + } }); program @@ -155,7 +162,7 @@ program await permission_manager.executeApp(); }); -program + program .command('time_travel') .alias('tt') .option('-p, --period ', 'Period of time in seconds to increase') @@ -172,7 +179,6 @@ program await time.increaseTimeToEpochDate(cmd.toEpochTime); } }); - program.parse(process.argv); if (typeof program.commands.length == 0) { diff --git a/CLI/scripts/stable_coin.sh b/CLI/scripts/stable_coin.sh new file mode 100755 index 000000000..98415d7cc --- /dev/null +++ b/CLI/scripts/stable_coin.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +# Exit script as soon as a command fails. +set -o errexit + +# Token symbol read from args. +TOKEN_SYMBOL=$1 + +# Paths +CWD="$(pwd)" +WHITELIST='/data/Transfer/GTM/whitelist_data.csv' +MULTIMINT='/data/ST/multi_mint_data.csv' + +# Scripts + +node polymath-cli st -t $TOKEN_SYMBOL -o false -n $TOKEN_SYMBOL -d '' -D true +node polymath-cli tm -t $TOKEN_SYMBOL -w $CWD$WHITELIST +node polymath-cli stm -t $TOKEN_SYMBOL -m $CWD$MULTIMINT diff --git a/contracts/ModuleRegistry.sol b/contracts/ModuleRegistry.sol index 5c538d583..d90527993 100644 --- a/contracts/ModuleRegistry.sol +++ b/contracts/ModuleRegistry.sol @@ -103,7 +103,7 @@ contract ModuleRegistry is IModuleRegistry, EternalStorage { } function initialize(address _polymathRegistry, address _owner) external payable { - require(!getBool(Encoder.getKey("initialised")), "already initialized"); + require(!getBoolValue(Encoder.getKey("initialised")), "already initialized"); require(_owner != address(0) && _polymathRegistry != address(0), "0x address is invalid"); set(Encoder.getKey("polymathRegistry"), _polymathRegistry); set(Encoder.getKey("owner"), _owner); @@ -120,14 +120,14 @@ contract ModuleRegistry is IModuleRegistry, EternalStorage { */ function useModule(address _moduleFactory) external { // This if statement is required to be able to add modules from the token proxy contract during deployment - if (ISecurityTokenRegistry(getAddress(Encoder.getKey("securityTokenRegistry"))).isSecurityToken(msg.sender)) { - if (IFeatureRegistry(getAddress(Encoder.getKey("featureRegistry"))).getFeatureStatus("customModulesAllowed")) { + if (ISecurityTokenRegistry(getAddressValue(Encoder.getKey("securityTokenRegistry"))).isSecurityToken(msg.sender)) { + if (IFeatureRegistry(getAddressValue(Encoder.getKey("featureRegistry"))).getFeatureStatus("customModulesAllowed")) { require( - getBool(Encoder.getKey("verified", _moduleFactory)) || IOwnable(_moduleFactory).owner() == IOwnable(msg.sender).owner(), + getBoolValue(Encoder.getKey("verified", _moduleFactory)) || IOwnable(_moduleFactory).owner() == IOwnable(msg.sender).owner(), "ModuleFactory must be verified or SecurityToken owner must be ModuleFactory owner" ); } else { - require(getBool(Encoder.getKey("verified", _moduleFactory)), "ModuleFactory must be verified"); + require(getBoolValue(Encoder.getKey("verified", _moduleFactory)), "ModuleFactory must be verified"); } require(_isCompatibleModule(_moduleFactory, msg.sender), "Version should within the compatible range of ST"); pushArray(Encoder.getKey("reputation", _moduleFactory), msg.sender); @@ -149,7 +149,7 @@ contract ModuleRegistry is IModuleRegistry, EternalStorage { * @param _moduleFactory is the address of the module factory to be registered */ function registerModule(address _moduleFactory) external whenNotPausedOrOwner { - if (IFeatureRegistry(getAddress(Encoder.getKey("featureRegistry"))).getFeatureStatus("customModulesAllowed")) { + if (IFeatureRegistry(getAddressValue(Encoder.getKey("featureRegistry"))).getFeatureStatus("customModulesAllowed")) { require( msg.sender == IOwnable(_moduleFactory).owner() || msg.sender == owner(), "msg.sender must be the Module Factory owner or registry curator" @@ -157,7 +157,7 @@ contract ModuleRegistry is IModuleRegistry, EternalStorage { } else { require(msg.sender == owner(), "Only owner allowed to register modules"); } - require(getUint(Encoder.getKey("registry", _moduleFactory)) == 0, "Module factory should not be pre-registered"); + require(getUintValue(Encoder.getKey("registry", _moduleFactory)) == 0, "Module factory should not be pre-registered"); IModuleFactory moduleFactory = IModuleFactory(_moduleFactory); //Enforce type uniqueness uint256 i; @@ -185,14 +185,14 @@ contract ModuleRegistry is IModuleRegistry, EternalStorage { * @param _moduleFactory is the address of the module factory to be deleted from the registry */ function removeModule(address _moduleFactory) external whenNotPausedOrOwner { - uint256 moduleType = getUint(Encoder.getKey("registry", _moduleFactory)); + uint256 moduleType = getUintValue(Encoder.getKey("registry", _moduleFactory)); require(moduleType != 0, "Module factory should be registered"); require( msg.sender == IOwnable(_moduleFactory).owner() || msg.sender == owner(), "msg.sender must be the Module Factory owner or registry curator" ); - uint256 index = getUint(Encoder.getKey("moduleListIndex", _moduleFactory)); + uint256 index = getUintValue(Encoder.getKey("moduleListIndex", _moduleFactory)); uint256 last = getArrayAddress(Encoder.getKey("moduleList", moduleType)).length - 1; address temp = getArrayAddress(Encoder.getKey("moduleList", moduleType))[last]; @@ -224,7 +224,7 @@ contract ModuleRegistry is IModuleRegistry, EternalStorage { * @return bool */ function verifyModule(address _moduleFactory, bool _verified) external onlyOwner { - require(getUint(Encoder.getKey("registry", _moduleFactory)) != uint256(0), "Module factory must be registered"); + require(getUintValue(Encoder.getKey("registry", _moduleFactory)) != uint256(0), "Module factory must be registered"); set(Encoder.getKey("verified", _moduleFactory), _verified); emit ModuleVerified(_moduleFactory, _verified); } @@ -307,16 +307,16 @@ contract ModuleRegistry is IModuleRegistry, EternalStorage { function getModulesByTypeAndToken(uint8 _moduleType, address _securityToken) public view returns(address[] memory) { uint256 _len = getArrayAddress(Encoder.getKey("moduleList", uint256(_moduleType))).length; address[] memory _addressList = getArrayAddress(Encoder.getKey("moduleList", uint256(_moduleType))); - bool _isCustomModuleAllowed = IFeatureRegistry(getAddress(Encoder.getKey("featureRegistry"))).getFeatureStatus( + bool _isCustomModuleAllowed = IFeatureRegistry(getAddressValue(Encoder.getKey("featureRegistry"))).getFeatureStatus( "customModulesAllowed" ); uint256 counter = 0; for (uint256 i = 0; i < _len; i++) { if (_isCustomModuleAllowed) { - if (IOwnable(_addressList[i]).owner() == IOwnable(_securityToken).owner() || getBool( + if (IOwnable(_addressList[i]).owner() == IOwnable(_securityToken).owner() || getBoolValue( Encoder.getKey("verified", _addressList[i]) )) if (_isCompatibleModule(_addressList[i], _securityToken)) counter++; - } else if (getBool(Encoder.getKey("verified", _addressList[i]))) { + } else if (getBoolValue(Encoder.getKey("verified", _addressList[i]))) { if (_isCompatibleModule(_addressList[i], _securityToken)) counter++; } } @@ -324,7 +324,7 @@ contract ModuleRegistry is IModuleRegistry, EternalStorage { counter = 0; for (uint256 j = 0; j < _len; j++) { if (_isCustomModuleAllowed) { - if (IOwnable(_addressList[j]).owner() == IOwnable(_securityToken).owner() || getBool( + if (IOwnable(_addressList[j]).owner() == IOwnable(_securityToken).owner() || getBoolValue( Encoder.getKey("verified", _addressList[j]) )) { if (_isCompatibleModule(_addressList[j], _securityToken)) { @@ -332,7 +332,7 @@ contract ModuleRegistry is IModuleRegistry, EternalStorage { counter++; } } - } else if (getBool(Encoder.getKey("verified", _addressList[j]))) { + } else if (getBoolValue(Encoder.getKey("verified", _addressList[j]))) { if (_isCompatibleModule(_addressList[j], _securityToken)) { _tempArray[counter] = _addressList[j]; counter++; @@ -375,7 +375,7 @@ contract ModuleRegistry is IModuleRegistry, EternalStorage { * @notice Stores the contract addresses of other key contracts from the PolymathRegistry */ function updateFromRegistry() external onlyOwner { - address _polymathRegistry = getAddress(Encoder.getKey("polymathRegistry")); + address _polymathRegistry = getAddressValue(Encoder.getKey("polymathRegistry")); set(Encoder.getKey("securityTokenRegistry"), IPolymathRegistry(_polymathRegistry).getAddress("SecurityTokenRegistry")); set(Encoder.getKey("featureRegistry"), IPolymathRegistry(_polymathRegistry).getAddress("FeatureRegistry")); set(Encoder.getKey("polyToken"), IPolymathRegistry(_polymathRegistry).getAddress("PolyToken")); @@ -396,7 +396,7 @@ contract ModuleRegistry is IModuleRegistry, EternalStorage { * @return address owner */ function owner() public view returns(address) { - return getAddress(Encoder.getKey("owner")); + return getAddressValue(Encoder.getKey("owner")); } /** @@ -404,6 +404,6 @@ contract ModuleRegistry is IModuleRegistry, EternalStorage { * @return bool */ function isPaused() public view returns(bool) { - return getBool(Encoder.getKey("paused")); + return getBoolValue(Encoder.getKey("paused")); } } diff --git a/contracts/STRGetter.sol b/contracts/STRGetter.sol index 31df84bdb..29d5264fd 100644 --- a/contracts/STRGetter.sol +++ b/contracts/STRGetter.sol @@ -24,7 +24,7 @@ contract STRGetter is EternalStorage { for (i = 0; i < tickers.length; i++) { string memory ticker = Util.bytes32ToString(tickers[i]); /*solium-disable-next-line security/no-block-members*/ - if (getUint(Encoder.getKey("registeredTickers_expiryDate", ticker)) >= now || getTickerStatus(ticker)) { + if (getUintValue(Encoder.getKey("registeredTickers_expiryDate", ticker)) >= now || getTickerStatus(ticker)) { counter ++; } } @@ -33,7 +33,7 @@ contract STRGetter is EternalStorage { for (i = 0; i < tickers.length; i++) { string memory ticker = Util.bytes32ToString(tickers[i]); /*solium-disable-next-line security/no-block-members*/ - if (getUint(Encoder.getKey("registeredTickers_expiryDate", ticker)) >= now || getTickerStatus(ticker)) { + if (getUintValue(Encoder.getKey("registeredTickers_expiryDate", ticker)) >= now || getTickerStatus(ticker)) { tempList[counter] = tickers[i]; counter ++; } @@ -74,7 +74,7 @@ contract STRGetter is EternalStorage { for (i = 0; i < activeUsers.length; i++) { tickers = getArrayBytes32(Encoder.getKey("userToTickers", activeUsers[i])); for (j = 0; j < tickers.length; j++) { - token = getAddress(Encoder.getKey("tickerToSecurityToken", Util.bytes32ToString(tickers[j]))); + token = getAddressValue(Encoder.getKey("tickerToSecurityToken", Util.bytes32ToString(tickers[j]))); if (token != address(0)) { if (_allTokens || IOwnable(token).owner() == _owner) { count = count + 1; @@ -87,7 +87,7 @@ contract STRGetter is EternalStorage { for (i = 0; i < activeUsers.length; i++) { tickers = getArrayBytes32(Encoder.getKey("userToTickers", activeUsers[i])); for (j = 0; j < tickers.length; j++) { - token = getAddress(Encoder.getKey("tickerToSecurityToken", Util.bytes32ToString(tickers[j]))); + token = getAddressValue(Encoder.getKey("tickerToSecurityToken", Util.bytes32ToString(tickers[j]))); if (token != address(0)) { if (_allTokens || IOwnable(token).owner() == _owner) { result[index] = token; @@ -111,15 +111,15 @@ contract STRGetter is EternalStorage { function getTickerDetails(string calldata _ticker) external view returns (address, uint256, uint256, string memory, bool) { string memory ticker = Util.upper(_ticker); bool tickerStatus = getTickerStatus(ticker); - uint256 expiryDate = getUint(Encoder.getKey("registeredTickers_expiryDate", ticker)); + uint256 expiryDate = getUintValue(Encoder.getKey("registeredTickers_expiryDate", ticker)); /*solium-disable-next-line security/no-block-members*/ if ((tickerStatus == true) || (expiryDate > now)) { return ( getTickerOwner(ticker), - getUint(Encoder.getKey("registeredTickers_registrationDate", ticker)), + getUintValue(Encoder.getKey("registeredTickers_registrationDate", ticker)), expiryDate, - getString(Encoder.getKey("registeredTickers_tokenName", ticker)), + getStringValue(Encoder.getKey("registeredTickers_tokenName", ticker)), tickerStatus ); } else { @@ -134,7 +134,7 @@ contract STRGetter is EternalStorage { */ function getSecurityTokenAddress(string calldata _ticker) external view returns (address) { string memory ticker = Util.upper(_ticker); - return getAddress(Encoder.getKey("tickerToSecurityToken", ticker)); + return getAddressValue(Encoder.getKey("tickerToSecurityToken", ticker)); } /** @@ -147,10 +147,10 @@ contract STRGetter is EternalStorage { */ function getSecurityTokenData(address _securityToken) external view returns (string memory, address, string memory, uint256) { return ( - getString(Encoder.getKey("securityTokens_ticker", _securityToken)), + getStringValue(Encoder.getKey("securityTokens_ticker", _securityToken)), IOwnable(_securityToken).owner(), - getString(Encoder.getKey("securityTokens_tokenDetails", _securityToken)), - getUint(Encoder.getKey("securityTokens_deployedAt", _securityToken)) + getStringValue(Encoder.getKey("securityTokens_tokenDetails", _securityToken)), + getUintValue(Encoder.getKey("securityTokens_deployedAt", _securityToken)) ); } @@ -158,14 +158,14 @@ contract STRGetter is EternalStorage { * @notice Returns the current STFactory Address */ function getSTFactoryAddress() public view returns(address) { - return getAddress(Encoder.getKey("protocolVersionST", getUint(Encoder.getKey("latestVersion")))); + return getAddressValue(Encoder.getKey("protocolVersionST", getUintValue(Encoder.getKey("latestVersion")))); } /** * @notice Gets Protocol version */ function getProtocolVersion() public view returns(uint8[] memory) { - return VersionUtils.unpack(uint24(getUint(Encoder.getKey("latestVersion")))); + return VersionUtils.unpack(uint24(getUintValue(Encoder.getKey("latestVersion")))); } /** @@ -173,7 +173,7 @@ contract STRGetter is EternalStorage { * @return Fee amount */ function getSecurityTokenLaunchFee() public view returns(uint256) { - return getUint(STLAUNCHFEE); + return getUintValue(STLAUNCHFEE); } /** @@ -181,7 +181,7 @@ contract STRGetter is EternalStorage { * @return Fee amount */ function getTickerRegistrationFee() public view returns(uint256) { - return getUint(TICKERREGFEE); + return getUintValue(TICKERREGFEE); } /** @@ -189,16 +189,16 @@ contract STRGetter is EternalStorage { * @return Expiry limit */ function getExpiryLimit() public view returns(uint256) { - return getUint(EXPIRYLIMIT); + return getUintValue(EXPIRYLIMIT); } /** * @notice Gets the status of the ticker * @param _ticker Ticker whose status need to determine - * @return bool + * @return bool */ function getTickerStatus(string memory _ticker) public view returns(bool) { - return getBool(Encoder.getKey("registeredTickers_status", _ticker)); + return getBoolValue(Encoder.getKey("registeredTickers_status", _ticker)); } /** @@ -207,7 +207,7 @@ contract STRGetter is EternalStorage { * @return address Address of the owner */ function getTickerOwner(string memory _ticker) public view returns(address) { - return getAddress(Encoder.getKey("registeredTickers_owner", _ticker)); + return getAddressValue(Encoder.getKey("registeredTickers_owner", _ticker)); } -} \ No newline at end of file +} diff --git a/contracts/SecurityTokenRegistry.sol b/contracts/SecurityTokenRegistry.sol index f8f6e010b..60c085e72 100644 --- a/contracts/SecurityTokenRegistry.sol +++ b/contracts/SecurityTokenRegistry.sol @@ -175,7 +175,7 @@ contract SecurityTokenRegistry is EternalStorage, Proxy { external payable { - require(!getBool(INITIALIZE),"already initialized"); + require(!getBoolValue(INITIALIZE),"already initialized"); require( _STFactory != address(0) && _owner != address(0) && _polymathRegistry != address(0) && _getterContract != address(0), "Invalid address" @@ -201,13 +201,13 @@ contract SecurityTokenRegistry is EternalStorage, Proxy { } function _updateFromRegistry() internal { - address polymathRegistry = getAddress(POLYMATHREGISTRY); + address polymathRegistry = getAddressValue(POLYMATHREGISTRY); set(POLYTOKEN, IPolymathRegistry(polymathRegistry).getAddress("PolyToken")); } /** * @notice Set the getter contract address - * @param _getterContract Address of the contract + * @param _getterContract Address of the contract */ function setGetterRegistry(address _getterContract) public onlyOwner { require(_getterContract != address(0)); @@ -215,7 +215,7 @@ contract SecurityTokenRegistry is EternalStorage, Proxy { } function _implementation() internal view returns(address) { - return getAddress(Encoder.getKey("STRGetter")); + return getAddressValue(Encoder.getKey("STRGetter")); } ///////////////////////////// @@ -234,9 +234,9 @@ contract SecurityTokenRegistry is EternalStorage, Proxy { require(_owner != address(0), "Owner should not be 0x"); require(bytes(_ticker).length > 0 && bytes(_ticker).length <= 10, "Ticker length range (0,10]"); // Attempt to charge the reg fee if it is > 0 POLY - uint256 tickerFee = getUint(TICKERREGFEE); + uint256 tickerFee = getUintValue(TICKERREGFEE); if (tickerFee > 0) - require(IERC20(getAddress(POLYTOKEN)).transferFrom(msg.sender, address(this), tickerFee), "Insufficent allowance"); + require(IERC20(getAddressValue(POLYTOKEN)).transferFrom(msg.sender, address(this), tickerFee), "Insufficent allowance"); string memory ticker = Util.upper(_ticker); require(_tickerAvailable(ticker), "Ticker is reserved"); // Check whether ticker was previously registered (and expired) @@ -245,7 +245,7 @@ contract SecurityTokenRegistry is EternalStorage, Proxy { _deleteTickerOwnership(previousOwner, ticker); } /*solium-disable-next-line security/no-block-members*/ - _addTicker(_owner, ticker, _tokenName, now, now.add(getUint(EXPIRYLIMIT)), false, false, tickerFee); + _addTicker(_owner, ticker, _tokenName, now, now.add(getUintValue(EXPIRYLIMIT)), false, false, tickerFee); } /** @@ -260,7 +260,7 @@ contract SecurityTokenRegistry is EternalStorage, Proxy { bool _status, bool _fromAdmin, uint256 _fee - ) + ) internal { _setTickerOwnership(_owner, _ticker); @@ -285,7 +285,7 @@ contract SecurityTokenRegistry is EternalStorage, Proxy { uint256 _registrationDate, uint256 _expiryDate, bool _status - ) + ) external onlyOwner { @@ -307,7 +307,7 @@ contract SecurityTokenRegistry is EternalStorage, Proxy { uint256 _registrationDate, uint256 _expiryDate, bool _status - ) + ) internal { address currentOwner = _tickerOwner(_ticker); @@ -319,13 +319,13 @@ contract SecurityTokenRegistry is EternalStorage, Proxy { } // If status is true, there must be a security token linked to the ticker already if (_status) { - require(getAddress(Encoder.getKey("tickerToSecurityToken", _ticker)) != address(0), "Token not registered"); + require(getAddressValue(Encoder.getKey("tickerToSecurityToken", _ticker)) != address(0), "Token not registered"); } _addTicker(_owner, _ticker, _tokenName, _registrationDate, _expiryDate, _status, true, uint256(0)); } function _tickerOwner(string memory _ticker) internal view returns(address) { - return getAddress(Encoder.getKey("registeredTickers_owner", _ticker)); + return getAddressValue(Encoder.getKey("registeredTickers_owner", _ticker)); } /** @@ -351,7 +351,7 @@ contract SecurityTokenRegistry is EternalStorage, Proxy { function _tickerAvailable(string memory _ticker) internal view returns(bool) { if (_tickerOwner(_ticker) != address(0)) { /*solium-disable-next-line security/no-block-members*/ - if ((now > getUint(Encoder.getKey("registeredTickers_expiryDate", _ticker))) && !_tickerStatus(_ticker)) { + if ((now > getUintValue(Encoder.getKey("registeredTickers_expiryDate", _ticker))) && !_tickerStatus(_ticker)) { return true; } else return false; } @@ -359,7 +359,7 @@ contract SecurityTokenRegistry is EternalStorage, Proxy { } function _tickerStatus(string memory _ticker) internal view returns(bool) { - return getBool(Encoder.getKey("registeredTickers_status", _ticker)); + return getBoolValue(Encoder.getKey("registeredTickers_status", _ticker)); } /** @@ -373,7 +373,7 @@ contract SecurityTokenRegistry is EternalStorage, Proxy { pushArray(_ownerKey, Util.stringToBytes32(_ticker)); set(Encoder.getKey("tickerIndex", _ticker), length); bytes32 seenKey = Encoder.getKey("seenUsers", _owner); - if (!getBool(seenKey)) { + if (!getBoolValue(seenKey)) { pushArray(Encoder.getKey("activeUsers"), _owner); set(seenKey, true); } @@ -389,19 +389,19 @@ contract SecurityTokenRegistry is EternalStorage, Proxy { uint256 _expiryDate, string memory _tokenName, bool _status - ) + ) internal { bytes32 key = Encoder.getKey("registeredTickers_owner", _ticker); - if (getAddress(key) != _owner) set(key, _owner); + if (getAddressValue(key) != _owner) set(key, _owner); key = Encoder.getKey("registeredTickers_registrationDate", _ticker); - if (getUint(key) != _registrationDate) set(key, _registrationDate); + if (getUintValue(key) != _registrationDate) set(key, _registrationDate); key = Encoder.getKey("registeredTickers_expiryDate", _ticker); - if (getUint(key) != _expiryDate) set(key, _expiryDate); + if (getUintValue(key) != _expiryDate) set(key, _expiryDate); key = Encoder.getKey("registeredTickers_tokenName", _ticker); - if (Encoder.getKey(getString(key)) != Encoder.getKey(_tokenName)) set(key, _tokenName); + if (Encoder.getKey(getStringValue(key)) != Encoder.getKey(_tokenName)) set(key, _tokenName); key = Encoder.getKey("registeredTickers_status", _ticker); - if (getBool(key) != _status) set(key, _status); + if (getBoolValue(key) != _status) set(key, _status); } /** @@ -413,9 +413,9 @@ contract SecurityTokenRegistry is EternalStorage, Proxy { string memory ticker = Util.upper(_ticker); require(_newOwner != address(0), "Invalid address"); bytes32 ownerKey = Encoder.getKey("registeredTickers_owner", ticker); - require(getAddress(ownerKey) == msg.sender, "Not authorised"); + require(getAddressValue(ownerKey) == msg.sender, "Not authorised"); if (_tickerStatus(ticker)) require( - IOwnable(getAddress(Encoder.getKey("tickerToSecurityToken", ticker))).owner() == _newOwner, + IOwnable(getAddressValue(Encoder.getKey("tickerToSecurityToken", ticker))).owner() == _newOwner, "New owner does not match token owner" ); _deleteTickerOwnership(msg.sender, ticker); @@ -428,7 +428,7 @@ contract SecurityTokenRegistry is EternalStorage, Proxy { * @notice Internal - Removes the owner of a ticker */ function _deleteTickerOwnership(address _owner, string memory _ticker) internal { - uint256 index = uint256(getUint(Encoder.getKey("tickerIndex", _ticker))); + uint256 index = uint256(getUintValue(Encoder.getKey("tickerIndex", _ticker))); bytes32 ownerKey = Encoder.getKey("userToTickers", _owner); bytes32[] memory tickers = getArrayBytes32(ownerKey); assert(index < tickers.length); @@ -446,7 +446,7 @@ contract SecurityTokenRegistry is EternalStorage, Proxy { */ function changeExpiryLimit(uint256 _newExpiry) external onlyOwner { require(_newExpiry >= 1 days, "Expiry should >= 1 day"); - emit ChangeExpiryLimit(getUint(EXPIRYLIMIT), _newExpiry); + emit ChangeExpiryLimit(getUintValue(EXPIRYLIMIT), _newExpiry); set(EXPIRYLIMIT, _newExpiry); } @@ -466,31 +466,31 @@ contract SecurityTokenRegistry is EternalStorage, Proxy { string calldata _ticker, string calldata _tokenDetails, bool _divisible - ) - external - whenNotPausedOrOwner + ) + external + whenNotPausedOrOwner { require(bytes(_name).length > 0 && bytes(_ticker).length > 0, "Ticker length > 0"); string memory ticker = Util.upper(_ticker); bytes32 statusKey = Encoder.getKey("registeredTickers_status", ticker); - require(!getBool(statusKey), "Already deployed"); + require(!getBoolValue(statusKey), "Already deployed"); set(statusKey, true); require(_tickerOwner(ticker) == msg.sender, "Not authorised"); /*solium-disable-next-line security/no-block-members*/ - require(getUint(Encoder.getKey("registeredTickers_expiryDate", ticker)) >= now, "Ticker gets expired"); + require(getUintValue(Encoder.getKey("registeredTickers_expiryDate", ticker)) >= now, "Ticker gets expired"); - uint256 launchFee = getUint(STLAUNCHFEE); + uint256 launchFee = getUintValue(STLAUNCHFEE); if (launchFee > 0) - require(IERC20(getAddress(POLYTOKEN)).transferFrom(msg.sender, address(this), launchFee), "Insufficient allowance"); + require(IERC20(getAddressValue(POLYTOKEN)).transferFrom(msg.sender, address(this), launchFee), "Insufficient allowance"); - address newSecurityTokenAddress = ISTFactory(getAddress(Encoder.getKey("protocolVersionST", getUint(Encoder.getKey("latestVersion"))))).deployToken( + address newSecurityTokenAddress = ISTFactory(getAddressValue(Encoder.getKey("protocolVersionST", getUintValue(Encoder.getKey("latestVersion"))))).deployToken( _name, ticker, 18, _tokenDetails, msg.sender, _divisible, - getAddress(POLYMATHREGISTRY) + getAddressValue(POLYMATHREGISTRY) ); /*solium-disable-next-line security/no-block-members*/ @@ -516,26 +516,26 @@ contract SecurityTokenRegistry is EternalStorage, Proxy { address _securityToken, string calldata _tokenDetails, uint256 _deployedAt - ) - external - onlyOwner + ) + external + onlyOwner { require(bytes(_name).length > 0 && bytes(_ticker).length > 0, "String length > 0"); require(bytes(_ticker).length <= 10, "Ticker length range (0,10]"); require(_deployedAt != 0 && _owner != address(0), "0 value params not allowed"); string memory ticker = Util.upper(_ticker); require(_securityToken != address(0), "ST address is 0x"); - uint256 registrationTime = getUint(Encoder.getKey("registeredTickers_registrationDate", ticker)); - uint256 expiryTime = getUint(Encoder.getKey("registeredTickers_expiryDate", ticker)); + uint256 registrationTime = getUintValue(Encoder.getKey("registeredTickers_registrationDate", ticker)); + uint256 expiryTime = getUintValue(Encoder.getKey("registeredTickers_expiryDate", ticker)); if (registrationTime == 0) { /*solium-disable-next-line security/no-block-members*/ registrationTime = now; - expiryTime = registrationTime.add(getUint(EXPIRYLIMIT)); + expiryTime = registrationTime.add(getUintValue(EXPIRYLIMIT)); } set(Encoder.getKey("tickerToSecurityToken", ticker), _securityToken); _modifyTicker(_owner, ticker, _name, registrationTime, expiryTime, true); _storeSecurityTokenData(_securityToken, ticker, _tokenDetails, _deployedAt); - emit NewSecurityToken(ticker, _name, _securityToken, _owner, _deployedAt, msg.sender, true, getUint(STLAUNCHFEE)); + emit NewSecurityToken(ticker, _name, _securityToken, _owner, _deployedAt, msg.sender, true, getUintValue(STLAUNCHFEE)); } /** @@ -558,7 +558,7 @@ contract SecurityTokenRegistry is EternalStorage, Proxy { * @return bool */ function isSecurityToken(address _securityToken) external view returns(bool) { - return (keccak256(bytes(getString(Encoder.getKey("securityTokens_ticker", _securityToken)))) != keccak256("")); + return (keccak256(bytes(getStringValue(Encoder.getKey("securityTokens_ticker", _securityToken)))) != keccak256("")); } ///////////////////////////// @@ -571,7 +571,7 @@ contract SecurityTokenRegistry is EternalStorage, Proxy { */ function transferOwnership(address _newOwner) external onlyOwner { require(_newOwner != address(0), "Invalid address"); - emit OwnershipTransferred(getAddress(OWNER), _newOwner); + emit OwnershipTransferred(getAddressValue(OWNER), _newOwner); set(OWNER, _newOwner); } @@ -598,7 +598,7 @@ contract SecurityTokenRegistry is EternalStorage, Proxy { * @param _tickerRegFee is the registration fee in POLY tokens (base 18 decimals) */ function changeTickerRegistrationFee(uint256 _tickerRegFee) external onlyOwner { - uint256 fee = getUint(TICKERREGFEE); + uint256 fee = getUintValue(TICKERREGFEE); require(fee != _tickerRegFee, "Fee not changed"); emit ChangeTickerRegistrationFee(fee, _tickerRegFee); set(TICKERREGFEE, _tickerRegFee); @@ -609,7 +609,7 @@ contract SecurityTokenRegistry is EternalStorage, Proxy { * @param _stLaunchFee is the registration fee in POLY tokens (base 18 decimals) */ function changeSecurityLaunchFee(uint256 _stLaunchFee) external onlyOwner { - uint256 fee = getUint(STLAUNCHFEE); + uint256 fee = getUintValue(STLAUNCHFEE); require(fee != _stLaunchFee, "Fee not changed"); emit ChangeSecurityLaunchFee(fee, _stLaunchFee); set(STLAUNCHFEE, _stLaunchFee); @@ -650,11 +650,11 @@ contract SecurityTokenRegistry is EternalStorage, Proxy { _version[2] = _patch; uint24 _packedVersion = VersionUtils.pack(_major, _minor, _patch); require( - VersionUtils.isValidVersion(VersionUtils.unpack(uint24(getUint(Encoder.getKey("latestVersion")))), _version), + VersionUtils.isValidVersion(VersionUtils.unpack(uint24(getUintValue(Encoder.getKey("latestVersion")))), _version), "In-valid version" ); set(Encoder.getKey("latestVersion"), uint256(_packedVersion)); - set(Encoder.getKey("protocolVersionST", getUint(Encoder.getKey("latestVersion"))), _STFactoryAddress); + set(Encoder.getKey("protocolVersionST", getUintValue(Encoder.getKey("latestVersion"))), _STFactoryAddress); } /** @@ -671,7 +671,7 @@ contract SecurityTokenRegistry is EternalStorage, Proxy { * @return bool */ function isPaused() public view returns(bool) { - return getBool(PAUSED); + return getBoolValue(PAUSED); } /** @@ -679,7 +679,7 @@ contract SecurityTokenRegistry is EternalStorage, Proxy { * @return address owner */ function owner() public view returns(address) { - return getAddress(OWNER); + return getAddressValue(OWNER); } } diff --git a/contracts/interfaces/IBoot.sol b/contracts/interfaces/IBoot.sol index ecade0f31..9e295fb2f 100644 --- a/contracts/interfaces/IBoot.sol +++ b/contracts/interfaces/IBoot.sol @@ -6,4 +6,4 @@ interface IBoot { * @return bytes4 Configure function signature */ function getInitFunction() external pure returns(bytes4); -} +} \ No newline at end of file diff --git a/contracts/interfaces/ICheckPermission.sol b/contracts/interfaces/ICheckPermission.sol new file mode 100644 index 000000000..2e14fa5b9 --- /dev/null +++ b/contracts/interfaces/ICheckPermission.sol @@ -0,0 +1,14 @@ +pragma solidity ^0.5.0; + +interface ICheckPermission { + /** + * @notice Validate permissions with PermissionManager if it exists, If no Permission return false + * @dev Note that IModule withPerm will allow ST owner all permissions anyway + * @dev this allows individual modules to override this logic if needed (to not allow ST owner all permissions) + * @param _delegate address of delegate + * @param _module address of PermissionManager module + * @param _perm the permissions + * @return success + */ + function checkPermission(address _delegate, address _module, bytes32 _perm) external view returns(bool); +} diff --git a/contracts/libraries/BokkyPooBahsDateTimeLibrary.sol b/contracts/libraries/BokkyPooBahsDateTimeLibrary.sol index fdfd3df2e..0e2f71d93 100644 --- a/contracts/libraries/BokkyPooBahsDateTimeLibrary.sol +++ b/contracts/libraries/BokkyPooBahsDateTimeLibrary.sol @@ -55,7 +55,7 @@ library BokkyPooBahsDateTimeLibrary { // - 3 * ((year + 4900 + (month - 14) / 12) / 100) / 4 // - offset // ------------------------------------------------------------------------ - function _daysFromDate(uint year, uint month, uint day) internal pure returns(uint _days) { + function _daysFromDate(uint year, uint month, uint day) internal pure returns (uint _days) { require(year >= 1970); int _year = int(year); int _month = int(month); @@ -83,7 +83,7 @@ library BokkyPooBahsDateTimeLibrary { // month = month + 2 - 12 * L // year = 100 * (N - 49) + year + L // ------------------------------------------------------------------------ - function _daysToDate(uint _days) internal pure returns(uint year, uint month, uint day) { + function _daysToDate(uint _days) internal pure returns (uint year, uint month, uint day) { int __days = int(_days); int L = __days + 68569 + OFFSET19700101; @@ -120,7 +120,7 @@ library BokkyPooBahsDateTimeLibrary { uint hour, uint minute, uint second - ) + ) { (year, month, day) = _daysToDate(timestamp / SECONDS_PER_DAY); uint secs = timestamp % SECONDS_PER_DAY; @@ -130,7 +130,7 @@ library BokkyPooBahsDateTimeLibrary { second = secs % SECONDS_PER_MINUTE; } - function isValidDate(uint year, uint month, uint day) internal pure returns(bool valid) { + function isValidDate(uint year, uint month, uint day) internal pure returns (bool valid) { if (year >= 1970 && month > 0 && month <= 12) { uint daysInMonth = _getDaysInMonth(year, month); if (day > 0 && day <= daysInMonth) { @@ -138,14 +138,14 @@ library BokkyPooBahsDateTimeLibrary { } } } - function isValidDateTime(uint year, uint month, uint day, uint hour, uint minute, uint second) internal pure returns(bool valid) { + function isValidDateTime(uint year, uint month, uint day, uint hour, uint minute, uint second) internal pure returns (bool valid) { if (isValidDate(year, month, day)) { if (hour < 24 && minute < 60 && second < 60) { valid = true; } } } - function isLeapYear(uint timestamp) internal pure returns(bool leapYear) { + function isLeapYear(uint timestamp) internal pure returns (bool leapYear) { uint year; uint month; uint day; @@ -168,7 +168,7 @@ library BokkyPooBahsDateTimeLibrary { (year, month, day) = _daysToDate(timestamp / SECONDS_PER_DAY); daysInMonth = _getDaysInMonth(year, month); } - function _getDaysInMonth(uint year, uint month) internal pure returns(uint daysInMonth) { + function _getDaysInMonth(uint year, uint month) internal pure returns (uint daysInMonth) { if (month == 1 || month == 3 || month == 5 || month == 7 || month == 8 || month == 10 || month == 12) { daysInMonth = 31; } else if (month != 2) { @@ -178,22 +178,22 @@ library BokkyPooBahsDateTimeLibrary { } } // 1 = Monday, 7 = Sunday - function getDayOfWeek(uint timestamp) internal pure returns(uint dayOfWeek) { + function getDayOfWeek(uint timestamp) internal pure returns (uint dayOfWeek) { uint _days = timestamp / SECONDS_PER_DAY; dayOfWeek = (_days + 3) % 7 + 1; } - function getYear(uint timestamp) internal pure returns(uint year) { + function getYear(uint timestamp) internal pure returns (uint year) { uint month; uint day; (year, month, day) = _daysToDate(timestamp / SECONDS_PER_DAY); } - function getMonth(uint timestamp) internal pure returns(uint month) { + function getMonth(uint timestamp) internal pure returns (uint month) { uint year; uint day; (year, month, day) = _daysToDate(timestamp / SECONDS_PER_DAY); } - function getDay(uint timestamp) internal pure returns(uint day) { + function getDay(uint timestamp) internal pure returns (uint day) { uint year; uint month; (year, month, day) = _daysToDate(timestamp / SECONDS_PER_DAY); @@ -260,7 +260,7 @@ library BokkyPooBahsDateTimeLibrary { require(newTimestamp >= timestamp); } - function subYears(uint timestamp, uint _years) internal pure returns(uint newTimestamp) { + function subYears(uint timestamp, uint _years) internal pure returns (uint newTimestamp) { uint year; uint month; uint day; @@ -294,23 +294,20 @@ library BokkyPooBahsDateTimeLibrary { newTimestamp = timestamp - _days * SECONDS_PER_DAY; require(newTimestamp <= timestamp); } - - function subHours(uint timestamp, uint _hours) internal pure returns(uint newTimestamp) { + function subHours(uint timestamp, uint _hours) internal pure returns (uint newTimestamp) { newTimestamp = timestamp - _hours * SECONDS_PER_HOUR; require(newTimestamp <= timestamp); } - - function subMinutes(uint timestamp, uint _minutes) internal pure returns(uint newTimestamp) { + function subMinutes(uint timestamp, uint _minutes) internal pure returns (uint newTimestamp) { newTimestamp = timestamp - _minutes * SECONDS_PER_MINUTE; require(newTimestamp <= timestamp); } - function subSeconds(uint timestamp, uint _seconds) internal pure returns(uint newTimestamp) { newTimestamp = timestamp - _seconds; require(newTimestamp <= timestamp); } - function diffYears(uint fromTimestamp, uint toTimestamp) internal pure returns(uint _years) { + function diffYears(uint fromTimestamp, uint toTimestamp) internal pure returns (uint _years) { require(fromTimestamp <= toTimestamp); uint fromYear; uint fromMonth; @@ -350,7 +347,7 @@ library BokkyPooBahsDateTimeLibrary { require(fromTimestamp <= toTimestamp); _minutes = (toTimestamp - fromTimestamp) / SECONDS_PER_MINUTE; } - + function diffSeconds(uint fromTimestamp, uint toTimestamp) internal pure returns(uint _seconds) { require(fromTimestamp <= toTimestamp); _seconds = toTimestamp - fromTimestamp; diff --git a/contracts/libraries/VolumeRestrictionLib.sol b/contracts/libraries/VolumeRestrictionLib.sol new file mode 100644 index 000000000..3b3a05da2 --- /dev/null +++ b/contracts/libraries/VolumeRestrictionLib.sol @@ -0,0 +1,130 @@ +pragma solidity ^0.5.0; + +import "openzeppelin-solidity/contracts/math/SafeMath.sol"; +import "../storage/VolumeRestrictionTMStorage.sol"; + +library VolumeRestrictionLib { + + using SafeMath for uint256; + + function _checkLengthOfArray( + address[] memory _holders, + uint256[] memory _allowedTokens, + uint256[] memory _startTimes, + uint256[] memory _rollingPeriodInDays, + uint256[] memory _endTimes, + uint256[] memory _restrictionTypes + ) + internal + pure + { + require( + _holders.length == _allowedTokens.length && + _allowedTokens.length == _startTimes.length && + _startTimes.length == _rollingPeriodInDays.length && + _rollingPeriodInDays.length == _endTimes.length && + _endTimes.length == _restrictionTypes.length, + "Length mismatch" + ); + } + + function deleteHolderFromList(VolumeRestrictionTMStorage.RestrictedData storage data, address _holder, uint8 _typeOfPeriod) public { + // Deleting the holder if holder's type of Period is `Both` type otherwise + // it will assign the given type `_typeOfPeriod` to the _holder typeOfPeriod + // `_typeOfPeriod` it always be contrary to the removing restriction + // if removing restriction is individual then typeOfPeriod is TypeOfPeriod.OneDay + // in uint8 its value is 1. if removing restriction is daily individual then typeOfPeriod + // is TypeOfPeriod.MultipleDays in uint8 its value is 0. + if (data.restrictedHolders[_holder].typeOfPeriod != uint8(VolumeRestrictionTMStorage.TypeOfPeriod.Both)) { + uint128 index = data.restrictedHolders[_holder].index; + uint256 _len = data.restrictedAddresses.length; + if (index != _len) { + data.restrictedHolders[data.restrictedAddresses[_len - 1]].index = index; + data.restrictedAddresses[index - 1] = data.restrictedAddresses[_len - 1]; + } + delete data.restrictedHolders[_holder]; + data.restrictedAddresses.length--; + } else { + data.restrictedHolders[_holder].typeOfPeriod = _typeOfPeriod; + } + } + + function addRestrictionData(VolumeRestrictionTMStorage.RestrictedData storage data, address _holder, uint8 _callFrom, uint256 _endTime) public { + uint128 index = data.restrictedHolders[_holder].index; + if (data.restrictedHolders[_holder].seen == 0) { + data.restrictedAddresses.push(_holder); + index = uint128(data.restrictedAddresses.length); + } + uint8 _type = _getTypeOfPeriod(data.restrictedHolders[_holder].typeOfPeriod, _callFrom, _endTime); + data.restrictedHolders[_holder] = VolumeRestrictionTMStorage.RestrictedHolder(uint8(1), _type, index); + } + + function _getTypeOfPeriod(uint8 _currentTypeOfPeriod, uint8 _callFrom, uint256 _endTime) internal pure returns(uint8) { + if (_currentTypeOfPeriod != _callFrom && _endTime != uint256(0)) + return uint8(VolumeRestrictionTMStorage.TypeOfPeriod.Both); + else + return _callFrom; + } + + function getRestrictionData( + VolumeRestrictionTMStorage.RestrictedData storage _holderData, + VolumeRestrictionTMStorage.IndividualRestrictions storage _individualRestrictions + ) public view returns( + address[] memory allAddresses, + uint256[] memory allowedTokens, + uint256[] memory startTime, + uint256[] memory rollingPeriodInDays, + uint256[] memory endTime, + uint8[] memory typeOfRestriction + ) + { + uint256 counter = 0; + uint256 i = 0; + for (i = 0; i < _holderData.restrictedAddresses.length; i++) { + counter = counter + (_holderData.restrictedHolders[_holderData.restrictedAddresses[i]].typeOfPeriod == uint8(2) ? 2 : 1); + } + allAddresses = new address[](counter); + allowedTokens = new uint256[](counter); + startTime = new uint256[](counter); + rollingPeriodInDays = new uint256[](counter); + endTime = new uint256[](counter); + typeOfRestriction = new uint8[](counter); + counter = 0; + for (i = 0; i < _holderData.restrictedAddresses.length; i++) { + allAddresses[counter] = _holderData.restrictedAddresses[i]; + if (_holderData.restrictedHolders[_holderData.restrictedAddresses[i]].typeOfPeriod == uint8(VolumeRestrictionTMStorage.TypeOfPeriod.MultipleDays)) { + _setValues(_individualRestrictions.individualRestriction[_holderData.restrictedAddresses[i]], allowedTokens, startTime, rollingPeriodInDays, endTime, typeOfRestriction, counter); + } + else if (_holderData.restrictedHolders[_holderData.restrictedAddresses[i]].typeOfPeriod == uint8(VolumeRestrictionTMStorage.TypeOfPeriod.OneDay)) { + _setValues(_individualRestrictions.individualDailyRestriction[_holderData.restrictedAddresses[i]], allowedTokens, startTime, rollingPeriodInDays, endTime, typeOfRestriction, counter); + } + else if (_holderData.restrictedHolders[_holderData.restrictedAddresses[i]].typeOfPeriod == uint8(VolumeRestrictionTMStorage.TypeOfPeriod.Both)) { + _setValues(_individualRestrictions.individualRestriction[_holderData.restrictedAddresses[i]], allowedTokens, startTime, rollingPeriodInDays, endTime, typeOfRestriction, counter); + counter++; + allAddresses[counter] = _holderData.restrictedAddresses[i]; + _setValues(_individualRestrictions.individualDailyRestriction[_holderData.restrictedAddresses[i]], allowedTokens, startTime, rollingPeriodInDays, endTime, typeOfRestriction, counter); + } + counter++; + } + } + + function _setValues( + VolumeRestrictionTMStorage.VolumeRestriction memory restriction, + uint256[] memory allowedTokens, + uint256[] memory startTime, + uint256[] memory rollingPeriodInDays, + uint256[] memory endTime, + uint8[] memory typeOfRestriction, + uint256 index + ) + internal + pure + { + allowedTokens[index] = restriction.allowedTokens; + startTime[index] = restriction.startTime; + rollingPeriodInDays[index] = restriction.rollingPeriodInDays; + endTime[index] = restriction.endTime; + typeOfRestriction[index] = uint8(restriction.typeOfRestriction); + } + +} diff --git a/contracts/mocks/PolyTokenFaucet.sol b/contracts/mocks/PolyTokenFaucet.sol index c903223f6..4ebd9863f 100644 --- a/contracts/mocks/PolyTokenFaucet.sol +++ b/contracts/mocks/PolyTokenFaucet.sol @@ -17,8 +17,8 @@ contract PolyTokenFaucet { mapping(address => uint256) balances; mapping(address => mapping(address => uint256)) allowed; - event Transfer(address indexed _from, address indexed _to, uint256 _value); - event Approval(address indexed _owner, address indexed _spender, uint256 _value); + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); constructor() public { decimals = 18; diff --git a/contracts/modules/Checkpoint/DividendCheckpoint.sol b/contracts/modules/Checkpoint/DividendCheckpoint.sol index e54aa27fa..92a89dc01 100644 --- a/contracts/modules/Checkpoint/DividendCheckpoint.sol +++ b/contracts/modules/Checkpoint/DividendCheckpoint.sol @@ -1,238 +1,400 @@ -/** - * DISCLAIMER: Under certain conditions, the function pushDividendPayment - * may fail due to block gas limits. - * If the total number of investors that ever held tokens is greater than ~15,000 then - * the function may fail. If this happens investors can pull their dividends, or the Issuer - * can use pushDividendPaymentToAddresses to provide an explict address list in batches - */ -pragma solidity ^0.5.0; - -import "./ICheckpoint.sol"; -import "./DividendCheckpointStorage.sol"; -import "../Module.sol"; -import "../../interfaces/ISecurityToken.sol"; -import "openzeppelin-solidity/contracts/math/SafeMath.sol"; -import "openzeppelin-solidity/contracts/math/Math.sol"; - -/** - * @title Checkpoint module for issuing ether dividends - * @dev abstract contract - */ -contract DividendCheckpoint is DividendCheckpointStorage, ICheckpoint, Module { - using SafeMath for uint256; - - event SetDefaultExcludedAddresses(address[] _excluded, uint256 _timestamp); - event SetWithholding(address[] _investors, uint256[] _withholding, uint256 _timestamp); - event SetWithholdingFixed(address[] _investors, uint256 _withholding, uint256 _timestamp); - - modifier validDividendIndex(uint256 _dividendIndex) { - require(_dividendIndex < dividends.length, "Invalid dividend"); - require(!dividends[_dividendIndex].reclaimed, "Dividend reclaimed"); - /*solium-disable-next-line security/no-block-members*/ - require(now >= dividends[_dividendIndex].maturity, "Dividend maturity in future"); - /*solium-disable-next-line security/no-block-members*/ - require(now < dividends[_dividendIndex].expiry, "Dividend expiry in past"); - _; - } - - /** - * @notice Init function i.e generalise function to maintain the structure of the module contract - * @return bytes4 - */ - function getInitFunction() public pure returns(bytes4) { - return bytes4(0); - } - - /** - * @notice Return the default excluded addresses - * @return List of excluded addresses - */ - function getDefaultExcluded() external view returns(address[] memory) { - return excluded; - } - - /** - * @notice Creates a checkpoint on the security token - * @return Checkpoint ID - */ - function createCheckpoint() public withPerm(CHECKPOINT) returns(uint256) { - return ISecurityToken(securityToken).createCheckpoint(); - } - - /** - * @notice Function to clear and set list of excluded addresses used for future dividends - * @param _excluded Addresses of investors - */ - function setDefaultExcluded(address[] memory _excluded) public withPerm(MANAGE) { - require(_excluded.length <= EXCLUDED_ADDRESS_LIMIT, "Too many excluded addresses"); - for (uint256 j = 0; j < _excluded.length; j++) { - require(_excluded[j] != address(0), "Invalid address"); - for (uint256 i = j + 1; i < _excluded.length; i++) { - require(_excluded[j] != _excluded[i], "Duplicate exclude address"); - } - } - excluded = _excluded; - /*solium-disable-next-line security/no-block-members*/ - emit SetDefaultExcludedAddresses(excluded, now); - } - - /** - * @notice Function to set withholding tax rates for investors - * @param _investors Addresses of investors - * @param _withholding Withholding tax for individual investors (multiplied by 10**16) - */ - function setWithholding(address[] memory _investors, uint256[] memory _withholding) public withPerm(MANAGE) { - require(_investors.length == _withholding.length, "Mismatched input lengths"); - /*solium-disable-next-line security/no-block-members*/ - emit SetWithholding(_investors, _withholding, now); - for (uint256 i = 0; i < _investors.length; i++) { - require(_withholding[i] <= 10 ** 18, "Incorrect withholding tax"); - withholdingTax[_investors[i]] = _withholding[i]; - } - } - - /** - * @notice Function to set withholding tax rates for investors - * @param _investors Addresses of investor - * @param _withholding Withholding tax for all investors (multiplied by 10**16) - */ - function setWithholdingFixed(address[] memory _investors, uint256 _withholding) public withPerm(MANAGE) { - require(_withholding <= 10 ** 18, "Incorrect withholding tax"); - /*solium-disable-next-line security/no-block-members*/ - emit SetWithholdingFixed(_investors, _withholding, now); - for (uint256 i = 0; i < _investors.length; i++) { - withholdingTax[_investors[i]] = _withholding; - } - } - - /** - * @notice Issuer can push dividends to provided addresses - * @param _dividendIndex Dividend to push - * @param _payees Addresses to which to push the dividend - */ - function pushDividendPaymentToAddresses( - uint256 _dividendIndex, - address payable[] memory _payees - ) - public - withPerm(DISTRIBUTE) - validDividendIndex(_dividendIndex) - { - Dividend storage dividend = dividends[_dividendIndex]; - for (uint256 i = 0; i < _payees.length; i++) { - if ((!dividend.claimed[_payees[i]]) && (!dividend.dividendExcluded[_payees[i]])) { - _payDividend(_payees[i], dividend, _dividendIndex); - } - } - } - - /** - * @notice Issuer can push dividends using the investor list from the security token - * @param _dividendIndex Dividend to push - * @param _start Index in investor list at which to start pushing dividends - * @param _iterations Number of addresses to push dividends for - */ - function pushDividendPayment( - uint256 _dividendIndex, - uint256 _start, - uint256 _iterations - ) public - withPerm(DISTRIBUTE) - validDividendIndex(_dividendIndex) - { - Dividend storage dividend = dividends[_dividendIndex]; - address[] memory investors = ISecurityToken(securityToken).getInvestors(); - uint256 numberInvestors = Math.min(investors.length, _start.add(_iterations)); - for (uint256 i = _start; i < numberInvestors; i++) { - address payable payee = address(uint160(investors[i])); - if ((!dividend.claimed[payee]) && (!dividend.dividendExcluded[payee])) { - _payDividend(payee, dividend, _dividendIndex); - } - } - } - - /** - * @notice Investors can pull their own dividends - * @param _dividendIndex Dividend to pull - */ - function pullDividendPayment(uint256 _dividendIndex) public validDividendIndex(_dividendIndex) { - Dividend storage dividend = dividends[_dividendIndex]; - require(!dividend.claimed[msg.sender], "Dividend already claimed"); - require(!dividend.dividendExcluded[msg.sender], "msg.sender excluded from Dividend"); - _payDividend(msg.sender, dividend, _dividendIndex); - } - - /** - * @notice Internal function for paying dividends - * @param _payee Address of investor - * @param _dividend Storage with previously issued dividends - * @param _dividendIndex Dividend to pay - */ - function _payDividend(address payable _payee, Dividend storage _dividend, uint256 _dividendIndex) internal; - - /** - * @notice Issuer can reclaim remaining unclaimed dividend amounts, for expired dividends - * @param _dividendIndex Dividend to reclaim - */ - function reclaimDividend(uint256 _dividendIndex) external; - - /** - * @notice Calculate amount of dividends claimable - * @param _dividendIndex Dividend to calculate - * @param _payee Affected investor address - * @return claim, withheld amounts - */ - function calculateDividend(uint256 _dividendIndex, address _payee) public view returns(uint256, uint256) { - require(_dividendIndex < dividends.length, "Invalid dividend"); - Dividend storage dividend = dividends[_dividendIndex]; - if (dividend.claimed[_payee] || dividend.dividendExcluded[_payee]) { - return (0, 0); - } - uint256 balance = ISecurityToken(securityToken).balanceOfAt(_payee, dividend.checkpointId); - uint256 claim = balance.mul(dividend.amount).div(dividend.totalSupply); - uint256 withheld = claim.mul(withholdingTax[_payee]).div(uint256(10 ** 18)); - return (claim, withheld); - } - - /** - * @notice Get the index according to the checkpoint id - * @param _checkpointId Checkpoint id to query - * @return uint256[] - */ - function getDividendIndex(uint256 _checkpointId) public view returns(uint256[] memory) { - uint256 counter = 0; - for (uint256 i = 0; i < dividends.length; i++) { - if (dividends[i].checkpointId == _checkpointId) { - counter++; - } - } - - uint256[] memory index = new uint256[](counter); - counter = 0; - for (uint256 j = 0; j < dividends.length; j++) { - if (dividends[j].checkpointId == _checkpointId) { - index[counter] = j; - counter++; - } - } - return index; - } - - /** - * @notice Allows issuer to withdraw withheld tax - * @param _dividendIndex Dividend to withdraw from - */ - function withdrawWithholding(uint256 _dividendIndex) external; - - /** - * @notice Return the permissions flag that are associated with this module - * @return bytes32 array - */ - function getPermissions() public view returns(bytes32[] memory) { - bytes32[] memory allPermissions = new bytes32[](2); - allPermissions[0] = DISTRIBUTE; - allPermissions[1] = MANAGE; - return allPermissions; - } - -} +/** + * DISCLAIMER: Under certain conditions, the function pushDividendPayment + * may fail due to block gas limits. + * If the total number of investors that ever held tokens is greater than ~15,000 then + * the function may fail. If this happens investors can pull their dividends, or the Issuer + * can use pushDividendPaymentToAddresses to provide an explict address list in batches + */ +pragma solidity ^0.5.0; + +import "./ICheckpoint.sol"; +import "./DividendCheckpointStorage.sol"; +import "../Module.sol"; +import "../../interfaces/ISecurityToken.sol"; +import "openzeppelin-solidity/contracts/math/SafeMath.sol"; +import "openzeppelin-solidity/contracts/math/Math.sol"; + +/** + * @title Checkpoint module for issuing ether dividends + * @dev abstract contract + */ +contract DividendCheckpoint is DividendCheckpointStorage, ICheckpoint, Module { + using SafeMath for uint256; + + event SetDefaultExcludedAddresses(address[] _excluded, uint256 _timestamp); + event SetWithholding(address[] _investors, uint256[] _withholding, uint256 _timestamp); + event SetWithholdingFixed(address[] _investors, uint256 _withholding, uint256 _timestamp); + event SetWallet(address indexed _oldWallet, address indexed _newWallet, uint256 _timestamp); + + modifier validDividendIndex(uint256 _dividendIndex) { + require(_dividendIndex < dividends.length, "Invalid dividend"); + require(!dividends[_dividendIndex].reclaimed, "Dividend reclaimed"); + /*solium-disable-next-line security/no-block-members*/ + require(now >= dividends[_dividendIndex].maturity, "Dividend maturity in future"); + /*solium-disable-next-line security/no-block-members*/ + require(now < dividends[_dividendIndex].expiry, "Dividend expiry in past"); + _; + } + + /** + * @notice Function used to intialize the contract variables + * @param _wallet Ethereum account address to receive reclaimed dividends and tax + */ + function configure( + address payable _wallet + ) public onlyFactory { + _setWallet(_wallet); + } + + /** + * @notice Init function i.e generalise function to maintain the structure of the module contract + * @return bytes4 + */ + function getInitFunction() public pure returns(bytes4) { + return this.configure.selector; + } + + /** + * @notice Function used to change wallet address + * @param _wallet Ethereum account address to receive reclaimed dividends and tax + */ + function changeWallet(address payable _wallet) external onlyOwner { + _setWallet(_wallet); + } + + function _setWallet(address payable _wallet) internal { + require(_wallet != address(0)); + emit SetWallet(wallet, _wallet, now); + wallet = _wallet; + } + + /** + * @notice Return the default excluded addresses + * @return List of excluded addresses + */ + function getDefaultExcluded() external view returns(address[] memory) { + return excluded; + } + + /** + * @notice Creates a checkpoint on the security token + * @return Checkpoint ID + */ + function createCheckpoint() public withPerm(CHECKPOINT) returns(uint256) { + return ISecurityToken(securityToken).createCheckpoint(); + } + + /** + * @notice Function to clear and set list of excluded addresses used for future dividends + * @param _excluded Addresses of investors + */ + function setDefaultExcluded(address[] memory _excluded) public withPerm(MANAGE) { + require(_excluded.length <= EXCLUDED_ADDRESS_LIMIT, "Too many excluded addresses"); + for (uint256 j = 0; j < _excluded.length; j++) { + require(_excluded[j] != address(0), "Invalid address"); + for (uint256 i = j + 1; i < _excluded.length; i++) { + require(_excluded[j] != _excluded[i], "Duplicate exclude address"); + } + } + excluded = _excluded; + /*solium-disable-next-line security/no-block-members*/ + emit SetDefaultExcludedAddresses(excluded, now); + } + + /** + * @notice Function to set withholding tax rates for investors + * @param _investors Addresses of investors + * @param _withholding Withholding tax for individual investors (multiplied by 10**16) + */ + function setWithholding(address[] memory _investors, uint256[] memory _withholding) public withPerm(MANAGE) { + require(_investors.length == _withholding.length, "Mismatched input lengths"); + /*solium-disable-next-line security/no-block-members*/ + emit SetWithholding(_investors, _withholding, now); + for (uint256 i = 0; i < _investors.length; i++) { + require(_withholding[i] <= 10 ** 18, "Incorrect withholding tax"); + withholdingTax[_investors[i]] = _withholding[i]; + } + } + + /** + * @notice Function to set withholding tax rates for investors + * @param _investors Addresses of investor + * @param _withholding Withholding tax for all investors (multiplied by 10**16) + */ + function setWithholdingFixed(address[] memory _investors, uint256 _withholding) public withPerm(MANAGE) { + require(_withholding <= 10 ** 18, "Incorrect withholding tax"); + /*solium-disable-next-line security/no-block-members*/ + emit SetWithholdingFixed(_investors, _withholding, now); + for (uint256 i = 0; i < _investors.length; i++) { + withholdingTax[_investors[i]] = _withholding; + } + } + + /** + * @notice Issuer can push dividends to provided addresses + * @param _dividendIndex Dividend to push + * @param _payees Addresses to which to push the dividend + */ + function pushDividendPaymentToAddresses( + uint256 _dividendIndex, + address payable[] memory _payees + ) + public + withPerm(DISTRIBUTE) + validDividendIndex(_dividendIndex) + { + Dividend storage dividend = dividends[_dividendIndex]; + for (uint256 i = 0; i < _payees.length; i++) { + if ((!dividend.claimed[_payees[i]]) && (!dividend.dividendExcluded[_payees[i]])) { + _payDividend(_payees[i], dividend, _dividendIndex); + } + } + } + + /** + * @notice Issuer can push dividends using the investor list from the security token + * @param _dividendIndex Dividend to push + * @param _start Index in investor list at which to start pushing dividends + * @param _iterations Number of addresses to push dividends for + */ + function pushDividendPayment( + uint256 _dividendIndex, + uint256 _start, + uint256 _iterations + ) public + withPerm(DISTRIBUTE) + validDividendIndex(_dividendIndex) + { + Dividend storage dividend = dividends[_dividendIndex]; + uint256 checkpointId = dividend.checkpointId; + address[] memory investors = ISecurityToken(securityToken).getInvestorsAt(checkpointId); + uint256 numberInvestors = Math.min(investors.length, _start.add(_iterations)); + for (uint256 i = _start; i < numberInvestors; i++) { + address payable payee = address(uint160(investors[i])); + if ((!dividend.claimed[payee]) && (!dividend.dividendExcluded[payee])) { + _payDividend(payee, dividend, _dividendIndex); + } + } + } + + /** + * @notice Investors can pull their own dividends + * @param _dividendIndex Dividend to pull + */ + function pullDividendPayment(uint256 _dividendIndex) public validDividendIndex(_dividendIndex) { + Dividend storage dividend = dividends[_dividendIndex]; + require(!dividend.claimed[msg.sender], "Dividend already claimed"); + require(!dividend.dividendExcluded[msg.sender], "msg.sender excluded from Dividend"); + _payDividend(msg.sender, dividend, _dividendIndex); + } + + /** + * @notice Internal function for paying dividends + * @param _payee Address of investor + * @param _dividend Storage with previously issued dividends + * @param _dividendIndex Dividend to pay + */ + function _payDividend(address payable _payee, Dividend storage _dividend, uint256 _dividendIndex) internal; + + /** + * @notice Issuer can reclaim remaining unclaimed dividend amounts, for expired dividends + * @param _dividendIndex Dividend to reclaim + */ + function reclaimDividend(uint256 _dividendIndex) external; + + /** + * @notice Calculate amount of dividends claimable + * @param _dividendIndex Dividend to calculate + * @param _payee Affected investor address + * @return claim, withheld amounts + */ + function calculateDividend(uint256 _dividendIndex, address _payee) public view returns(uint256, uint256) { + require(_dividendIndex < dividends.length, "Invalid dividend"); + Dividend storage dividend = dividends[_dividendIndex]; + if (dividend.claimed[_payee] || dividend.dividendExcluded[_payee]) { + return (0, 0); + } + uint256 balance = ISecurityToken(securityToken).balanceOfAt(_payee, dividend.checkpointId); + uint256 claim = balance.mul(dividend.amount).div(dividend.totalSupply); + uint256 withheld = claim.mul(withholdingTax[_payee]).div(uint256(10 ** 18)); + return (claim, withheld); + } + + /** + * @notice Get the index according to the checkpoint id + * @param _checkpointId Checkpoint id to query + * @return uint256[] + */ + function getDividendIndex(uint256 _checkpointId) public view returns(uint256[] memory) { + uint256 counter = 0; + for (uint256 i = 0; i < dividends.length; i++) { + if (dividends[i].checkpointId == _checkpointId) { + counter++; + } + } + + uint256[] memory index = new uint256[](counter); + counter = 0; + for (uint256 j = 0; j < dividends.length; j++) { + if (dividends[j].checkpointId == _checkpointId) { + index[counter] = j; + counter++; + } + } + return index; + } + + /** + * @notice Allows issuer to withdraw withheld tax + * @param _dividendIndex Dividend to withdraw from + */ + function withdrawWithholding(uint256 _dividendIndex) external; + + /** + * @notice Get static dividend data + * @return uint256[] timestamp of dividends creation + * @return uint256[] timestamp of dividends maturity + * @return uint256[] timestamp of dividends expiry + * @return uint256[] amount of dividends + * @return uint256[] claimed amount of dividends + * @return bytes32[] name of dividends + */ + function getDividendsData() external view returns ( + uint256[] memory createds, + uint256[] memory maturitys, + uint256[] memory expirys, + uint256[] memory amounts, + uint256[] memory claimedAmounts, + bytes32[] memory names) + { + createds = new uint256[](dividends.length); + maturitys = new uint256[](dividends.length); + expirys = new uint256[](dividends.length); + amounts = new uint256[](dividends.length); + claimedAmounts = new uint256[](dividends.length); + names = new bytes32[](dividends.length); + for (uint256 i = 0; i < dividends.length; i++) { + (createds[i], maturitys[i], expirys[i], amounts[i], claimedAmounts[i], names[i]) = getDividendData(i); + } + } + + /** + * @notice Get static dividend data + * @return uint256 timestamp of dividend creation + * @return uint256 timestamp of dividend maturity + * @return uint256 timestamp of dividend expiry + * @return uint256 amount of dividend + * @return uint256 claimed amount of dividend + * @return bytes32 name of dividend + */ + function getDividendData(uint256 _dividendIndex) public view returns ( + uint256 created, + uint256 maturity, + uint256 expiry, + uint256 amount, + uint256 claimedAmount, + bytes32 name) + { + created = dividends[_dividendIndex].created; + maturity = dividends[_dividendIndex].maturity; + expiry = dividends[_dividendIndex].expiry; + amount = dividends[_dividendIndex].amount; + claimedAmount = dividends[_dividendIndex].claimedAmount; + name = dividends[_dividendIndex].name; + } + + /** + * @notice Retrieves list of investors, their claim status and whether they are excluded + * @param _dividendIndex Dividend to withdraw from + * @return address[] list of investors + * @return bool[] whether investor has claimed + * @return bool[] whether investor is excluded + * @return uint256[] amount of withheld tax (estimate if not claimed) + * @return uint256[] amount of claim (estimate if not claimeed) + * @return uint256[] investor balance + */ + function getDividendProgress(uint256 _dividendIndex) external view returns ( + address[] memory investors, + bool[] memory resultClaimed, + bool[] memory resultExcluded, + uint256[] memory resultWithheld, + uint256[] memory resultAmount, + uint256[] memory resultBalance) + { + require(_dividendIndex < dividends.length, "Invalid dividend"); + //Get list of Investors + Dividend storage dividend = dividends[_dividendIndex]; + uint256 checkpointId = dividend.checkpointId; + investors = ISecurityToken(securityToken).getInvestorsAt(checkpointId); + resultClaimed = new bool[](investors.length); + resultExcluded = new bool[](investors.length); + resultWithheld = new uint256[](investors.length); + resultAmount = new uint256[](investors.length); + resultBalance = new uint256[](investors.length); + for (uint256 i; i < investors.length; i++) { + resultClaimed[i] = dividend.claimed[investors[i]]; + resultExcluded[i] = dividend.dividendExcluded[investors[i]]; + resultBalance[i] = ISecurityToken(securityToken).balanceOfAt(investors[i], dividend.checkpointId); + if (!resultExcluded[i]) { + if (resultClaimed[i]) { + resultWithheld[i] = dividend.withheld[investors[i]]; + resultAmount[i] = resultBalance[i].mul(dividend.amount).div(dividend.totalSupply).sub(resultWithheld[i]); + } else { + (uint256 claim, uint256 withheld) = calculateDividend(_dividendIndex, investors[i]); + resultWithheld[i] = withheld; + resultAmount[i] = claim.sub(withheld); + } + } + } + } + + /** + * @notice Retrieves list of investors, their balances, and their current withholding tax percentage + * @param _checkpointId Checkpoint Id to query for + * @return address[] list of investors + * @return uint256[] investor balances + * @return uint256[] investor withheld percentages + */ + function getCheckpointData(uint256 _checkpointId) external view returns (address[] memory investors, uint256[] memory balances, uint256[] memory withholdings) { + require(_checkpointId <= ISecurityToken(securityToken).currentCheckpointId(), "Invalid checkpoint"); + investors = ISecurityToken(securityToken).getInvestorsAt(_checkpointId); + balances = new uint256[](investors.length); + withholdings = new uint256[](investors.length); + for (uint256 i; i < investors.length; i++) { + balances[i] = ISecurityToken(securityToken).balanceOfAt(investors[i], _checkpointId); + withholdings[i] = withholdingTax[investors[i]]; + } + } + + /** + * @notice Checks whether an address is excluded from claiming a dividend + * @param _dividendIndex Dividend to withdraw from + * @return bool whether the address is excluded + */ + function isExcluded(address _investor, uint256 _dividendIndex) external view returns (bool) { + require(_dividendIndex < dividends.length, "Invalid dividend"); + return dividends[_dividendIndex].dividendExcluded[_investor]; + } + + /** + * @notice Checks whether an address has claimed a dividend + * @param _dividendIndex Dividend to withdraw from + * @return bool whether the address has claimed + */ + function isClaimed(address _investor, uint256 _dividendIndex) external view returns (bool) { + require(_dividendIndex < dividends.length, "Invalid dividend"); + return dividends[_dividendIndex].claimed[_investor]; + } + + /** + * @notice Return the permissions flag that are associated with this module + * @return bytes32 array + */ + function getPermissions() public view returns(bytes32[] memory) { + bytes32[] memory allPermissions = new bytes32[](2); + allPermissions[0] = DISTRIBUTE; + allPermissions[1] = MANAGE; + return allPermissions; + } + +} diff --git a/contracts/modules/Checkpoint/DividendCheckpointStorage.sol b/contracts/modules/Checkpoint/DividendCheckpointStorage.sol index fa2f2793f..27315b16e 100644 --- a/contracts/modules/Checkpoint/DividendCheckpointStorage.sol +++ b/contracts/modules/Checkpoint/DividendCheckpointStorage.sol @@ -5,7 +5,10 @@ pragma solidity ^0.5.0; * @dev abstract contract */ contract DividendCheckpointStorage { - uint256 public EXCLUDED_ADDRESS_LIMIT = 50; + + // Address to which reclaimed dividends and withholding tax is sent + address payable public wallet; + uint256 public EXCLUDED_ADDRESS_LIMIT = 150; bytes32 public constant DISTRIBUTE = "DISTRIBUTE"; bytes32 public constant MANAGE = "MANAGE"; bytes32 public constant CHECKPOINT = "CHECKPOINT"; @@ -14,16 +17,17 @@ contract DividendCheckpointStorage { uint256 checkpointId; uint256 created; // Time at which the dividend was created uint256 maturity; // Time after which dividend can be claimed - set to 0 to bypass - uint256 expiry; // Time until which dividend can be claimed - after this time any remaining amount can be withdrawn by issuer - - // set to very high value to bypass + uint256 expiry; // Time until which dividend can be claimed - after this time any remaining amount can be withdrawn by issuer - + // set to very high value to bypass uint256 amount; // Dividend amount in WEI uint256 claimedAmount; // Amount of dividend claimed so far uint256 totalSupply; // Total supply at the associated checkpoint (avoids recalculating this) - bool reclaimed; // True if expiry has passed and issuer has reclaimed remaining dividend - uint256 dividendWithheld; - uint256 dividendWithheldReclaimed; - mapping(address => bool) claimed; // List of addresses which have claimed dividend - mapping(address => bool) dividendExcluded; // List of addresses which cannot claim dividends + bool reclaimed; // True if expiry has passed and issuer has reclaimed remaining dividend + uint256 totalWithheld; + uint256 totalWithheldWithdrawn; + mapping (address => bool) claimed; // List of addresses which have claimed dividend + mapping (address => bool) dividendExcluded; // List of addresses which cannot claim dividends + mapping (address => uint256) withheld; // Amount of tax withheld from claim bytes32 name; // Name/title - used for identification } @@ -34,7 +38,7 @@ contract DividendCheckpointStorage { address[] public excluded; // Mapping from address to withholding tax as a percentage * 10**16 - mapping(address => uint256) public withholdingTax; + mapping (address => uint256) public withholdingTax; // Total amount of ETH withheld per investor mapping(address => uint256) public investorWithheld; diff --git a/contracts/modules/Checkpoint/ERC20DividendCheckpoint.sol b/contracts/modules/Checkpoint/ERC20DividendCheckpoint.sol index fecf21f8f..a672b88ef 100644 --- a/contracts/modules/Checkpoint/ERC20DividendCheckpoint.sol +++ b/contracts/modules/Checkpoint/ERC20DividendCheckpoint.sol @@ -1,279 +1,279 @@ -pragma solidity ^0.5.0; - -import "./DividendCheckpoint.sol"; -import "./ERC20DividendCheckpointStorage.sol"; -import "../../interfaces/IOwnable.sol"; -import "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol"; - -/** - * @title Checkpoint module for issuing ERC20 dividends - */ -contract ERC20DividendCheckpoint is ERC20DividendCheckpointStorage, DividendCheckpoint { - using SafeMath for uint256; - - event ERC20DividendDeposited( - address indexed _depositor, - uint256 _checkpointId, - uint256 _created, - uint256 _maturity, - uint256 _expiry, - address indexed _token, - uint256 _amount, - uint256 _totalSupply, - uint256 _dividendIndex, - bytes32 indexed _name - ); - event ERC20DividendClaimed(address indexed _payee, uint256 _dividendIndex, address indexed _token, uint256 _amount, uint256 _withheld); - event ERC20DividendReclaimed(address indexed _claimer, uint256 _dividendIndex, address indexed _token, uint256 _claimedAmount); - event ERC20DividendWithholdingWithdrawn( - address indexed _claimer, - uint256 _dividendIndex, - address indexed _token, - uint256 _withheldAmount - ); - - /** - * @notice Constructor - * @param _securityToken Address of the security token - */ - constructor(address _securityToken, address _polyToken) public Module(_securityToken, _polyToken) { - - } - - /** - * @notice Creates a dividend and checkpoint for the dividend - * @param _maturity Time from which dividend can be paid - * @param _expiry Time until dividend can no longer be paid, and can be reclaimed by issuer - * @param _token Address of ERC20 token in which dividend is to be denominated - * @param _amount Amount of specified token for dividend - * @param _name Name/Title for identification - */ - function createDividend( - uint256 _maturity, - uint256 _expiry, - address _token, - uint256 _amount, - bytes32 _name - ) - external - withPerm(MANAGE) - { - createDividendWithExclusions(_maturity, _expiry, _token, _amount, excluded, _name); - } - - /** - * @notice Creates a dividend with a provided checkpoint - * @param _maturity Time from which dividend can be paid - * @param _expiry Time until dividend can no longer be paid, and can be reclaimed by issuer - * @param _token Address of ERC20 token in which dividend is to be denominated - * @param _amount Amount of specified token for dividend - * @param _checkpointId Checkpoint id from which to create dividends - * @param _name Name/Title for identification - */ - function createDividendWithCheckpoint( - uint256 _maturity, - uint256 _expiry, - address _token, - uint256 _amount, - uint256 _checkpointId, - bytes32 _name - ) - external - withPerm(MANAGE) - { - _createDividendWithCheckpointAndExclusions(_maturity, _expiry, _token, _amount, _checkpointId, excluded, _name); - } - - /** - * @notice Creates a dividend and checkpoint for the dividend - * @param _maturity Time from which dividend can be paid - * @param _expiry Time until dividend can no longer be paid, and can be reclaimed by issuer - * @param _token Address of ERC20 token in which dividend is to be denominated - * @param _amount Amount of specified token for dividend - * @param _excluded List of addresses to exclude - * @param _name Name/Title for identification - */ - function createDividendWithExclusions( - uint256 _maturity, - uint256 _expiry, - address _token, - uint256 _amount, - address[] memory _excluded, - bytes32 _name - ) - public - withPerm(MANAGE) - { - uint256 checkpointId = ISecurityToken(securityToken).createCheckpoint(); - _createDividendWithCheckpointAndExclusions(_maturity, _expiry, _token, _amount, checkpointId, _excluded, _name); - } - - /** - * @notice Creates a dividend with a provided checkpoint - * @param _maturity Time from which dividend can be paid - * @param _expiry Time until dividend can no longer be paid, and can be reclaimed by issuer - * @param _token Address of ERC20 token in which dividend is to be denominated - * @param _amount Amount of specified token for dividend - * @param _checkpointId Checkpoint id from which to create dividends - * @param _excluded List of addresses to exclude - * @param _name Name/Title for identification - */ - function createDividendWithCheckpointAndExclusions( - uint256 _maturity, - uint256 _expiry, - address _token, - uint256 _amount, - uint256 _checkpointId, - address[] memory _excluded, - bytes32 _name - ) - public - withPerm(MANAGE) - { - _createDividendWithCheckpointAndExclusions(_maturity, _expiry, _token, _amount, _checkpointId, _excluded, _name); - } - - /** - * @notice Creates a dividend with a provided checkpoint - * @param _maturity Time from which dividend can be paid - * @param _expiry Time until dividend can no longer be paid, and can be reclaimed by issuer - * @param _token Address of ERC20 token in which dividend is to be denominated - * @param _amount Amount of specified token for dividend - * @param _checkpointId Checkpoint id from which to create dividends - * @param _excluded List of addresses to exclude - * @param _name Name/Title for identification - */ - function _createDividendWithCheckpointAndExclusions( - uint256 _maturity, - uint256 _expiry, - address _token, - uint256 _amount, - uint256 _checkpointId, - address[] memory _excluded, - bytes32 _name - ) - internal - { - ISecurityToken securityTokenInstance = ISecurityToken(securityToken); - require(_excluded.length <= EXCLUDED_ADDRESS_LIMIT, "Too many addresses excluded"); - require(_expiry > _maturity, "Expiry before maturity"); - /*solium-disable-next-line security/no-block-members*/ - require(_expiry > now, "Expiry in past"); - require(_amount > 0, "No dividend sent"); - require(_token != address(0), "Invalid token"); - require(_checkpointId <= securityTokenInstance.currentCheckpointId(), "Invalid checkpoint"); - require(IERC20(_token).transferFrom(msg.sender, address(this), _amount), "insufficent allowance"); - require(_name[0] != 0); - uint256 dividendIndex = dividends.length; - uint256 currentSupply = securityTokenInstance.totalSupplyAt(_checkpointId); - uint256 excludedSupply = 0; - dividends.push( - Dividend( - _checkpointId, - now, /*solium-disable-line security/no-block-members*/ - _maturity, - _expiry, - _amount, - 0, - 0, - false, - 0, - 0, - _name - ) - ); - - for (uint256 j = 0; j < _excluded.length; j++) { - require(_excluded[j] != address(0), "Invalid address"); - require(!dividends[dividendIndex].dividendExcluded[_excluded[j]], "duped exclude address"); - excludedSupply = excludedSupply.add(securityTokenInstance.balanceOfAt(_excluded[j], _checkpointId)); - dividends[dividendIndex].dividendExcluded[_excluded[j]] = true; - } - - dividends[dividendIndex].totalSupply = currentSupply.sub(excludedSupply); - dividendTokens[dividendIndex] = _token; - _emitERC20DividendDepositedEvent(_checkpointId, _maturity, _expiry, _token, _amount, currentSupply, dividendIndex, _name); - } - - /** - * @notice Emits the ERC20DividendDeposited event. - * Seperated into a different function as a workaround for stack too deep error - */ - function _emitERC20DividendDepositedEvent( - uint256 _checkpointId, - uint256 _maturity, - uint256 _expiry, - address _token, - uint256 _amount, - uint256 currentSupply, - uint256 dividendIndex, - bytes32 _name - ) - internal - { - /*solium-disable-next-line security/no-block-members*/ - emit ERC20DividendDeposited( - msg.sender, - _checkpointId, - now, - _maturity, - _expiry, - _token, - _amount, - currentSupply, - dividendIndex, - _name - ); - } - - /** - * @notice Internal function for paying dividends - * @param _payee Address of investor - * @param _dividend Storage with previously issued dividends - * @param _dividendIndex Dividend to pay - */ - function _payDividend(address payable _payee, Dividend storage _dividend, uint256 _dividendIndex) internal { - (uint256 claim, uint256 withheld) = calculateDividend(_dividendIndex, _payee); - _dividend.claimed[_payee] = true; - _dividend.claimedAmount = claim.add(_dividend.claimedAmount); - uint256 claimAfterWithheld = claim.sub(withheld); - if (claimAfterWithheld > 0) { - require(IERC20(dividendTokens[_dividendIndex]).transfer(_payee, claimAfterWithheld), "transfer failed"); - _dividend.dividendWithheld = _dividend.dividendWithheld.add(withheld); - investorWithheld[_payee] = investorWithheld[_payee].add(withheld); - emit ERC20DividendClaimed(_payee, _dividendIndex, dividendTokens[_dividendIndex], claim, withheld); - } - } - - /** - * @notice Issuer can reclaim remaining unclaimed dividend amounts, for expired dividends - * @param _dividendIndex Dividend to reclaim - */ - function reclaimDividend(uint256 _dividendIndex) external withPerm(MANAGE) { - require(_dividendIndex < dividends.length, "Invalid dividend"); - /*solium-disable-next-line security/no-block-members*/ - require(now >= dividends[_dividendIndex].expiry, "Dividend expiry in future"); - require(!dividends[_dividendIndex].reclaimed, "already claimed"); - dividends[_dividendIndex].reclaimed = true; - Dividend storage dividend = dividends[_dividendIndex]; - uint256 remainingAmount = dividend.amount.sub(dividend.claimedAmount); - address owner = IOwnable(securityToken).owner(); - require(IERC20(dividendTokens[_dividendIndex]).transfer(owner, remainingAmount), "transfer failed"); - emit ERC20DividendReclaimed(owner, _dividendIndex, dividendTokens[_dividendIndex], remainingAmount); - } - - /** - * @notice Allows issuer to withdraw withheld tax - * @param _dividendIndex Dividend to withdraw from - */ - function withdrawWithholding(uint256 _dividendIndex) external withPerm(MANAGE) { - require(_dividendIndex < dividends.length, "Invalid dividend"); - Dividend storage dividend = dividends[_dividendIndex]; - uint256 remainingWithheld = dividend.dividendWithheld.sub(dividend.dividendWithheldReclaimed); - dividend.dividendWithheldReclaimed = dividend.dividendWithheld; - address owner = IOwnable(securityToken).owner(); - require(IERC20(dividendTokens[_dividendIndex]).transfer(owner, remainingWithheld), "transfer failed"); - emit ERC20DividendWithholdingWithdrawn(owner, _dividendIndex, dividendTokens[_dividendIndex], remainingWithheld); - } - -} +pragma solidity ^0.5.0; + +import "./DividendCheckpoint.sol"; +import "./ERC20DividendCheckpointStorage.sol"; +import "../../interfaces/IOwnable.sol"; +import "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol"; + +/** + * @title Checkpoint module for issuing ERC20 dividends + */ +contract ERC20DividendCheckpoint is ERC20DividendCheckpointStorage, DividendCheckpoint { + using SafeMath for uint256; + + event ERC20DividendDeposited( + address indexed _depositor, + uint256 _checkpointId, + uint256 _created, + uint256 _maturity, + uint256 _expiry, + address indexed _token, + uint256 _amount, + uint256 _totalSupply, + uint256 _dividendIndex, + bytes32 indexed _name + ); + event ERC20DividendClaimed(address indexed _payee, uint256 indexed _dividendIndex, address indexed _token, uint256 _amount, uint256 _withheld); + event ERC20DividendReclaimed(address indexed _claimer, uint256 indexed _dividendIndex, address indexed _token, uint256 _claimedAmount); + event ERC20DividendWithholdingWithdrawn( + address indexed _claimer, + uint256 indexed _dividendIndex, + address indexed _token, + uint256 _withheldAmount + ); + + /** + * @notice Constructor + * @param _securityToken Address of the security token + */ + constructor(address _securityToken, address _polyToken) public Module(_securityToken, _polyToken) { + + } + + /** + * @notice Creates a dividend and checkpoint for the dividend + * @param _maturity Time from which dividend can be paid + * @param _expiry Time until dividend can no longer be paid, and can be reclaimed by issuer + * @param _token Address of ERC20 token in which dividend is to be denominated + * @param _amount Amount of specified token for dividend + * @param _name Name/Title for identification + */ + function createDividend( + uint256 _maturity, + uint256 _expiry, + address _token, + uint256 _amount, + bytes32 _name + ) + external + withPerm(MANAGE) + { + createDividendWithExclusions(_maturity, _expiry, _token, _amount, excluded, _name); + } + + /** + * @notice Creates a dividend with a provided checkpoint + * @param _maturity Time from which dividend can be paid + * @param _expiry Time until dividend can no longer be paid, and can be reclaimed by issuer + * @param _token Address of ERC20 token in which dividend is to be denominated + * @param _amount Amount of specified token for dividend + * @param _checkpointId Checkpoint id from which to create dividends + * @param _name Name/Title for identification + */ + function createDividendWithCheckpoint( + uint256 _maturity, + uint256 _expiry, + address _token, + uint256 _amount, + uint256 _checkpointId, + bytes32 _name + ) + external + withPerm(MANAGE) + { + _createDividendWithCheckpointAndExclusions(_maturity, _expiry, _token, _amount, _checkpointId, excluded, _name); + } + + /** + * @notice Creates a dividend and checkpoint for the dividend + * @param _maturity Time from which dividend can be paid + * @param _expiry Time until dividend can no longer be paid, and can be reclaimed by issuer + * @param _token Address of ERC20 token in which dividend is to be denominated + * @param _amount Amount of specified token for dividend + * @param _excluded List of addresses to exclude + * @param _name Name/Title for identification + */ + function createDividendWithExclusions( + uint256 _maturity, + uint256 _expiry, + address _token, + uint256 _amount, + address[] memory _excluded, + bytes32 _name + ) + public + withPerm(MANAGE) + { + uint256 checkpointId = ISecurityToken(securityToken).createCheckpoint(); + _createDividendWithCheckpointAndExclusions(_maturity, _expiry, _token, _amount, checkpointId, _excluded, _name); + } + + /** + * @notice Creates a dividend with a provided checkpoint + * @param _maturity Time from which dividend can be paid + * @param _expiry Time until dividend can no longer be paid, and can be reclaimed by issuer + * @param _token Address of ERC20 token in which dividend is to be denominated + * @param _amount Amount of specified token for dividend + * @param _checkpointId Checkpoint id from which to create dividends + * @param _excluded List of addresses to exclude + * @param _name Name/Title for identification + */ + function createDividendWithCheckpointAndExclusions( + uint256 _maturity, + uint256 _expiry, + address _token, + uint256 _amount, + uint256 _checkpointId, + address[] memory _excluded, + bytes32 _name + ) + public + withPerm(MANAGE) + { + _createDividendWithCheckpointAndExclusions(_maturity, _expiry, _token, _amount, _checkpointId, _excluded, _name); + } + + /** + * @notice Creates a dividend with a provided checkpoint + * @param _maturity Time from which dividend can be paid + * @param _expiry Time until dividend can no longer be paid, and can be reclaimed by issuer + * @param _token Address of ERC20 token in which dividend is to be denominated + * @param _amount Amount of specified token for dividend + * @param _checkpointId Checkpoint id from which to create dividends + * @param _excluded List of addresses to exclude + * @param _name Name/Title for identification + */ + function _createDividendWithCheckpointAndExclusions( + uint256 _maturity, + uint256 _expiry, + address _token, + uint256 _amount, + uint256 _checkpointId, + address[] memory _excluded, + bytes32 _name + ) + internal + { + ISecurityToken securityTokenInstance = ISecurityToken(securityToken); + require(_excluded.length <= EXCLUDED_ADDRESS_LIMIT, "Too many addresses excluded"); + require(_expiry > _maturity, "Expiry before maturity"); + /*solium-disable-next-line security/no-block-members*/ + require(_expiry > now, "Expiry in past"); + require(_amount > 0, "No dividend sent"); + require(_token != address(0), "Invalid token"); + require(_checkpointId <= securityTokenInstance.currentCheckpointId(), "Invalid checkpoint"); + require(IERC20(_token).transferFrom(msg.sender, address(this), _amount), "insufficent allowance"); + require(_name[0] != 0); + uint256 dividendIndex = dividends.length; + uint256 currentSupply = securityTokenInstance.totalSupplyAt(_checkpointId); + uint256 excludedSupply = 0; + dividends.push( + Dividend( + _checkpointId, + now, /*solium-disable-line security/no-block-members*/ + _maturity, + _expiry, + _amount, + 0, + 0, + false, + 0, + 0, + _name + ) + ); + + for (uint256 j = 0; j < _excluded.length; j++) { + require(_excluded[j] != address(0), "Invalid address"); + require(!dividends[dividendIndex].dividendExcluded[_excluded[j]], "duped exclude address"); + excludedSupply = excludedSupply.add(securityTokenInstance.balanceOfAt(_excluded[j], _checkpointId)); + dividends[dividendIndex].dividendExcluded[_excluded[j]] = true; + } + + dividends[dividendIndex].totalSupply = currentSupply.sub(excludedSupply); + dividendTokens[dividendIndex] = _token; + _emitERC20DividendDepositedEvent(_checkpointId, _maturity, _expiry, _token, _amount, currentSupply, dividendIndex, _name); + } + + /** + * @notice Emits the ERC20DividendDeposited event. + * Seperated into a different function as a workaround for stack too deep error + */ + function _emitERC20DividendDepositedEvent( + uint256 _checkpointId, + uint256 _maturity, + uint256 _expiry, + address _token, + uint256 _amount, + uint256 currentSupply, + uint256 dividendIndex, + bytes32 _name + ) + internal + { + /*solium-disable-next-line security/no-block-members*/ + emit ERC20DividendDeposited( + msg.sender, + _checkpointId, + now, + _maturity, + _expiry, + _token, + _amount, + currentSupply, + dividendIndex, + _name + ); + } + + /** + * @notice Internal function for paying dividends + * @param _payee Address of investor + * @param _dividend Storage with previously issued dividends + * @param _dividendIndex Dividend to pay + */ + function _payDividend(address payable _payee, Dividend storage _dividend, uint256 _dividendIndex) internal { + (uint256 claim, uint256 withheld) = calculateDividend(_dividendIndex, _payee); + _dividend.claimed[_payee] = true; + _dividend.claimedAmount = claim.add(_dividend.claimedAmount); + uint256 claimAfterWithheld = claim.sub(withheld); + if (claimAfterWithheld > 0) { + require(IERC20(dividendTokens[_dividendIndex]).transfer(_payee, claimAfterWithheld), "transfer failed"); + if (withheld > 0) { + _dividend.totalWithheld = _dividend.totalWithheld.add(withheld); + _dividend.withheld[_payee] = withheld; + } + emit ERC20DividendClaimed(_payee, _dividendIndex, dividendTokens[_dividendIndex], claim, withheld); + } + } + + /** + * @notice Issuer can reclaim remaining unclaimed dividend amounts, for expired dividends + * @param _dividendIndex Dividend to reclaim + */ + function reclaimDividend(uint256 _dividendIndex) external withPerm(MANAGE) { + require(_dividendIndex < dividends.length, "Invalid dividend"); + /*solium-disable-next-line security/no-block-members*/ + require(now >= dividends[_dividendIndex].expiry, "Dividend expiry in future"); + require(!dividends[_dividendIndex].reclaimed, "already claimed"); + dividends[_dividendIndex].reclaimed = true; + Dividend storage dividend = dividends[_dividendIndex]; + uint256 remainingAmount = dividend.amount.sub(dividend.claimedAmount); + require(IERC20(dividendTokens[_dividendIndex]).transfer(wallet, remainingAmount), "transfer failed"); + emit ERC20DividendReclaimed(wallet, _dividendIndex, dividendTokens[_dividendIndex], remainingAmount); + } + + /** + * @notice Allows issuer to withdraw withheld tax + * @param _dividendIndex Dividend to withdraw from + */ + function withdrawWithholding(uint256 _dividendIndex) external withPerm(MANAGE) { + require(_dividendIndex < dividends.length, "Invalid dividend"); + Dividend storage dividend = dividends[_dividendIndex]; + uint256 remainingWithheld = dividend.totalWithheld.sub(dividend.totalWithheldWithdrawn); + dividend.totalWithheldWithdrawn = dividend.totalWithheld; + require(IERC20(dividendTokens[_dividendIndex]).transfer(wallet, remainingWithheld), "transfer failed"); + emit ERC20DividendWithholdingWithdrawn(wallet, _dividendIndex, dividendTokens[_dividendIndex], remainingWithheld); + } + +} diff --git a/contracts/modules/Checkpoint/ERC20DividendCheckpointFactory.sol b/contracts/modules/Checkpoint/ERC20DividendCheckpointFactory.sol index b9b83ef0d..041c8222a 100644 --- a/contracts/modules/Checkpoint/ERC20DividendCheckpointFactory.sol +++ b/contracts/modules/Checkpoint/ERC20DividendCheckpointFactory.sol @@ -1,6 +1,8 @@ pragma solidity ^0.5.0; import "../../proxy/ERC20DividendCheckpointProxy.sol"; +import "../../libraries/Util.sol"; +import "../../interfaces/IBoot.sol"; import "../ModuleFactory.sol"; /** @@ -21,9 +23,9 @@ contract ERC20DividendCheckpointFactory is ModuleFactory { uint256 _usageCost, uint256 _subscriptionCost, address _logicContract - ) - public - ModuleFactory(_setupCost, _usageCost, _subscriptionCost) + ) + public + ModuleFactory(_setupCost, _usageCost, _subscriptionCost) { require(_logicContract != address(0), "Invalid logic contract"); version = "2.1.0"; @@ -39,14 +41,15 @@ contract ERC20DividendCheckpointFactory is ModuleFactory { * @notice Used to launch the Module with the help of factory * @return Address Contract address of the Module */ - function deploy( - bytes calldata /* _data */ - ) - external - returns(address) - { + function deploy(bytes calldata _data) external returns(address) { address polyToken = _takeFee(); - address erc20DividendCheckpoint = address(new ERC20DividendCheckpointProxy(msg.sender, polyToken, logicContract)); + address erc20DividendCheckpoint = address(new ERC20DividendCheckpointProxy(msg.sender, address(polyToken), logicContract)); + //Checks that _data is valid (not calling anything it shouldn't) + require(Util.getSig(_data) == IBoot(erc20DividendCheckpoint).getInitFunction(), "Invalid data"); + /*solium-disable-next-line security/no-low-level-calls*/ + bool success; + (success, ) = erc20DividendCheckpoint.call(_data); + require(success, "Unsuccessful call"); /*solium-disable-next-line security/no-block-members*/ emit GenerateModuleFromFactory(erc20DividendCheckpoint, getName(), address(this), msg.sender, setupCost, now); return erc20DividendCheckpoint; diff --git a/contracts/modules/Checkpoint/EtherDividendCheckpoint.sol b/contracts/modules/Checkpoint/EtherDividendCheckpoint.sol index 30ac0e724..0c8cae750 100644 --- a/contracts/modules/Checkpoint/EtherDividendCheckpoint.sol +++ b/contracts/modules/Checkpoint/EtherDividendCheckpoint.sol @@ -1,218 +1,218 @@ -pragma solidity ^0.5.0; - -import "./DividendCheckpoint.sol"; -import "../../interfaces/IOwnable.sol"; - -/** - * @title Checkpoint module for issuing ether dividends - */ -contract EtherDividendCheckpoint is DividendCheckpoint { - using SafeMath for uint256; - - event EtherDividendDeposited( - address indexed _depositor, - uint256 _checkpointId, - uint256 _created, - uint256 _maturity, - uint256 _expiry, - uint256 _amount, - uint256 _totalSupply, - uint256 _dividendIndex, - bytes32 indexed _name - ); - event EtherDividendClaimed(address indexed _payee, uint256 _dividendIndex, uint256 _amount, uint256 _withheld); - event EtherDividendReclaimed(address indexed _claimer, uint256 _dividendIndex, uint256 _claimedAmount); - event EtherDividendClaimFailed(address indexed _payee, uint256 _dividendIndex, uint256 _amount, uint256 _withheld); - event EtherDividendWithholdingWithdrawn(address indexed _claimer, uint256 _dividendIndex, uint256 _withheldAmount); - - /** - * @notice Constructor - * @param _securityToken Address of the security token - */ - constructor(address _securityToken, address _polyToken) public Module(_securityToken, _polyToken) { - - } - - /** - * @notice Creates a dividend and checkpoint for the dividend, using global list of excluded addresses - * @param _maturity Time from which dividend can be paid - * @param _expiry Time until dividend can no longer be paid, and can be reclaimed by issuer - * @param _name Name/title for identification - */ - function createDividend(uint256 _maturity, uint256 _expiry, bytes32 _name) external payable withPerm(MANAGE) { - createDividendWithExclusions(_maturity, _expiry, excluded, _name); - } - - /** - * @notice Creates a dividend with a provided checkpoint, using global list of excluded addresses - * @param _maturity Time from which dividend can be paid - * @param _expiry Time until dividend can no longer be paid, and can be reclaimed by issuer - * @param _checkpointId Id of the checkpoint from which to issue dividend - * @param _name Name/title for identification - */ - function createDividendWithCheckpoint( - uint256 _maturity, - uint256 _expiry, - uint256 _checkpointId, - bytes32 _name - ) - external - payable - withPerm(MANAGE) - { - _createDividendWithCheckpointAndExclusions(_maturity, _expiry, _checkpointId, excluded, _name); - } - - /** - * @notice Creates a dividend and checkpoint for the dividend, specifying explicit excluded addresses - * @param _maturity Time from which dividend can be paid - * @param _expiry Time until dividend can no longer be paid, and can be reclaimed by issuer - * @param _excluded List of addresses to exclude - * @param _name Name/title for identification - */ - function createDividendWithExclusions( - uint256 _maturity, - uint256 _expiry, - address[] memory _excluded, - bytes32 _name - ) - public - payable - withPerm(MANAGE) - { - uint256 checkpointId = ISecurityToken(securityToken).createCheckpoint(); - _createDividendWithCheckpointAndExclusions(_maturity, _expiry, checkpointId, _excluded, _name); - } - - /** - * @notice Creates a dividend with a provided checkpoint, specifying explicit excluded addresses - * @param _maturity Time from which dividend can be paid - * @param _expiry Time until dividend can no longer be paid, and can be reclaimed by issuer - * @param _checkpointId Id of the checkpoint from which to issue dividend - * @param _excluded List of addresses to exclude - * @param _name Name/title for identification - */ - function createDividendWithCheckpointAndExclusions( - uint256 _maturity, - uint256 _expiry, - uint256 _checkpointId, - address[] memory _excluded, - bytes32 _name - ) - public - payable - withPerm(MANAGE) - { - _createDividendWithCheckpointAndExclusions(_maturity, _expiry, _checkpointId, _excluded, _name); - } - - /** - * @notice Creates a dividend with a provided checkpoint, specifying explicit excluded addresses - * @param _maturity Time from which dividend can be paid - * @param _expiry Time until dividend can no longer be paid, and can be reclaimed by issuer - * @param _checkpointId Id of the checkpoint from which to issue dividend - * @param _excluded List of addresses to exclude - * @param _name Name/title for identification - */ - function _createDividendWithCheckpointAndExclusions( - uint256 _maturity, - uint256 _expiry, - uint256 _checkpointId, - address[] memory _excluded, - bytes32 _name - ) - internal - { - require(_excluded.length <= EXCLUDED_ADDRESS_LIMIT, "Too many addresses excluded"); - require(_expiry > _maturity, "Expiry is before maturity"); - /*solium-disable-next-line security/no-block-members*/ - require(_expiry > now, "Expiry is in the past"); - require(msg.value > 0, "No dividend sent"); - require(_checkpointId <= ISecurityToken(securityToken).currentCheckpointId()); - require(_name[0] != 0); - uint256 dividendIndex = dividends.length; - uint256 currentSupply = ISecurityToken(securityToken).totalSupplyAt(_checkpointId); - uint256 excludedSupply = 0; - dividends.push( - Dividend( - _checkpointId, - now, /*solium-disable-line security/no-block-members*/ - _maturity, - _expiry, - msg.value, - 0, - 0, - false, - 0, - 0, - _name - ) - ); - - for (uint256 j = 0; j < _excluded.length; j++) { - require(_excluded[j] != address(0), "Invalid address"); - require(!dividends[dividendIndex].dividendExcluded[_excluded[j]], "duped exclude address"); - excludedSupply = excludedSupply.add(ISecurityToken(securityToken).balanceOfAt(_excluded[j], _checkpointId)); - dividends[dividendIndex].dividendExcluded[_excluded[j]] = true; - } - dividends[dividendIndex].totalSupply = currentSupply.sub(excludedSupply); - /*solium-disable-next-line security/no-block-members*/ - emit EtherDividendDeposited(msg.sender, _checkpointId, now, _maturity, _expiry, msg.value, currentSupply, dividendIndex, _name); - } - - /** - * @notice Internal function for paying dividends - * @param _payee address of investor - * @param _dividend storage with previously issued dividends - * @param _dividendIndex Dividend to pay - */ - function _payDividend(address payable _payee, Dividend storage _dividend, uint256 _dividendIndex) internal { - (uint256 claim, uint256 withheld) = calculateDividend(_dividendIndex, _payee); - _dividend.claimed[_payee] = true; - uint256 claimAfterWithheld = claim.sub(withheld); - if (claimAfterWithheld > 0) { - /*solium-disable-next-line security/no-send*/ - if (_payee.send(claimAfterWithheld)) { - _dividend.claimedAmount = _dividend.claimedAmount.add(claim); - _dividend.dividendWithheld = _dividend.dividendWithheld.add(withheld); - investorWithheld[_payee] = investorWithheld[_payee].add(withheld); - emit EtherDividendClaimed(_payee, _dividendIndex, claim, withheld); - } else { - _dividend.claimed[_payee] = false; - emit EtherDividendClaimFailed(_payee, _dividendIndex, claim, withheld); - } - } - } - - /** - * @notice Issuer can reclaim remaining unclaimed dividend amounts, for expired dividends - * @param _dividendIndex Dividend to reclaim - */ - function reclaimDividend(uint256 _dividendIndex) external withPerm(MANAGE) { - require(_dividendIndex < dividends.length, "Incorrect dividend index"); - /*solium-disable-next-line security/no-block-members*/ - require(now >= dividends[_dividendIndex].expiry, "Dividend expiry is in the future"); - require(!dividends[_dividendIndex].reclaimed, "Dividend is already claimed"); - Dividend storage dividend = dividends[_dividendIndex]; - dividend.reclaimed = true; - uint256 remainingAmount = dividend.amount.sub(dividend.claimedAmount); - address payable owner = address(uint160(IOwnable(securityToken).owner())); - owner.transfer(remainingAmount); - emit EtherDividendReclaimed(owner, _dividendIndex, remainingAmount); - } - - /** - * @notice Allows issuer to withdraw withheld tax - * @param _dividendIndex Dividend to withdraw from - */ - function withdrawWithholding(uint256 _dividendIndex) external withPerm(MANAGE) { - require(_dividendIndex < dividends.length, "Incorrect dividend index"); - Dividend storage dividend = dividends[_dividendIndex]; - uint256 remainingWithheld = dividend.dividendWithheld.sub(dividend.dividendWithheldReclaimed); - dividend.dividendWithheldReclaimed = dividend.dividendWithheld; - address payable owner = address(uint160(IOwnable(securityToken).owner())); - owner.transfer(remainingWithheld); - emit EtherDividendWithholdingWithdrawn(owner, _dividendIndex, remainingWithheld); - } - -} +pragma solidity ^0.5.0; + +import "./DividendCheckpoint.sol"; +import "../../interfaces/IOwnable.sol"; + +/** + * @title Checkpoint module for issuing ether dividends + */ +contract EtherDividendCheckpoint is DividendCheckpoint { + using SafeMath for uint256; + + event EtherDividendDeposited( + address indexed _depositor, + uint256 _checkpointId, + uint256 _created, + uint256 _maturity, + uint256 _expiry, + uint256 _amount, + uint256 _totalSupply, + uint256 indexed _dividendIndex, + bytes32 indexed _name + ); + event EtherDividendClaimed(address indexed _payee, uint256 indexed _dividendIndex, uint256 _amount, uint256 _withheld); + event EtherDividendReclaimed(address indexed _claimer, uint256 indexed _dividendIndex, uint256 _claimedAmount); + event EtherDividendClaimFailed(address indexed _payee, uint256 indexed _dividendIndex, uint256 _amount, uint256 _withheld); + event EtherDividendWithholdingWithdrawn(address indexed _claimer, uint256 indexed _dividendIndex, uint256 _withheldAmount); + + /** + * @notice Constructor + * @param _securityToken Address of the security token + */ + constructor(address _securityToken, address _polyToken) public Module(_securityToken, _polyToken) { + + } + + /** + * @notice Creates a dividend and checkpoint for the dividend, using global list of excluded addresses + * @param _maturity Time from which dividend can be paid + * @param _expiry Time until dividend can no longer be paid, and can be reclaimed by issuer + * @param _name Name/title for identification + */ + function createDividend(uint256 _maturity, uint256 _expiry, bytes32 _name) external payable withPerm(MANAGE) { + createDividendWithExclusions(_maturity, _expiry, excluded, _name); + } + + /** + * @notice Creates a dividend with a provided checkpoint, using global list of excluded addresses + * @param _maturity Time from which dividend can be paid + * @param _expiry Time until dividend can no longer be paid, and can be reclaimed by issuer + * @param _checkpointId Id of the checkpoint from which to issue dividend + * @param _name Name/title for identification + */ + function createDividendWithCheckpoint( + uint256 _maturity, + uint256 _expiry, + uint256 _checkpointId, + bytes32 _name + ) + external + payable + withPerm(MANAGE) + { + _createDividendWithCheckpointAndExclusions(_maturity, _expiry, _checkpointId, excluded, _name); + } + + /** + * @notice Creates a dividend and checkpoint for the dividend, specifying explicit excluded addresses + * @param _maturity Time from which dividend can be paid + * @param _expiry Time until dividend can no longer be paid, and can be reclaimed by issuer + * @param _excluded List of addresses to exclude + * @param _name Name/title for identification + */ + function createDividendWithExclusions( + uint256 _maturity, + uint256 _expiry, + address[] memory _excluded, + bytes32 _name + ) + public + payable + withPerm(MANAGE) + { + uint256 checkpointId = ISecurityToken(securityToken).createCheckpoint(); + _createDividendWithCheckpointAndExclusions(_maturity, _expiry, checkpointId, _excluded, _name); + } + + /** + * @notice Creates a dividend with a provided checkpoint, specifying explicit excluded addresses + * @param _maturity Time from which dividend can be paid + * @param _expiry Time until dividend can no longer be paid, and can be reclaimed by issuer + * @param _checkpointId Id of the checkpoint from which to issue dividend + * @param _excluded List of addresses to exclude + * @param _name Name/title for identification + */ + function createDividendWithCheckpointAndExclusions( + uint256 _maturity, + uint256 _expiry, + uint256 _checkpointId, + address[] memory _excluded, + bytes32 _name + ) + public + payable + withPerm(MANAGE) + { + _createDividendWithCheckpointAndExclusions(_maturity, _expiry, _checkpointId, _excluded, _name); + } + + /** + * @notice Creates a dividend with a provided checkpoint, specifying explicit excluded addresses + * @param _maturity Time from which dividend can be paid + * @param _expiry Time until dividend can no longer be paid, and can be reclaimed by issuer + * @param _checkpointId Id of the checkpoint from which to issue dividend + * @param _excluded List of addresses to exclude + * @param _name Name/title for identification + */ + function _createDividendWithCheckpointAndExclusions( + uint256 _maturity, + uint256 _expiry, + uint256 _checkpointId, + address[] memory _excluded, + bytes32 _name + ) + internal + { + require(_excluded.length <= EXCLUDED_ADDRESS_LIMIT, "Too many addresses excluded"); + require(_expiry > _maturity, "Expiry is before maturity"); + /*solium-disable-next-line security/no-block-members*/ + require(_expiry > now, "Expiry is in the past"); + require(msg.value > 0, "No dividend sent"); + require(_checkpointId <= ISecurityToken(securityToken).currentCheckpointId()); + require(_name[0] != 0); + uint256 dividendIndex = dividends.length; + uint256 currentSupply = ISecurityToken(securityToken).totalSupplyAt(_checkpointId); + uint256 excludedSupply = 0; + dividends.push( + Dividend( + _checkpointId, + now, /*solium-disable-line security/no-block-members*/ + _maturity, + _expiry, + msg.value, + 0, + 0, + false, + 0, + 0, + _name + ) + ); + + for (uint256 j = 0; j < _excluded.length; j++) { + require(_excluded[j] != address(0), "Invalid address"); + require(!dividends[dividendIndex].dividendExcluded[_excluded[j]], "duped exclude address"); + excludedSupply = excludedSupply.add(ISecurityToken(securityToken).balanceOfAt(_excluded[j], _checkpointId)); + dividends[dividendIndex].dividendExcluded[_excluded[j]] = true; + } + dividends[dividendIndex].totalSupply = currentSupply.sub(excludedSupply); + /*solium-disable-next-line security/no-block-members*/ + emit EtherDividendDeposited(msg.sender, _checkpointId, now, _maturity, _expiry, msg.value, currentSupply, dividendIndex, _name); + } + + /** + * @notice Internal function for paying dividends + * @param _payee address of investor + * @param _dividend storage with previously issued dividends + * @param _dividendIndex Dividend to pay + */ + function _payDividend(address payable _payee, Dividend storage _dividend, uint256 _dividendIndex) internal { + (uint256 claim, uint256 withheld) = calculateDividend(_dividendIndex, _payee); + _dividend.claimed[_payee] = true; + uint256 claimAfterWithheld = claim.sub(withheld); + if (claimAfterWithheld > 0) { + /*solium-disable-next-line security/no-send*/ + if (_payee.send(claimAfterWithheld)) { + _dividend.claimedAmount = _dividend.claimedAmount.add(claim); + if (withheld > 0) { + _dividend.totalWithheld = _dividend.totalWithheld.add(withheld); + _dividend.withheld[_payee] = withheld; + } + emit EtherDividendClaimed(_payee, _dividendIndex, claim, withheld); + } else { + _dividend.claimed[_payee] = false; + emit EtherDividendClaimFailed(_payee, _dividendIndex, claim, withheld); + } + } + } + + /** + * @notice Issuer can reclaim remaining unclaimed dividend amounts, for expired dividends + * @param _dividendIndex Dividend to reclaim + */ + function reclaimDividend(uint256 _dividendIndex) external withPerm(MANAGE) { + require(_dividendIndex < dividends.length, "Incorrect dividend index"); + /*solium-disable-next-line security/no-block-members*/ + require(now >= dividends[_dividendIndex].expiry, "Dividend expiry is in the future"); + require(!dividends[_dividendIndex].reclaimed, "Dividend is already claimed"); + Dividend storage dividend = dividends[_dividendIndex]; + dividend.reclaimed = true; + uint256 remainingAmount = dividend.amount.sub(dividend.claimedAmount); + wallet.transfer(remainingAmount); + emit EtherDividendReclaimed(wallet, _dividendIndex, remainingAmount); + } + + /** + * @notice Allows issuer to withdraw withheld tax + * @param _dividendIndex Dividend to withdraw from + */ + function withdrawWithholding(uint256 _dividendIndex) external withPerm(MANAGE) { + require(_dividendIndex < dividends.length, "Incorrect dividend index"); + Dividend storage dividend = dividends[_dividendIndex]; + uint256 remainingWithheld = dividend.totalWithheld.sub(dividend.totalWithheldWithdrawn); + dividend.totalWithheldWithdrawn = dividend.totalWithheld; + wallet.transfer(remainingWithheld); + emit EtherDividendWithholdingWithdrawn(wallet, _dividendIndex, remainingWithheld); + } + +} diff --git a/contracts/modules/Checkpoint/EtherDividendCheckpointFactory.sol b/contracts/modules/Checkpoint/EtherDividendCheckpointFactory.sol index 92b1ba7f3..9802af2ee 100644 --- a/contracts/modules/Checkpoint/EtherDividendCheckpointFactory.sol +++ b/contracts/modules/Checkpoint/EtherDividendCheckpointFactory.sol @@ -1,6 +1,8 @@ pragma solidity ^0.5.0; import "../../proxy/EtherDividendCheckpointProxy.sol"; +import "../../libraries/Util.sol"; +import "../../interfaces/IBoot.sol"; import "../ModuleFactory.sol"; /** @@ -21,9 +23,9 @@ contract EtherDividendCheckpointFactory is ModuleFactory { uint256 _usageCost, uint256 _subscriptionCost, address _logicContract - ) - public - ModuleFactory(_setupCost, _usageCost, _subscriptionCost) + ) + public + ModuleFactory(_setupCost, _usageCost, _subscriptionCost) { require(_logicContract != address(0), "Invalid logic contract"); version = "2.1.0"; @@ -39,14 +41,15 @@ contract EtherDividendCheckpointFactory is ModuleFactory { * @notice Used to launch the Module with the help of factory * @return address Contract address of the Module */ - function deploy( - bytes calldata /* _data */ - ) - external - returns(address) - { + function deploy(bytes calldata _data) external returns(address) { address polyToken = _takeFee(); - address ethDividendCheckpoint = address(new EtherDividendCheckpointProxy(msg.sender, polyToken, logicContract)); + address ethDividendCheckpoint = address(new EtherDividendCheckpointProxy(msg.sender, address(polyToken), logicContract)); + //Checks that _data is valid (not calling anything it shouldn't) + require(Util.getSig(_data) == IBoot(ethDividendCheckpoint).getInitFunction(), "Invalid data"); + /*solium-disable-next-line security/no-low-level-calls*/ + bool success; + (success, ) = ethDividendCheckpoint.call(_data); + require(success, "Unsuccessful call"); /*solium-disable-next-line security/no-block-members*/ emit GenerateModuleFromFactory(ethDividendCheckpoint, getName(), address(this), msg.sender, setupCost, now); return ethDividendCheckpoint; diff --git a/contracts/modules/Experimental/TransferManager/BlacklistTransferManager.sol b/contracts/modules/Experimental/TransferManager/BlacklistTransferManager.sol new file mode 100644 index 000000000..68da9084b --- /dev/null +++ b/contracts/modules/Experimental/TransferManager/BlacklistTransferManager.sol @@ -0,0 +1,399 @@ +pragma solidity ^0.5.0; + +import "../../TransferManager/TransferManager.sol"; +import "openzeppelin-solidity/contracts/math/SafeMath.sol"; + +/** + * @title Transfer Manager module to automate blacklist and restrict transfers + */ +contract BlacklistTransferManager is TransferManager { + using SafeMath for uint256; + + bytes32 public constant ADMIN = "ADMIN"; + + struct BlacklistsDetails { + uint256 startTime; + uint256 endTime; + uint256 repeatPeriodTime; + } + + //hold the different blacklist details corresponds to its name + mapping(bytes32 => BlacklistsDetails) public blacklists; + + //hold the different name of blacklist corresponds to a investor + mapping(address => bytes32[]) investorToBlacklist; + + //get list of the addresses for a particular blacklist + mapping(bytes32 => address[]) blacklistToInvestor; + + //mapping use to store the indexes for different blacklist types for a investor + mapping(address => mapping(bytes32 => uint256)) investorToIndex; + + //mapping use to store the indexes for different investor for a blacklist type + mapping(bytes32 => mapping(address => uint256)) blacklistToIndex; + + bytes32[] allBlacklists; + + // Emit when new blacklist type is added + event AddBlacklistType( + uint256 _startTime, + uint256 _endTime, + bytes32 _blacklistName, + uint256 _repeatPeriodTime + ); + + // Emit when there is a change in the blacklist type + event ModifyBlacklistType( + uint256 _startTime, + uint256 _endTime, + bytes32 _blacklistName, + uint256 _repeatPeriodTime + ); + + // Emit when the added blacklist type is deleted + event DeleteBlacklistType( + bytes32 _blacklistName + ); + + // Emit when new investor is added to the blacklist type + event AddInvestorToBlacklist( + address indexed _investor, + bytes32 _blacklistName + ); + + // Emit when investor is deleted from the blacklist type + event DeleteInvestorFromBlacklist( + address indexed _investor, + bytes32 _blacklistName + ); + + + /** + * @notice Constructor + * @param _securityToken Address of the security token + * @param _polyAddress Address of the polytoken + */ + constructor (address _securityToken, address _polyAddress) + public + Module(_securityToken, _polyAddress) + { + } + + /** + * @notice This function returns the signature of configure function + */ + function getInitFunction() public pure returns (bytes4) { + return bytes4(0); + } + + + /** + * @notice Used to verify the transfer transaction + * @param _from Address of the sender + * @dev Restrict the blacklist address to transfer tokens + * if the current time is between the timeframe define for the + * blacklist type associated with the _from address + */ + function verifyTransfer(address _from, address /* _to */, uint256 /* _amount */, bytes memory/* _data */, bool /* _isTransfer */) public returns(Result) { + if (!paused) { + if (investorToBlacklist[_from].length != 0) { + for (uint256 i = 0; i < investorToBlacklist[_from].length; i++) { + uint256 endTimeTemp = blacklists[investorToBlacklist[_from][i]].endTime; + uint256 startTimeTemp = blacklists[investorToBlacklist[_from][i]].startTime; + uint256 repeatPeriodTimeTemp = blacklists[investorToBlacklist[_from][i]].repeatPeriodTime * 1 days; + /*solium-disable-next-line security/no-block-members*/ + if (now > startTimeTemp) { + // Find the repeating parameter that will be used to calculate the new startTime and endTime + // based on the new current time value + /*solium-disable-next-line security/no-block-members*/ + uint256 repeater = (now.sub(startTimeTemp)).div(repeatPeriodTimeTemp); + /*solium-disable-next-line security/no-block-members*/ + if (startTimeTemp.add(repeatPeriodTimeTemp.mul(repeater)) <= now && endTimeTemp.add(repeatPeriodTimeTemp.mul(repeater)) >= now) { + return Result.INVALID; + } + } + } + } + } + return Result.NA; + } + + /** + * @notice Used to add the blacklist type + * @param _startTime Start date of the blacklist type + * @param _endTime End date of the blacklist type + * @param _blacklistName Name of the blacklist type + * @param _repeatPeriodTime Repeat period of the blacklist type + */ + function addBlacklistType(uint256 _startTime, uint256 _endTime, bytes32 _blacklistName, uint256 _repeatPeriodTime) public withPerm(ADMIN) { + _addBlacklistType(_startTime, _endTime, _blacklistName, _repeatPeriodTime); + } + + /** + * @notice Used to add the multiple blacklist type + * @param _startTimes Start date of the blacklist type + * @param _endTimes End date of the blacklist type + * @param _blacklistNames Name of the blacklist type + * @param _repeatPeriodTimes Repeat period of the blacklist type + */ + function addBlacklistTypeMulti( + uint256[] calldata _startTimes, + uint256[] calldata _endTimes, + bytes32[] calldata _blacklistNames, + uint256[] calldata _repeatPeriodTimes + ) + external + withPerm(ADMIN) + { + require (_startTimes.length == _endTimes.length && _endTimes.length == _blacklistNames.length && _blacklistNames.length == _repeatPeriodTimes.length, "Input array's length mismatch"); + for (uint256 i = 0; i < _startTimes.length; i++){ + _addBlacklistType(_startTimes[i], _endTimes[i], _blacklistNames[i], _repeatPeriodTimes[i]); + } + } + + /** + * @notice Internal function + */ + function _validParams(uint256 _startTime, uint256 _endTime, bytes32 _blacklistName, uint256 _repeatPeriodTime) internal view { + require(_blacklistName != bytes32(0), "Invalid blacklist name"); + require(_startTime >= now && _startTime < _endTime, "Invalid start or end date"); + require(_repeatPeriodTime.mul(1 days) >= _endTime.sub(_startTime) || _repeatPeriodTime == 0); + } + + /** + * @notice Used to modify the details of a given blacklist type + * @param _startTime Start date of the blacklist type + * @param _endTime End date of the blacklist type + * @param _blacklistName Name of the blacklist type + * @param _repeatPeriodTime Repeat period of the blacklist type + */ + function modifyBlacklistType(uint256 _startTime, uint256 _endTime, bytes32 _blacklistName, uint256 _repeatPeriodTime) public withPerm(ADMIN) { + require(blacklists[_blacklistName].endTime != 0, "Blacklist type doesn't exist"); + _validParams(_startTime, _endTime, _blacklistName, _repeatPeriodTime); + blacklists[_blacklistName] = BlacklistsDetails(_startTime, _endTime, _repeatPeriodTime); + emit ModifyBlacklistType(_startTime, _endTime, _blacklistName, _repeatPeriodTime); + } + + /** + * @notice Used to modify the details of a given multpile blacklist types + * @param _startTimes Start date of the blacklist type + * @param _endTimes End date of the blacklist type + * @param _blacklistNames Name of the blacklist type + * @param _repeatPeriodTimes Repeat period of the blacklist type + */ + function modifyBlacklistTypeMulti( + uint256[] calldata _startTimes, + uint256[] calldata _endTimes, + bytes32[] calldata _blacklistNames, + uint256[] calldata _repeatPeriodTimes + ) + external + withPerm(ADMIN) + { + require (_startTimes.length == _endTimes.length && _endTimes.length == _blacklistNames.length && _blacklistNames.length == _repeatPeriodTimes.length, "Input array's length mismatch"); + for (uint256 i = 0; i < _startTimes.length; i++){ + modifyBlacklistType(_startTimes[i], _endTimes[i], _blacklistNames[i], _repeatPeriodTimes[i]); + } + } + + /** + * @notice Used to delete the blacklist type + * @param _blacklistName Name of the blacklist type + */ + function deleteBlacklistType(bytes32 _blacklistName) public withPerm(ADMIN) { + require(blacklists[_blacklistName].endTime != 0, "Blacklist type doesn’t exist"); + require(blacklistToInvestor[_blacklistName].length == 0, "Investors are associated with the blacklist"); + // delete blacklist type + delete(blacklists[_blacklistName]); + uint256 i = 0; + for (i = 0; i < allBlacklists.length; i++) { + if (allBlacklists[i] == _blacklistName) { + break; + } + } + if (i != allBlacklists.length -1) { + allBlacklists[i] = allBlacklists[allBlacklists.length -1]; + } + allBlacklists.length--; + emit DeleteBlacklistType(_blacklistName); + } + + /** + * @notice Used to delete the multiple blacklist type + * @param _blacklistNames Name of the blacklist type + */ + function deleteBlacklistTypeMulti(bytes32[] calldata _blacklistNames) external withPerm(ADMIN) { + for(uint256 i = 0; i < _blacklistNames.length; i++){ + deleteBlacklistType(_blacklistNames[i]); + } + } + + /** + * @notice Used to assign the blacklist type to the investor + * @param _investor Address of the investor + * @param _blacklistName Name of the blacklist + */ + function addInvestorToBlacklist(address _investor, bytes32 _blacklistName) public withPerm(ADMIN) { + require(blacklists[_blacklistName].endTime != 0, "Blacklist type doesn't exist"); + require(_investor != address(0), "Invalid investor address"); + uint256 index = investorToIndex[_investor][_blacklistName]; + if (index < investorToBlacklist[_investor].length) + require(investorToBlacklist[_investor][index] != _blacklistName, "Blacklist already added to investor"); + uint256 investorIndex = investorToBlacklist[_investor].length; + // Add blacklist index to the investor + investorToIndex[_investor][_blacklistName] = investorIndex; + uint256 blacklistIndex = blacklistToInvestor[_blacklistName].length; + // Add investor index to the blacklist + blacklistToIndex[_blacklistName][_investor] = blacklistIndex; + investorToBlacklist[_investor].push(_blacklistName); + blacklistToInvestor[_blacklistName].push(_investor); + emit AddInvestorToBlacklist(_investor, _blacklistName); + } + + /** + * @notice Used to assign the blacklist type to the multiple investor + * @param _investors Address of the investor + * @param _blacklistName Name of the blacklist + */ + function addInvestorToBlacklistMulti(address[] calldata _investors, bytes32 _blacklistName) external withPerm(ADMIN){ + for(uint256 i = 0; i < _investors.length; i++){ + addInvestorToBlacklist(_investors[i], _blacklistName); + } + } + + /** + * @notice Used to assign the multiple blacklist type to the multiple investor + * @param _investors Address of the investor + * @param _blacklistNames Name of the blacklist + */ + function addMultiInvestorToBlacklistMulti(address[] calldata _investors, bytes32[] calldata _blacklistNames) external withPerm(ADMIN){ + require (_investors.length == _blacklistNames.length, "Input array's length mismatch"); + for(uint256 i = 0; i < _investors.length; i++){ + addInvestorToBlacklist(_investors[i], _blacklistNames[i]); + } + } + + /** + * @notice Used to assign the new blacklist type to the investor + * @param _startTime Start date of the blacklist type + * @param _endTime End date of the blacklist type + * @param _blacklistName Name of the blacklist type + * @param _repeatPeriodTime Repeat period of the blacklist type + * @param _investor Address of the investor + */ + function addInvestorToNewBlacklist(uint256 _startTime, uint256 _endTime, bytes32 _blacklistName, uint256 _repeatPeriodTime, address _investor) external withPerm(ADMIN){ + _addBlacklistType(_startTime, _endTime, _blacklistName, _repeatPeriodTime); + addInvestorToBlacklist(_investor, _blacklistName); + } + + /** + * @notice Used to delete the investor from all the associated blacklist types + * @param _investor Address of the investor + */ + function deleteInvestorFromAllBlacklist(address _investor) public withPerm(ADMIN) { + require(_investor != address(0), "Invalid investor address"); + require(investorToBlacklist[_investor].length != 0, "Investor is not associated to any blacklist type"); + uint256 index = investorToBlacklist[_investor].length - 1; + for (uint256 i = index; i >= 0 && i <= index; i--){ + deleteInvestorFromBlacklist(_investor, investorToBlacklist[_investor][i]); + } + } + + /** + * @notice Used to delete the multiple investor from all the associated blacklist types + * @param _investor Address of the investor + */ + function deleteInvestorFromAllBlacklistMulti(address[] calldata _investor) external withPerm(ADMIN) { + for(uint256 i = 0; i < _investor.length; i++){ + deleteInvestorFromAllBlacklist(_investor[i]); + } + } + + /** + * @notice Used to delete the investor from the blacklist + * @param _investor Address of the investor + * @param _blacklistName Name of the blacklist + */ + function deleteInvestorFromBlacklist(address _investor, bytes32 _blacklistName) public withPerm(ADMIN) { + require(_investor != address(0), "Invalid investor address"); + require(_blacklistName != bytes32(0),"Invalid blacklist name"); + require(investorToBlacklist[_investor][investorToIndex[_investor][_blacklistName]] == _blacklistName, "Investor not associated to the blacklist"); + // delete the investor from the blacklist type + uint256 _blacklistIndex = blacklistToIndex[_blacklistName][_investor]; + uint256 _len = blacklistToInvestor[_blacklistName].length; + if ( _blacklistIndex < _len -1) { + blacklistToInvestor[_blacklistName][_blacklistIndex] = blacklistToInvestor[_blacklistName][_len - 1]; + blacklistToIndex[_blacklistName][blacklistToInvestor[_blacklistName][_blacklistIndex]] = _blacklistIndex; + } + blacklistToInvestor[_blacklistName].length--; + // delete the investor index from the blacklist + delete(blacklistToIndex[_blacklistName][_investor]); + // delete the blacklist from the investor + uint256 _investorIndex = investorToIndex[_investor][_blacklistName]; + _len = investorToBlacklist[_investor].length; + if ( _investorIndex < _len -1) { + investorToBlacklist[_investor][_investorIndex] = investorToBlacklist[_investor][_len - 1]; + investorToIndex[_investor][investorToBlacklist[_investor][_investorIndex]] = _investorIndex; + } + investorToBlacklist[_investor].length--; + // delete the blacklist index from the invetsor + delete(investorToIndex[_investor][_blacklistName]); + emit DeleteInvestorFromBlacklist(_investor, _blacklistName); + } + + /** + * @notice Used to delete the multiple investor from the blacklist + * @param _investors address of the investor + * @param _blacklistNames name of the blacklist + */ + function deleteMultiInvestorsFromBlacklistMulti(address[] calldata _investors, bytes32[] calldata _blacklistNames) external withPerm(ADMIN) { + require (_investors.length == _blacklistNames.length, "Input array's length mismatch"); + for(uint256 i = 0; i < _investors.length; i++){ + deleteInvestorFromBlacklist(_investors[i], _blacklistNames[i]); + } + } + + function _addBlacklistType(uint256 _startTime, uint256 _endTime, bytes32 _blacklistName, uint256 _repeatPeriodTime) internal { + require(blacklists[_blacklistName].endTime == 0, "Blacklist type already exist"); + _validParams(_startTime, _endTime, _blacklistName, _repeatPeriodTime); + blacklists[_blacklistName] = BlacklistsDetails(_startTime, _endTime, _repeatPeriodTime); + allBlacklists.push(_blacklistName); + emit AddBlacklistType(_startTime, _endTime, _blacklistName, _repeatPeriodTime); + } + + /** + * @notice get the list of the investors of a blacklist type + * @param _blacklistName Name of the blacklist type + * @return address List of investors associated with the blacklist + */ + function getListOfAddresses(bytes32 _blacklistName) external view returns(address[] memory) { + require(blacklists[_blacklistName].endTime != 0, "Blacklist type doesn't exist"); + return blacklistToInvestor[_blacklistName]; + } + + /** + * @notice get the list of the investors of a blacklist type + * @param _user Address of the user + * @return bytes32 List of blacklist names associated with the given address + */ + function getBlacklistNamesToUser(address _user) external view returns(bytes32[] memory) { + return investorToBlacklist[_user]; + } + + /** + * @notice get the list of blacklist names + * @return bytes32 Array of blacklist names + */ + function getAllBlacklists() external view returns(bytes32[] memory) { + return allBlacklists; + } + + /** + * @notice Return the permissions flag that are associated with blacklist transfer manager + */ + function getPermissions() public view returns(bytes32[] memory) { + bytes32[] memory allPermissions = new bytes32[](1); + allPermissions[0] = ADMIN; + return allPermissions; + } +} diff --git a/contracts/modules/Experimental/TransferManager/BlacklistTransferManagerFactory.sol b/contracts/modules/Experimental/TransferManager/BlacklistTransferManagerFactory.sol new file mode 100644 index 000000000..d193864b9 --- /dev/null +++ b/contracts/modules/Experimental/TransferManager/BlacklistTransferManagerFactory.sol @@ -0,0 +1,68 @@ +pragma solidity ^0.5.0; + +import "./BlacklistTransferManager.sol"; +import "../../ModuleFactory.sol"; +import "../../../libraries/Util.sol"; + +/** + * @title Factory for deploying BlacklistManager module + */ +contract BlacklistTransferManagerFactory is ModuleFactory { + + /** + * @notice Constructor + * @param _setupCost Setup cost of the module + * @param _usageCost Usage cost of the module + * @param _subscriptionCost Subscription cost of the module + */ + constructor (uint256 _setupCost, uint256 _usageCost, uint256 _subscriptionCost) public + ModuleFactory(_setupCost, _usageCost, _subscriptionCost) + { + version = "2.1.0"; + name = "BlacklistTransferManager"; + title = "Blacklist Transfer Manager"; + description = "Automate blacklist to restrict selling"; + compatibleSTVersionRange["lowerBound"] = VersionUtils.pack(uint8(0), uint8(0), uint8(0)); + compatibleSTVersionRange["upperBound"] = VersionUtils.pack(uint8(0), uint8(0), uint8(0)); + } + + /** + * @notice used to launch the Module with the help of factory + * @return address Contract address of the Module + */ + function deploy(bytes calldata /* _data */) external returns(address) { + address polyToken = _takeFee(); + address blacklistTransferManager = address(new BlacklistTransferManager(msg.sender, address(polyToken))); + /*solium-disable-next-line security/no-block-members*/ + emit GenerateModuleFromFactory(blacklistTransferManager, getName(), address(this), msg.sender, setupCost, now); + return blacklistTransferManager; + } + + /** + * @notice Type of the Module factory + */ + function getTypes() external view returns(uint8[] memory) { + uint8[] memory res = new uint8[](1); + res[0] = 2; + return res; + } + + /** + * @notice Get the Instructions that helped to used the module + */ + function getInstructions() public view returns(string memory) { + return "Allows an issuer to blacklist the addresses."; + } + + /** + * @notice Get the tags related to the module factory + */ + function getTags() public view returns(bytes32[] memory) { + bytes32[] memory availableTags = new bytes32[](2); + availableTags[0] = "Blacklist"; + availableTags[1] = "Restricted transfer"; + return availableTags; + } + + +} diff --git a/contracts/modules/Experimental/TransferManager/LockUpTransferManager.sol b/contracts/modules/Experimental/TransferManager/LockUpTransferManager.sol new file mode 100644 index 000000000..bcb60b149 --- /dev/null +++ b/contracts/modules/Experimental/TransferManager/LockUpTransferManager.sol @@ -0,0 +1,636 @@ +pragma solidity ^0.5.0; + +import "../../TransferManager/TransferManager.sol"; +import "openzeppelin-solidity/contracts/math/SafeMath.sol"; + +contract LockUpTransferManager is TransferManager { + + using SafeMath for uint256; + + // permission definition + bytes32 public constant ADMIN = "ADMIN"; + + // a per-user lockup + struct LockUp { + uint256 lockupAmount; // Amount to be locked + uint256 startTime; // when this lockup starts (seconds) + uint256 lockUpPeriodSeconds; // total period of lockup (seconds) + uint256 releaseFrequencySeconds; // how often to release a tranche of tokens (seconds) + } + + // mapping use to store the lockup details corresponds to lockup name + mapping (bytes32 => LockUp) public lockups; + // mapping user addresses to an array of lockups name for that user + mapping (address => bytes32[]) internal userToLockups; + // get list of the addresses for a particular lockupName + mapping (bytes32 => address[]) internal lockupToUsers; + // holds lockup index corresponds to user address. userAddress => lockupName => lockupIndex + mapping (address => mapping(bytes32 => uint256)) internal userToLockupIndex; + // holds the user address index corresponds to the lockup. lockupName => userAddress => userIndex + mapping (bytes32 => mapping(address => uint256)) internal lockupToUserIndex; + + bytes32[] lockupArray; + + event AddLockUpToUser( + address indexed _userAddress, + bytes32 indexed _lockupName + ); + + event RemoveLockUpFromUser( + address indexed _userAddress, + bytes32 indexed _lockupName + ); + + event ModifyLockUpType( + uint256 _lockupAmount, + uint256 _startTime, + uint256 _lockUpPeriodSeconds, + uint256 _releaseFrequencySeconds, + bytes32 indexed _lockupName + ); + + event AddNewLockUpType( + bytes32 indexed _lockupName, + uint256 _lockupAmount, + uint256 _startTime, + uint256 _lockUpPeriodSeconds, + uint256 _releaseFrequencySeconds + ); + + event RemoveLockUpType(bytes32 indexed _lockupName); + + /** + * @notice Constructor + * @param _securityToken Address of the security token + * @param _polyAddress Address of the polytoken + */ + constructor (address _securityToken, address _polyAddress) + public + Module(_securityToken, _polyAddress) + { + } + + /** @notice Used to verify the transfer transaction and prevent locked up tokens from being transferred + * @param _from Address of the sender + * @param _amount The amount of tokens to transfer + */ + function verifyTransfer(address _from, address /* _to*/, uint256 _amount, bytes memory /* _data */, bool /*_isTransfer*/) public returns(Result) { + // only attempt to verify the transfer if the token is unpaused, this isn't a mint txn, and there exists a lockup for this user + if (!paused && _from != address(0) && userToLockups[_from].length != 0) { + // check if this transfer is valid + return _checkIfValidTransfer(_from, _amount); + } + return Result.NA; + } + + /** + * @notice Use to add the new lockup type + * @param _lockupAmount Amount of tokens that need to lock. + * @param _startTime When this lockup starts (seconds) + * @param _lockUpPeriodSeconds Total period of lockup (seconds) + * @param _releaseFrequencySeconds How often to release a tranche of tokens (seconds) + * @param _lockupName Name of the lockup + */ + function addNewLockUpType( + uint256 _lockupAmount, + uint256 _startTime, + uint256 _lockUpPeriodSeconds, + uint256 _releaseFrequencySeconds, + bytes32 _lockupName + ) + public + withPerm(ADMIN) + { + _addNewLockUpType( + _lockupAmount, + _startTime, + _lockUpPeriodSeconds, + _releaseFrequencySeconds, + _lockupName + ); + } + + /** + * @notice Use to add the new lockup type + * @param _lockupAmounts Array of amount of tokens that need to lock. + * @param _startTimes Array of startTimes when this lockup starts (seconds) + * @param _lockUpPeriodsSeconds Array of total period of lockup (seconds) + * @param _releaseFrequenciesSeconds Array of how often to release a tranche of tokens (seconds) + * @param _lockupNames Array of names of the lockup + */ + function addNewLockUpTypeMulti( + uint256[] calldata _lockupAmounts, + uint256[] calldata _startTimes, + uint256[] calldata _lockUpPeriodsSeconds, + uint256[] calldata _releaseFrequenciesSeconds, + bytes32[] calldata _lockupNames + ) + external + withPerm(ADMIN) + { + require( + _lockupNames.length == _lockUpPeriodsSeconds.length && /*solium-disable-line operator-whitespace*/ + _lockupNames.length == _releaseFrequenciesSeconds.length && /*solium-disable-line operator-whitespace*/ + _lockupNames.length == _startTimes.length && /*solium-disable-line operator-whitespace*/ + _lockupNames.length == _lockupAmounts.length, + "Input array length mismatch" + ); + for (uint256 i = 0; i < _lockupNames.length; i++) { + _addNewLockUpType( + _lockupAmounts[i], + _startTimes[i], + _lockUpPeriodsSeconds[i], + _releaseFrequenciesSeconds[i], + _lockupNames[i] + ); + } + } + + /** + * @notice Add the lockup to a user + * @param _userAddress Address of the user + * @param _lockupName Name of the lockup + */ + function addLockUpByName( + address _userAddress, + bytes32 _lockupName + ) + public + withPerm(ADMIN) + { + _addLockUpByName(_userAddress, _lockupName); + } + + /** + * @notice Add the lockup to a user + * @param _userAddresses Address of the user + * @param _lockupNames Name of the lockup + */ + function addLockUpByNameMulti( + address[] calldata _userAddresses, + bytes32[] calldata _lockupNames + ) + external + withPerm(ADMIN) + { + require(_userAddresses.length == _lockupNames.length, "Length mismatch"); + for (uint256 i = 0; i < _userAddresses.length; i++) { + _addLockUpByName(_userAddresses[i], _lockupNames[i]); + } + } + + /** + * @notice Lets the admin create a volume restriction lockup for a given address. + * @param _userAddress Address of the user whose tokens should be locked up + * @param _lockupAmount Amount of tokens that need to lock. + * @param _startTime When this lockup starts (seconds) + * @param _lockUpPeriodSeconds Total period of lockup (seconds) + * @param _releaseFrequencySeconds How often to release a tranche of tokens (seconds) + * @param _lockupName Name of the lockup + */ + function addNewLockUpToUser( + address _userAddress, + uint256 _lockupAmount, + uint256 _startTime, + uint256 _lockUpPeriodSeconds, + uint256 _releaseFrequencySeconds, + bytes32 _lockupName + ) + external + withPerm(ADMIN) + { + _addNewLockUpToUser( + _userAddress, + _lockupAmount, + _startTime, + _lockUpPeriodSeconds, + _releaseFrequencySeconds, + _lockupName + ); + } + + /** + * @notice Lets the admin create multiple volume restriction lockups for multiple given addresses. + * @param _userAddresses Array of address of the user whose tokens should be locked up + * @param _lockupAmounts Array of the amounts that need to be locked for the different addresses. + * @param _startTimes Array of When this lockup starts (seconds) + * @param _lockUpPeriodsSeconds Array of total periods of lockup (seconds) + * @param _releaseFrequenciesSeconds Array of how often to release a tranche of tokens (seconds) + * @param _lockupNames Array of names of the lockup + */ + function addNewLockUpToUserMulti( + address[] memory _userAddresses, + uint256[] memory _lockupAmounts, + uint256[] memory _startTimes, + uint256[] memory _lockUpPeriodsSeconds, + uint256[] memory _releaseFrequenciesSeconds, + bytes32[] memory _lockupNames + ) + public + withPerm(ADMIN) + { + require( + _userAddresses.length == _lockUpPeriodsSeconds.length && /*solium-disable-line operator-whitespace*/ + _userAddresses.length == _releaseFrequenciesSeconds.length && /*solium-disable-line operator-whitespace*/ + _userAddresses.length == _startTimes.length && /*solium-disable-line operator-whitespace*/ + _userAddresses.length == _lockupAmounts.length && + _userAddresses.length == _lockupNames.length, + "Input array length mismatch" + ); + for (uint256 i = 0; i < _userAddresses.length; i++) { + _addNewLockUpToUser(_userAddresses[i], _lockupAmounts[i], _startTimes[i], _lockUpPeriodsSeconds[i], _releaseFrequenciesSeconds[i], _lockupNames[i]); + } + } + + /** + * @notice Lets the admin remove a user's lock up + * @param _userAddress Address of the user whose tokens are locked up + * @param _lockupName Name of the lockup need to be removed. + */ + function removeLockUpFromUser(address _userAddress, bytes32 _lockupName) external withPerm(ADMIN) { + _removeLockUpFromUser(_userAddress, _lockupName); + } + + /** + * @notice Used to remove the lockup type + * @param _lockupName Name of the lockup + */ + function removeLockupType(bytes32 _lockupName) external withPerm(ADMIN) { + _removeLockupType(_lockupName); + } + + /** + * @notice Used to remove the multiple lockup type + * @param _lockupNames Array of the lockup names. + */ + function removeLockupTypeMulti(bytes32[] calldata _lockupNames) external withPerm(ADMIN) { + for (uint256 i = 0; i < _lockupNames.length; i++) { + _removeLockupType(_lockupNames[i]); + } + } + + /** + * @notice Use to remove the lockup for multiple users + * @param _userAddresses Array of addresses of the user whose tokens are locked up + * @param _lockupNames Array of the names of the lockup that needs to be removed. + */ + function removeLockUpFromUserMulti(address[] calldata _userAddresses, bytes32[] calldata _lockupNames) external withPerm(ADMIN) { + require(_userAddresses.length == _lockupNames.length, "Array length mismatch"); + for (uint256 i = 0; i < _userAddresses.length; i++) { + _removeLockUpFromUser(_userAddresses[i], _lockupNames[i]); + } + } + + /** + * @notice Lets the admin modify a lockup. + * @param _lockupAmount Amount of tokens that needs to be locked + * @param _startTime When this lockup starts (seconds) + * @param _lockUpPeriodSeconds Total period of lockup (seconds) + * @param _releaseFrequencySeconds How often to release a tranche of tokens (seconds) + * @param _lockupName name of the lockup that needs to be modified. + */ + function modifyLockUpType( + uint256 _lockupAmount, + uint256 _startTime, + uint256 _lockUpPeriodSeconds, + uint256 _releaseFrequencySeconds, + bytes32 _lockupName + ) + external + withPerm(ADMIN) + { + _modifyLockUpType( + _lockupAmount, + _startTime, + _lockUpPeriodSeconds, + _releaseFrequencySeconds, + _lockupName + ); + } + + /** + * @notice Lets the admin modify a volume restriction lockup for a multiple address. + * @param _lockupAmounts Array of the amount of tokens that needs to be locked for the respective addresses. + * @param _startTimes Array of the start time of the lockups (seconds) + * @param _lockUpPeriodsSeconds Array of unix timestamp for the list of lockups (seconds). + * @param _releaseFrequenciesSeconds How often to release a tranche of tokens (seconds) + * @param _lockupNames Array of the lockup names that needs to be modified + */ + function modifyLockUpTypeMulti( + uint256[] memory _lockupAmounts, + uint256[] memory _startTimes, + uint256[] memory _lockUpPeriodsSeconds, + uint256[] memory _releaseFrequenciesSeconds, + bytes32[] memory _lockupNames + ) + public + withPerm(ADMIN) + { + require( + _lockupNames.length == _lockUpPeriodsSeconds.length && /*solium-disable-line operator-whitespace*/ + _lockupNames.length == _releaseFrequenciesSeconds.length && /*solium-disable-line operator-whitespace*/ + _lockupNames.length == _startTimes.length && /*solium-disable-line operator-whitespace*/ + _lockupNames.length == _lockupAmounts.length, + "Input array length mismatch" + ); + for (uint256 i = 0; i < _lockupNames.length; i++) { + _modifyLockUpType( + _lockupAmounts[i], + _startTimes[i], + _lockUpPeriodsSeconds[i], + _releaseFrequenciesSeconds[i], + _lockupNames[i] + ); + } + } + + /** + * @notice Get a specific element in a user's lockups array given the user's address and the element index + * @param _lockupName The name of the lockup + */ + function getLockUp(bytes32 _lockupName) external view returns ( + uint256 lockupAmount, + uint256 startTime, + uint256 lockUpPeriodSeconds, + uint256 releaseFrequencySeconds, + uint256 unlockedAmount + ) { + if (lockups[_lockupName].lockupAmount != 0) { + return ( + lockups[_lockupName].lockupAmount, + lockups[_lockupName].startTime, + lockups[_lockupName].lockUpPeriodSeconds, + lockups[_lockupName].releaseFrequencySeconds, + _getUnlockedAmountForLockup(_lockupName) + ); + } + return (uint256(0), uint256(0), uint256(0), uint256(0), uint256(0)); + } + + /** + * @notice get the list of the users of a lockup type + * @param _lockupName Name of the lockup type + * @return address List of users associated with the blacklist + */ + function getListOfAddresses(bytes32 _lockupName) external view returns(address[] memory) { + require(lockups[_lockupName].startTime != 0, "Blacklist type doesn't exist"); + return lockupToUsers[_lockupName]; + } + + /** + * @notice get the list of lockups names + * @return bytes32 Array of lockups names + */ + function getAllLockups() external view returns(bytes32[] memory) { + return lockupArray; + } + + /** + * @notice get the list of the lockups for a given user + * @param _user Address of the user + * @return bytes32 List of lockups names associated with the given address + */ + function getLockupsNamesToUser(address _user) external view returns(bytes32[] memory) { + return userToLockups[_user]; + } + + /** + * @notice Use to get the total locked tokens for a given user + * @param _userAddress Address of the user + * @return uint256 Total locked tokens amount + */ + function getLockedTokenToUser(address _userAddress) public view returns(uint256) { + require(_userAddress != address(0), "Invalid address"); + bytes32[] memory userLockupNames = userToLockups[_userAddress]; + uint256 totalRemainingLockedAmount = 0; + + for (uint256 i = 0; i < userLockupNames.length; i++) { + // Find out the remaining locked amount for a given lockup + uint256 remainingLockedAmount = lockups[userLockupNames[i]].lockupAmount.sub(_getUnlockedAmountForLockup(userLockupNames[i])); + // aggregating all the remaining locked amount for all the lockups for a given address + totalRemainingLockedAmount = totalRemainingLockedAmount.add(remainingLockedAmount); + } + return totalRemainingLockedAmount; + } + + /** + * @notice Checks whether the transfer is allowed + * @param _userAddress Address of the user whose lock ups should be checked + * @param _amount Amount of tokens that need to transact + */ + function _checkIfValidTransfer(address _userAddress, uint256 _amount) internal view returns (Result) { + uint256 totalRemainingLockedAmount = getLockedTokenToUser(_userAddress); + // Present balance of the user + uint256 currentBalance = IERC20(securityToken).balanceOf(_userAddress); + if ((currentBalance.sub(_amount)) >= totalRemainingLockedAmount) { + return Result.NA; + } + return Result.INVALID; + } + + /** + * @notice Provide the unlock amount for the given lockup for a particular user + */ + function _getUnlockedAmountForLockup(bytes32 _lockupName) internal view returns (uint256) { + /*solium-disable-next-line security/no-block-members*/ + if (lockups[_lockupName].startTime > now) { + return 0; + } else if (lockups[_lockupName].startTime.add(lockups[_lockupName].lockUpPeriodSeconds) <= now) { + return lockups[_lockupName].lockupAmount; + } else { + // Calculate the no. of periods for a lockup + uint256 noOfPeriods = (lockups[_lockupName].lockUpPeriodSeconds).div(lockups[_lockupName].releaseFrequencySeconds); + // Calculate the transaction time lies in which period + /*solium-disable-next-line security/no-block-members*/ + uint256 elapsedPeriod = (now.sub(lockups[_lockupName].startTime)).div(lockups[_lockupName].releaseFrequencySeconds); + // Find out the unlocked amount for a given lockup + uint256 unLockedAmount = (lockups[_lockupName].lockupAmount.mul(elapsedPeriod)).div(noOfPeriods); + return unLockedAmount; + } + } + + function _removeLockupType(bytes32 _lockupName) internal { + require(lockups[_lockupName].startTime != 0, "Lockup type doesn’t exist"); + require(lockupToUsers[_lockupName].length == 0, "Users are associated with the lockup"); + // delete lockup type + delete(lockups[_lockupName]); + uint256 i = 0; + for (i = 0; i < lockupArray.length; i++) { + if (lockupArray[i] == _lockupName) { + break; + } + } + if (i != lockupArray.length -1) { + lockupArray[i] = lockupArray[lockupArray.length -1]; + } + lockupArray.length--; + emit RemoveLockUpType(_lockupName); + } + + function _modifyLockUpType( + uint256 _lockupAmount, + uint256 _startTime, + uint256 _lockUpPeriodSeconds, + uint256 _releaseFrequencySeconds, + bytes32 _lockupName + ) + internal + { + /*solium-disable-next-line security/no-block-members*/ + uint256 startTime = _startTime; + + if (_startTime == 0) { + startTime = now; + } + require(startTime >= now, "Invalid start time"); + require(lockups[_lockupName].lockupAmount != 0, "Doesn't exist"); + + _checkLockUpParams( + _lockupAmount, + _lockUpPeriodSeconds, + _releaseFrequencySeconds + ); + + lockups[_lockupName] = LockUp( + _lockupAmount, + startTime, + _lockUpPeriodSeconds, + _releaseFrequencySeconds + ); + + emit ModifyLockUpType( + _lockupAmount, + startTime, + _lockUpPeriodSeconds, + _releaseFrequencySeconds, + _lockupName + ); + } + + function _removeLockUpFromUser(address _userAddress, bytes32 _lockupName) internal { + require(_userAddress != address(0), "Invalid address"); + require(_lockupName != bytes32(0), "Invalid lockup name"); + require( + userToLockups[_userAddress][userToLockupIndex[_userAddress][_lockupName]] == _lockupName, + "User not assosicated with given lockup" + ); + + // delete the user from the lockup type + uint256 _lockupIndex = lockupToUserIndex[_lockupName][_userAddress]; + uint256 _len = lockupToUsers[_lockupName].length; + if ( _lockupIndex != _len) { + lockupToUsers[_lockupName][_lockupIndex] = lockupToUsers[_lockupName][_len - 1]; + lockupToUserIndex[_lockupName][lockupToUsers[_lockupName][_lockupIndex]] = _lockupIndex; + } + lockupToUsers[_lockupName].length--; + // delete the user index from the lockup + delete(lockupToUserIndex[_lockupName][_userAddress]); + // delete the lockup from the user + uint256 _userIndex = userToLockupIndex[_userAddress][_lockupName]; + _len = userToLockups[_userAddress].length; + if ( _userIndex != _len) { + userToLockups[_userAddress][_userIndex] = userToLockups[_userAddress][_len - 1]; + userToLockupIndex[_userAddress][userToLockups[_userAddress][_userIndex]] = _userIndex; + } + userToLockups[_userAddress].length--; + // delete the lockup index from the user + delete(userToLockupIndex[_userAddress][_lockupName]); + emit RemoveLockUpFromUser(_userAddress, _lockupName); + } + + function _addNewLockUpToUser( + address _userAddress, + uint256 _lockupAmount, + uint256 _startTime, + uint256 _lockUpPeriodSeconds, + uint256 _releaseFrequencySeconds, + bytes32 _lockupName + ) + internal + { + require(_userAddress != address(0), "Invalid address"); + _addNewLockUpType( + _lockupAmount, + _startTime, + _lockUpPeriodSeconds, + _releaseFrequencySeconds, + _lockupName + ); + _addLockUpByName(_userAddress, _lockupName); + } + + function _addLockUpByName( + address _userAddress, + bytes32 _lockupName + ) + internal + { + require(_userAddress != address(0), "Invalid address"); + require(lockups[_lockupName].startTime >= now, "Lockup expired"); + + userToLockupIndex[_userAddress][_lockupName] = userToLockups[_userAddress].length; + lockupToUserIndex[_lockupName][_userAddress] = lockupToUsers[_lockupName].length; + userToLockups[_userAddress].push(_lockupName); + lockupToUsers[_lockupName].push(_userAddress); + emit AddLockUpToUser(_userAddress, _lockupName); + } + + function _addNewLockUpType( + uint256 _lockupAmount, + uint256 _startTime, + uint256 _lockUpPeriodSeconds, + uint256 _releaseFrequencySeconds, + bytes32 _lockupName + ) + internal + { + uint256 startTime = _startTime; + require(_lockupName != bytes32(0), "Invalid name"); + require(lockups[_lockupName].lockupAmount == 0, "Already exist"); + /*solium-disable-next-line security/no-block-members*/ + if (_startTime == 0) { + startTime = now; + } + require(startTime >= now, "Invalid start time"); + _checkLockUpParams(_lockupAmount, _lockUpPeriodSeconds, _releaseFrequencySeconds); + lockups[_lockupName] = LockUp(_lockupAmount, startTime, _lockUpPeriodSeconds, _releaseFrequencySeconds); + lockupArray.push(_lockupName); + emit AddNewLockUpType(_lockupName, _lockupAmount, startTime, _lockUpPeriodSeconds, _releaseFrequencySeconds); + } + + /** + * @notice Parameter checking function for creating or editing a lockup. + * This function will cause an exception if any of the parameters are bad. + * @param _lockupAmount Amount that needs to be locked + * @param _lockUpPeriodSeconds Total period of lockup (seconds) + * @param _releaseFrequencySeconds How often to release a tranche of tokens (seconds) + */ + function _checkLockUpParams( + uint256 _lockupAmount, + uint256 _lockUpPeriodSeconds, + uint256 _releaseFrequencySeconds + ) + internal + pure + { + require(_lockUpPeriodSeconds != 0, "lockUpPeriodSeconds cannot be zero"); + require(_releaseFrequencySeconds != 0, "releaseFrequencySeconds cannot be zero"); + require(_lockupAmount != 0, "lockupAmount cannot be zero"); + } + + /** + * @notice This function returns the signature of configure function + */ + function getInitFunction() public pure returns (bytes4) { + return bytes4(0); + } + + /** + * @notice Returns the permissions flag that are associated with Percentage transfer Manager + */ + function getPermissions() public view returns(bytes32[] memory) { + bytes32[] memory allPermissions = new bytes32[](1); + allPermissions[0] = ADMIN; + return allPermissions; + } +} diff --git a/contracts/modules/Experimental/TransferManager/LockupVolumeRestrictionTMFactory.sol b/contracts/modules/Experimental/TransferManager/LockUpTransferManagerFactory.sol similarity index 71% rename from contracts/modules/Experimental/TransferManager/LockupVolumeRestrictionTMFactory.sol rename to contracts/modules/Experimental/TransferManager/LockUpTransferManagerFactory.sol index a677cccdf..846133da6 100644 --- a/contracts/modules/Experimental/TransferManager/LockupVolumeRestrictionTMFactory.sol +++ b/contracts/modules/Experimental/TransferManager/LockUpTransferManagerFactory.sol @@ -1,12 +1,13 @@ pragma solidity ^0.5.0; -import "./../../ModuleFactory.sol"; -import "./LockupVolumeRestrictionTM.sol"; +import "../../ModuleFactory.sol"; +import "./LockUpTransferManager.sol"; /** - * @title Factory for deploying ManualApprovalTransferManager module + * @title Factory for deploying LockUpTransferManager module */ -contract LockupVolumeRestrictionTMFactory is ModuleFactory { +contract LockUpTransferManagerFactory is ModuleFactory { + /** * @notice Constructor * @param _setupCost Setup cost of the module @@ -17,13 +18,13 @@ contract LockupVolumeRestrictionTMFactory is ModuleFactory { uint256 _setupCost, uint256 _usageCost, uint256 _subscriptionCost - ) - public - ModuleFactory(_setupCost, _usageCost, _subscriptionCost) + ) + public + ModuleFactory(_setupCost, _usageCost, _subscriptionCost) { version = "1.0.0"; - name = "LockupVolumeRestrictionTM"; - title = "Lockup Volume Restriction Transfer Manager"; + name = "LockUpTransferManager"; + title = "LockUp Transfer Manager"; description = "Manage transfers using lock ups over time"; compatibleSTVersionRange["lowerBound"] = VersionUtils.pack(uint8(0), uint8(0), uint8(0)); compatibleSTVersionRange["upperBound"] = VersionUtils.pack(uint8(0), uint8(0), uint8(0)); @@ -35,15 +36,15 @@ contract LockupVolumeRestrictionTMFactory is ModuleFactory { */ function deploy( bytes calldata /* _data */ - ) - external - returns(address) + ) + external + returns(address) { address polyToken = _takeFee(); - LockupVolumeRestrictionTM lockupVolumeRestrictionTransferManager = new LockupVolumeRestrictionTM(msg.sender, polyToken); + LockUpTransferManager lockUpTransferManager = new LockUpTransferManager(msg.sender, polyToken); /*solium-disable-next-line security/no-block-members*/ - emit GenerateModuleFromFactory(address(lockupVolumeRestrictionTransferManager), getName(), address(this), msg.sender, now); - return address(lockupVolumeRestrictionTransferManager); + emit GenerateModuleFromFactory(address(lockUpTransferManager), getName(), address(this), msg.sender, setupCost, now); + return address(lockUpTransferManager); } /** @@ -68,7 +69,7 @@ contract LockupVolumeRestrictionTMFactory is ModuleFactory { */ function getTags() external view returns(bytes32[] memory) { bytes32[] memory availableTags = new bytes32[](2); - availableTags[0] = "Volume"; + availableTags[0] = "LockUp"; availableTags[1] = "Transfer Restriction"; return availableTags; } diff --git a/contracts/modules/Experimental/TransferManager/LockupVolumeRestrictionTM.sol b/contracts/modules/Experimental/TransferManager/LockupVolumeRestrictionTM.sol deleted file mode 100644 index 5d0ac2307..000000000 --- a/contracts/modules/Experimental/TransferManager/LockupVolumeRestrictionTM.sol +++ /dev/null @@ -1,403 +0,0 @@ -pragma solidity ^0.5.0; - -import "./../../TransferManager/TransferManager.sol"; -import "openzeppelin-solidity/contracts/math/SafeMath.sol"; - -contract LockupVolumeRestrictionTM is TransferManager { - using SafeMath for uint256; - - // permission definition - bytes32 public constant ADMIN = "ADMIN"; - - // a per-user lockup - struct LockUp { - uint lockUpPeriodSeconds; // total period of lockup (seconds) - uint releaseFrequencySeconds; // how often to release a tranche of tokens (seconds) - uint startTime; // when this lockup starts (seconds) - uint totalAmount; // total amount of locked up tokens - uint alreadyWithdrawn; // amount already withdrawn for this lockup - } - - // maps user addresses to an array of lockups for that user - mapping(address => LockUp[]) internal lockUps; - - event AddNewLockUp( - address indexed userAddress, - uint lockUpPeriodSeconds, - uint releaseFrequencySeconds, - uint startTime, - uint totalAmount, - uint indexed addedIndex - ); - - event RemoveLockUp( - address indexed userAddress, - uint lockUpPeriodSeconds, - uint releaseFrequencySeconds, - uint startTime, - uint totalAmount, - uint indexed removedIndex - ); - - event ModifyLockUp( - address indexed userAddress, - uint lockUpPeriodSeconds, - uint releaseFrequencySeconds, - uint startTime, - uint totalAmount, - uint indexed modifiedIndex - ); - - /** - * @notice Constructor - * @param _securityToken Address of the security token - */ - constructor(address _securityToken, address _polyToken) public Module(_securityToken, _polyToken) { - - } - - /** @notice Used to verify the transfer transaction and prevent locked up tokens from being transferred - * @param _from Address of the sender - * @param _amount The amount of tokens to transfer - * @param _isTransfer Whether or not this is an actual transfer or just a test to see if the tokens would be transferrable - */ - function verifyTransfer( - address _from, - address, /* _to*/ - uint256 _amount, - bytes calldata, /* _data */ - bool _isTransfer - ) - external - returns(Result) - { - // only attempt to verify the transfer if the token is unpaused, this isn't a mint txn, and there exists a lockup for this user - if (!paused && _from != address(0) && lockUps[_from].length != 0) { - // check if this transfer is valid - return _checkIfValidTransfer(_from, _amount, _isTransfer); - } - return Result.NA; - } - - /** - * @notice Lets the admin create a volume restriction lockup for a given address. - * @param _userAddress Address of the user whose tokens should be locked up - * @param _lockUpPeriodSeconds Total period of lockup (seconds) - * @param _releaseFrequencySeconds How often to release a tranche of tokens (seconds) - * @param _startTime When this lockup starts (seconds) - * @param _totalAmount Total amount of locked up tokens - */ - function addLockUp( - address _userAddress, - uint _lockUpPeriodSeconds, - uint _releaseFrequencySeconds, - uint _startTime, - uint _totalAmount - ) - public - withPerm(ADMIN) - { - uint256 startTime = _startTime; - _checkLockUpParams(_lockUpPeriodSeconds, _releaseFrequencySeconds, _totalAmount); - - // if a startTime of 0 is passed in, then start now. - if (startTime == 0) { - /*solium-disable-next-line security/no-block-members*/ - startTime = now; - } - - lockUps[_userAddress].push(LockUp(_lockUpPeriodSeconds, _releaseFrequencySeconds, startTime, _totalAmount, 0)); - - emit AddNewLockUp( - _userAddress, - _lockUpPeriodSeconds, - _releaseFrequencySeconds, - startTime, - _totalAmount, - lockUps[_userAddress].length - 1 - ); - } - - /** - * @notice Lets the admin create multiple volume restriction lockups for multiple given addresses. - * @param _userAddresses Array of address of the user whose tokens should be locked up - * @param _lockUpPeriodsSeconds Array of total periods of lockup (seconds) - * @param _releaseFrequenciesSeconds Array of how often to release a tranche of tokens (seconds) - * @param _startTimes Array of When this lockup starts (seconds) - * @param _totalAmounts Array of total amount of locked up tokens - */ - function addLockUpMulti( - address[] calldata _userAddresses, - uint[] calldata _lockUpPeriodsSeconds, - uint[] calldata _releaseFrequenciesSeconds, - uint[] calldata _startTimes, - uint[] calldata _totalAmounts - ) - external - withPerm(ADMIN) - { - require( - _userAddresses.length == _lockUpPeriodsSeconds.length && _userAddresses.length == _releaseFrequenciesSeconds.length && _userAddresses.length == _startTimes.length && _userAddresses.length == _totalAmounts.length, /*solium-disable-line operator-whitespace*/ /*solium-disable-line operator-whitespace*/ - "Input array length mismatch" - ); - - for (uint i = 0; i < _userAddresses.length; i++) { - addLockUp(_userAddresses[i], _lockUpPeriodsSeconds[i], _releaseFrequenciesSeconds[i], _startTimes[i], _totalAmounts[i]); - } - - } - - /** - * @notice Lets the admin remove a user's lock up - * @param _userAddress Address of the user whose tokens are locked up - * @param _lockUpIndex The index of the LockUp to remove for the given userAddress - */ - function removeLockUp(address _userAddress, uint _lockUpIndex) public withPerm(ADMIN) { - LockUp[] storage userLockUps = lockUps[_userAddress]; - require(_lockUpIndex < userLockUps.length, "Array out of bounds exception"); - - LockUp memory toRemove = userLockUps[_lockUpIndex]; - - emit RemoveLockUp( - _userAddress, - toRemove.lockUpPeriodSeconds, - toRemove.releaseFrequencySeconds, - toRemove.startTime, - toRemove.totalAmount, - _lockUpIndex - ); - - if (_lockUpIndex < userLockUps.length - 1) { - // move the last element in the array into the index that is desired to be removed. - userLockUps[_lockUpIndex] = userLockUps[userLockUps.length - 1]; - } - // delete the last element - userLockUps.length--; - } - - /** - * @notice Lets the admin modify a volume restriction lockup for a given address. - * @param _userAddress Address of the user whose tokens should be locked up - * @param _lockUpIndex The index of the LockUp to edit for the given userAddress - * @param _lockUpPeriodSeconds Total period of lockup (seconds) - * @param _releaseFrequencySeconds How often to release a tranche of tokens (seconds) - * @param _startTime When this lockup starts (seconds) - * @param _totalAmount Total amount of locked up tokens - */ - function modifyLockUp( - address _userAddress, - uint _lockUpIndex, - uint _lockUpPeriodSeconds, - uint _releaseFrequencySeconds, - uint _startTime, - uint _totalAmount - ) - public - withPerm(ADMIN) - { - require(_lockUpIndex < lockUps[_userAddress].length, "Array out of bounds exception"); - - uint256 startTime = _startTime; - // if a startTime of 0 is passed in, then start now. - if (startTime == 0) { - /*solium-disable-next-line security/no-block-members*/ - startTime = now; - } - - _checkLockUpParams(_lockUpPeriodSeconds, _releaseFrequencySeconds, _totalAmount); - - // Get the lockup from the master list and edit it - lockUps[_userAddress][_lockUpIndex] = LockUp( - _lockUpPeriodSeconds, - _releaseFrequencySeconds, - startTime, - _totalAmount, - lockUps[_userAddress][_lockUpIndex].alreadyWithdrawn - ); - - emit ModifyLockUp(_userAddress, _lockUpPeriodSeconds, _releaseFrequencySeconds, startTime, _totalAmount, _lockUpIndex); - } - - /** - * @notice Get the length of the lockups array for a specific user address - * @param _userAddress Address of the user whose tokens should be locked up - */ - function getLockUpsLength(address _userAddress) public view returns(uint) { - return lockUps[_userAddress].length; - } - - /** - * @notice Get a specific element in a user's lockups array given the user's address and the element index - * @param _userAddress Address of the user whose tokens should be locked up - * @param _lockUpIndex The index of the LockUp to edit for the given userAddress - */ - function getLockUp(address _userAddress, uint _lockUpIndex) public view returns( - uint lockUpPeriodSeconds, - uint releaseFrequencySeconds, - uint startTime, - uint totalAmount, - uint alreadyWithdrawn - ) { - require(_lockUpIndex < lockUps[_userAddress].length, "Array out of bounds exception"); - LockUp storage userLockUp = lockUps[_userAddress][_lockUpIndex]; - return (userLockUp.lockUpPeriodSeconds, userLockUp.releaseFrequencySeconds, userLockUp.startTime, userLockUp.totalAmount, userLockUp.alreadyWithdrawn); - } - - /** - * @notice Takes a userAddress as input, and returns a uint that represents the number of tokens allowed to be withdrawn right now - * @param userAddress Address of the user whose lock ups should be checked - */ - function _checkIfValidTransfer(address userAddress, uint amount, bool isTransfer) internal returns(Result) { - // get lock up array for this user - LockUp[] storage userLockUps = lockUps[userAddress]; - - // maps the index of userLockUps to the amount allowed in this transfer - uint[] memory allowedAmountPerLockup = new uint[](userLockUps.length); - - uint[3] memory tokenSums = [uint256(0), uint256(0), uint256(0)]; // allowed amount right now // total locked up, ever // already withdrawn, ever - - // loop over the user's lock ups - for (uint i = 0; i < userLockUps.length; i++) { - LockUp storage aLockUp = userLockUps[i]; - - uint allowedAmountForThisLockup = 0; - - // check if lockup has entirely passed - /*solium-disable-next-line security/no-block-members*/ - if (now >= aLockUp.startTime.add(aLockUp.lockUpPeriodSeconds)) { - // lockup has passed, or not started yet. allow all. - allowedAmountForThisLockup = aLockUp.totalAmount.sub(aLockUp.alreadyWithdrawn); - /*solium-disable-next-line security/no-block-members*/ - } else if (now >= aLockUp.startTime) { - // lockup is active. calculate how many to allow to be withdrawn right now - // calculate how many periods have elapsed already - /*solium-disable-next-line security/no-block-members*/ - uint elapsedPeriods = (now.sub(aLockUp.startTime)).div(aLockUp.releaseFrequencySeconds); - // calculate the total number of periods, overall - uint totalPeriods = aLockUp.lockUpPeriodSeconds.div(aLockUp.releaseFrequencySeconds); - // calculate how much should be released per period - uint amountPerPeriod = aLockUp.totalAmount.div(totalPeriods); - // calculate the number of tokens that should be released, - // multiplied by the number of periods that have elapsed already - // and add it to the total tokenSums[0] - allowedAmountForThisLockup = amountPerPeriod.mul(elapsedPeriods).sub(aLockUp.alreadyWithdrawn); - - } - // tokenSums[0] is allowed sum - tokenSums[0] = tokenSums[0].add(allowedAmountForThisLockup); - // tokenSums[1] is total locked up - tokenSums[1] = tokenSums[1].add(aLockUp.totalAmount); - // tokenSums[2] is total already withdrawn - tokenSums[2] = tokenSums[2].add(aLockUp.alreadyWithdrawn); - - allowedAmountPerLockup[i] = allowedAmountForThisLockup; - } - - // tokenSums[0] is allowed sum - if (amount <= tokenSums[0]) { - // transfer is valid and will succeed. - if (!isTransfer) { - // if this isn't a real transfer, don't subtract the withdrawn amounts from the lockups. it's a "read only" txn - return Result.VALID; - } - - // we are going to write the withdrawn balances back to the lockups, so make sure that the person calling this function is the securityToken itself, since its public - require(msg.sender == securityToken, "Sender is not securityToken"); - - // subtract amounts so they are now known to be withdrawen - for (uint256 i = 0; i < userLockUps.length; i++) { - LockUp storage aLockUp = userLockUps[i]; - - // tokenSums[0] is allowed sum - if (allowedAmountPerLockup[i] >= tokenSums[0]) { - aLockUp.alreadyWithdrawn = aLockUp.alreadyWithdrawn.add(tokenSums[0]); - // we withdrew the entire tokenSums[0] from the lockup. We are done. - break; - } else { - // we have to split the tokenSums[0] across mutiple lockUps - aLockUp.alreadyWithdrawn = aLockUp.alreadyWithdrawn.add(allowedAmountPerLockup[i]); - // subtract the amount withdrawn from this lockup - tokenSums[0] = tokenSums[0].sub(allowedAmountPerLockup[i]); - } - - } - return Result.VALID; - } - - return _checkIfUnlockedTokenTransferIsPossible(userAddress, amount, tokenSums[1], tokenSums[2]); - } - - function _checkIfUnlockedTokenTransferIsPossible( - address userAddress, - uint amount, - uint totalSum, - uint alreadyWithdrawnSum - ) - internal - view - returns(Result) - { - // the amount the user wants to withdraw is greater than their allowed amounts according to the lockups. however, if the user has like, 10 tokens, but only 4 are locked up, we should let the transfer go through for those 6 that aren't locked up - uint currentUserBalance = ISecurityToken(securityToken).balanceOf(userAddress); - uint stillLockedAmount = totalSum.sub(alreadyWithdrawnSum); - if (currentUserBalance >= stillLockedAmount && amount <= currentUserBalance.sub(stillLockedAmount)) { - // the user has more tokens in their balance than are actually locked up. they should be allowed to withdraw the difference - return Result.VALID; - } - return Result.INVALID; - } - - /** - * @notice Parameter checking function for creating or editing a lockup. This function will cause an exception if any of the parameters are bad. - * @param lockUpPeriodSeconds Total period of lockup (seconds) - * @param releaseFrequencySeconds How often to release a tranche of tokens (seconds) - * @param totalAmount Total amount of locked up tokens - */ - function _checkLockUpParams(uint lockUpPeriodSeconds, uint releaseFrequencySeconds, uint totalAmount) internal view { - require(lockUpPeriodSeconds != 0, "lockUpPeriodSeconds cannot be zero"); - require(releaseFrequencySeconds != 0, "releaseFrequencySeconds cannot be zero"); - require(totalAmount != 0, "totalAmount cannot be zero"); - - // check that the total amount to be released isn't too granular - require( - totalAmount % ISecurityToken(securityToken).granularity() == 0, - "The total amount to be released is more granular than allowed by the token" - ); - - // check that releaseFrequencySeconds evenly divides lockUpPeriodSeconds - require( - lockUpPeriodSeconds % releaseFrequencySeconds == 0, - "lockUpPeriodSeconds must be evenly divisible by releaseFrequencySeconds" - ); - - // check that totalPeriods evenly divides totalAmount - uint totalPeriods = lockUpPeriodSeconds.div(releaseFrequencySeconds); - require( - totalAmount % totalPeriods == 0, - "The total amount being locked up must be evenly divisible by the number of total periods" - ); - - // make sure the amount to be released per period is not too granular for the token - uint amountPerPeriod = totalAmount.div(totalPeriods); - require( - amountPerPeriod % ISecurityToken(securityToken).granularity() == 0, - "The amount to be released per period is more granular than allowed by the token" - ); - } - - /** - * @notice This function returns the signature of configure function - */ - function getInitFunction() public pure returns(bytes4) { - return bytes4(0); - } - - /** - * @notice Returns the permissions flag that are associated with Percentage transfer Manager - */ - function getPermissions() public view returns(bytes32[] memory) { - bytes32[] memory allPermissions = new bytes32[](1); - allPermissions[0] = ADMIN; - return allPermissions; - } -} diff --git a/contracts/modules/Experimental/TransferManager/SingleTradeVolumeRestrictionTM.sol b/contracts/modules/Experimental/TransferManager/SingleTradeVolumeRestrictionTM.sol deleted file mode 100644 index 431408336..000000000 --- a/contracts/modules/Experimental/TransferManager/SingleTradeVolumeRestrictionTM.sol +++ /dev/null @@ -1,338 +0,0 @@ -pragma solidity ^0.5.0; - -import "./../../TransferManager/TransferManager.sol"; -import "openzeppelin-solidity/contracts/math/SafeMath.sol"; - -/** - * @title Transfer Manager for limiting volume of tokens in a single trade - */ - -contract SingleTradeVolumeRestrictionTM is TransferManager { - using SafeMath for uint256; - - bytes32 public constant ADMIN = "ADMIN"; - - bool public isTransferLimitInPercentage; - - uint256 public globalTransferLimitInTokens; - - // should be multipled by 10^16. if the transfer percentage is 20%, then globalTransferLimitInPercentage should be 20*10^16 - uint256 public globalTransferLimitInPercentage; - - // Ignore transactions which are part of the primary issuance - bool public allowPrimaryIssuance = true; - - //mapping to store the wallets that are exempted from the volume restriction - mapping(address => bool) public exemptWallets; - - //addresses on this list have special transfer restrictions apart from global - mapping(address => uint) public specialTransferLimitsInTokens; - - mapping(address => uint) public specialTransferLimitsInPercentages; - - event ExemptWalletAdded(address _wallet); - event ExemptWalletRemoved(address _wallet); - event TransferLimitInTokensSet(address _wallet, uint256 _amount); - event TransferLimitInPercentageSet(address _wallet, uint _percentage); - event TransferLimitInPercentageRemoved(address _wallet); - event TransferLimitInTokensRemoved(address _wallet); - event GlobalTransferLimitInTokensSet(uint256 _amount, uint256 _oldAmount); - event GlobalTransferLimitInPercentageSet(uint256 _percentage, uint256 _oldPercentage); - event TransferLimitChangedToTokens(); - event TransferLimitChangedtoPercentage(); - event SetAllowPrimaryIssuance(bool _allowPrimaryIssuance, uint256 _timestamp); - - /** - * @notice Constructor - * @param _securityToken Address of the security token - */ - constructor(address _securityToken, address _polyToken) public Module(_securityToken, _polyToken) { - - } - - /** @notice Used to verify the transfer transaction and prevent an account from sending more tokens than allowed in a single transfer - * @param _from Address of the sender - * @param _amount The amount of tokens to transfer - */ - function verifyTransfer( - address _from, - address /* _to */, - uint256 _amount, - bytes calldata /* _data */, - bool /* _isTransfer */ - ) - external - returns(Result) - { - bool validTransfer; - - if (exemptWallets[_from] || paused) return Result.NA; - - if (_from == address(0) && allowPrimaryIssuance) { - return Result.NA; - } - - if (isTransferLimitInPercentage) { - if (specialTransferLimitsInPercentages[_from] > 0) { - validTransfer = (_amount.mul(10 ** 18).div( - ISecurityToken(securityToken).totalSupply() - )) <= specialTransferLimitsInPercentages[_from]; - } else { - validTransfer = (_amount.mul(10 ** 18).div(ISecurityToken(securityToken).totalSupply())) <= globalTransferLimitInPercentage; - } - } else { - if (specialTransferLimitsInTokens[_from] > 0) { - validTransfer = _amount <= specialTransferLimitsInTokens[_from]; - } else { - validTransfer = _amount <= globalTransferLimitInTokens; - } - } - if (validTransfer) return Result.NA; - return Result.INVALID; - } - - /** - * @notice Used to intialize the variables of the contract - * @param _isTransferLimitInPercentage true if the transfer limit is in percentage else false - * @param _globalTransferLimitInPercentageOrToken transfer limit per single transaction. - */ - function configure( - bool _isTransferLimitInPercentage, - uint256 _globalTransferLimitInPercentageOrToken, - bool _allowPrimaryIssuance - ) - public - onlyFactory - { - isTransferLimitInPercentage = _isTransferLimitInPercentage; - if (isTransferLimitInPercentage) { - changeGlobalLimitInPercentage(_globalTransferLimitInPercentageOrToken); - } else { - changeGlobalLimitInTokens(_globalTransferLimitInPercentageOrToken); - } - allowPrimaryIssuance = _allowPrimaryIssuance; - } - - /** - * @notice Sets whether or not to consider primary issuance transfers - * @param _allowPrimaryIssuance whether to allow all primary issuance transfers - */ - function setAllowPrimaryIssuance(bool _allowPrimaryIssuance) public withPerm(ADMIN) { - require(_allowPrimaryIssuance != allowPrimaryIssuance, "Must change setting"); - allowPrimaryIssuance = _allowPrimaryIssuance; - /*solium-disable-next-line security/no-block-members*/ - emit SetAllowPrimaryIssuance(_allowPrimaryIssuance, now); - } - - /** - * @notice Changes the manager to use transfer limit as Percentages - * @param _newGlobalTransferLimitInPercentage uint256 new global Transfer Limit In Percentage. - * @dev specialTransferLimits set for wallets have to re-configured - */ - function changeTransferLimitToPercentage(uint256 _newGlobalTransferLimitInPercentage) public withPerm(ADMIN) { - require(!isTransferLimitInPercentage, "Transfer limit already in percentage"); - isTransferLimitInPercentage = true; - changeGlobalLimitInPercentage(_newGlobalTransferLimitInPercentage); - emit TransferLimitChangedtoPercentage(); - } - - /** - * @notice Changes the manager to use transfer limit as tokens - * @param _newGlobalTransferLimit uint256 new global Transfer Limit in tokens. - * @dev specialTransferLimits set for wallets have to re-configured - */ - function changeTransferLimitToTokens(uint _newGlobalTransferLimit) public withPerm(ADMIN) { - require(isTransferLimitInPercentage, "Transfer limit already in tokens"); - isTransferLimitInPercentage = false; - changeGlobalLimitInTokens(_newGlobalTransferLimit); - emit TransferLimitChangedToTokens(); - } - /** - * @notice Changes the global transfer limit - * @param _newGlobalTransferLimitInTokens new transfer limit in tokens - * @dev This function can be used only when The manager is configured to use limits in tokens - */ - function changeGlobalLimitInTokens(uint256 _newGlobalTransferLimitInTokens) public withPerm(ADMIN) { - require(!isTransferLimitInPercentage, "Transfer limit not set in tokens"); - require(_newGlobalTransferLimitInTokens > 0, "Transfer limit has to greater than zero"); - emit GlobalTransferLimitInTokensSet(_newGlobalTransferLimitInTokens, globalTransferLimitInTokens); - globalTransferLimitInTokens = _newGlobalTransferLimitInTokens; - - } - - /** - * @notice Changes the global transfer limit - * @param _newGlobalTransferLimitInPercentage new transfer limit in percentage. - * Multiply the percentage by 10^16. Eg 22% will be 22*10^16 - * @dev This function can be used only when The manager is configured to use limits in percentage - */ - function changeGlobalLimitInPercentage(uint256 _newGlobalTransferLimitInPercentage) public withPerm(ADMIN) { - require(isTransferLimitInPercentage, "Transfer limit not set in Percentage"); - require( - _newGlobalTransferLimitInPercentage > 0 && _newGlobalTransferLimitInPercentage <= 100 * 10 ** 16, - "Limit not within [0,100]" - ); - emit GlobalTransferLimitInPercentageSet(_newGlobalTransferLimitInPercentage, globalTransferLimitInPercentage); - globalTransferLimitInPercentage = _newGlobalTransferLimitInPercentage; - - } - - /** - * @notice Adds an exempt wallet - * @param _wallet exempt wallet address - */ - function addExemptWallet(address _wallet) public withPerm(ADMIN) { - require(_wallet != address(0), "Wallet address cannot be a zero address"); - exemptWallets[_wallet] = true; - emit ExemptWalletAdded(_wallet); - } - - /** - * @notice Removes an exempt wallet - * @param _wallet exempt wallet address - */ - function removeExemptWallet(address _wallet) public withPerm(ADMIN) { - require(_wallet != address(0), "Wallet address cannot be a zero address"); - exemptWallets[_wallet] = false; - emit ExemptWalletRemoved(_wallet); - } - - /** - * @notice Adds an array of exempt wallet - * @param _wallets array of exempt wallet addresses - */ - function addExemptWalletMulti(address[] memory _wallets) public withPerm(ADMIN) { - require(_wallets.length > 0, "Wallets cannot be empty"); - for (uint256 i = 0; i < _wallets.length; i++) { - addExemptWallet(_wallets[i]); - } - } - - /** - * @notice Removes an array of exempt wallet - * @param _wallets array of exempt wallet addresses - */ - function removeExemptWalletMulti(address[] memory _wallets) public withPerm(ADMIN) { - require(_wallets.length > 0, "Wallets cannot be empty"); - for (uint256 i = 0; i < _wallets.length; i++) { - removeExemptWallet(_wallets[i]); - } - } - - /** - * @notice Sets transfer limit per wallet - * @param _wallet wallet address - * @param _transferLimit transfer limit for the wallet in tokens - * @dev the manager has to be configured to use limits in tokens - */ - function setTransferLimitInTokens(address _wallet, uint _transferLimit) public withPerm(ADMIN) { - require(_transferLimit > 0, "Transfer limit has to be greater than 0"); - require(!isTransferLimitInPercentage, "Transfer limit not in token amount"); - specialTransferLimitsInTokens[_wallet] = _transferLimit; - emit TransferLimitInTokensSet(_wallet, _transferLimit); - } - - /** - * @notice Sets transfer limit for a wallet - * @param _wallet wallet address - * @param _transferLimitInPercentage transfer limit for the wallet in percentage. - * Multiply the percentage by 10^16. Eg 22% will be 22*10^16 - * @dev The manager has to be configured to use percentages - */ - function setTransferLimitInPercentage(address _wallet, uint _transferLimitInPercentage) public withPerm(ADMIN) { - require(isTransferLimitInPercentage, "Transfer limit not in percentage"); - require(_transferLimitInPercentage > 0 && _transferLimitInPercentage <= 100 * 10 ** 16, "Transfer limit not in required range"); - specialTransferLimitsInPercentages[_wallet] = _transferLimitInPercentage; - emit TransferLimitInPercentageSet(_wallet, _transferLimitInPercentage); - } - - /** - * @notice Removes transfer limit set in percentage for a wallet - * @param _wallet wallet address - */ - function removeTransferLimitInPercentage(address _wallet) public withPerm(ADMIN) { - require(specialTransferLimitsInPercentages[_wallet] > 0, "Wallet Address does not have a transfer limit"); - specialTransferLimitsInPercentages[_wallet] = 0; - emit TransferLimitInPercentageRemoved(_wallet); - } - - /** - * @notice Removes transfer limit set in tokens for a wallet - * @param _wallet wallet address - */ - function removeTransferLimitInTokens(address _wallet) public withPerm(ADMIN) { - require(specialTransferLimitsInTokens[_wallet] > 0, "Wallet Address does not have a transfer limit"); - specialTransferLimitsInTokens[_wallet] = 0; - emit TransferLimitInTokensRemoved(_wallet); - } - - /** - * @notice Sets transfer limits for an array of wallet - * @param _wallets array of wallet addresses - * @param _transferLimits array of transfer limits for each wallet in tokens - * @dev The manager has to be configured to use tokens as limit - */ - function setTransferLimitInTokensMulti(address[] memory _wallets, uint[] memory _transferLimits) public withPerm(ADMIN) { - require(_wallets.length > 0, "Wallets cannot be empty"); - require(_wallets.length == _transferLimits.length, "Wallets don't match to transfer limits"); - for (uint256 i = 0; i < _wallets.length; i++) { - setTransferLimitInTokens(_wallets[i], _transferLimits[i]); - } - } - - /** - * @notice Sets transfer limits for an array of wallet - * @param _wallets array of wallet addresses - * @param _transferLimitsInPercentage array of transfer limits for each wallet in percentages - * The percentage has to be multipled by 10 ** 16. Eg: 20% would be 20 * 10 ** 16 - * @dev The manager has to be configured to use percentage as limit - */ - function setTransferLimitInPercentageMulti( - address[] memory _wallets, - uint[] memory _transferLimitsInPercentage - ) public withPerm(ADMIN) { - require(_wallets.length > 0, "Wallets cannot be empty"); - require(_wallets.length == _transferLimitsInPercentage.length, "Wallets don't match to percentage limits"); - for (uint256 i = 0; i < _wallets.length; i++) { - setTransferLimitInPercentage(_wallets[i], _transferLimitsInPercentage[i]); - } - } - - /** - * @notice Removes transfer limits set in tokens for an array of wallet - * @param _wallets array of wallet addresses - */ - function removeTransferLimitInTokensMulti(address[] memory _wallets) public withPerm(ADMIN) { - require(_wallets.length > 0, "Wallets cannot be empty"); - for (uint i = 0; i < _wallets.length; i++) { - removeTransferLimitInTokens(_wallets[i]); - } - } - - /** - * @notice Removes transfer limits set in percentage for an array of wallet - * @param _wallets array of wallet addresses - */ - function removeTransferLimitInPercentageMulti(address[] memory _wallets) public withPerm(ADMIN) { - require(_wallets.length > 0, "Wallets cannot be empty"); - for (uint i = 0; i < _wallets.length; i++) { - removeTransferLimitInPercentage(_wallets[i]); - } - } - - /** - * @notice This function returns the signature of configure function - */ - function getInitFunction() public pure returns(bytes4) { - return bytes4(keccak256("configure(bool,uint256,bool)")); - } - - /** - * @notice Returns the permissions flag that are associated with SingleTradeVolumeRestrictionManager - */ - function getPermissions() public view returns(bytes32[] memory) { - bytes32[] memory allPermissions = new bytes32[](1); - allPermissions[0] = ADMIN; - return allPermissions; - } -} diff --git a/contracts/modules/Experimental/TransferManager/SingleTradeVolumeRestrictionTMFactory.sol b/contracts/modules/Experimental/TransferManager/SingleTradeVolumeRestrictionTMFactory.sol deleted file mode 100644 index d00a32bf0..000000000 --- a/contracts/modules/Experimental/TransferManager/SingleTradeVolumeRestrictionTMFactory.sol +++ /dev/null @@ -1,81 +0,0 @@ -pragma solidity ^0.5.0; - -import "./../../ModuleFactory.sol"; -import "./SingleTradeVolumeRestrictionTM.sol"; -import "../../../libraries/Util.sol"; - -/** - * @title Factory for deploying SingleTradeVolumeRestrictionManager - */ -contract SingleTradeVolumeRestrictionTMFactory is ModuleFactory { - /** - * @notice Constructor - * @param _setupCost Setup cost of the module - * @param _usageCost Usage cost of the module - * @param _subscriptionCost Subscription cost of the module - */ - constructor( - uint256 _setupCost, - uint256 _usageCost, - uint256 _subscriptionCost - ) - public - ModuleFactory(_setupCost, _usageCost, _subscriptionCost) - { - version = "1.0.0"; - name = "SingleTradeVolumeRestrictionTM"; - title = "Single Trade Volume Restriction Manager"; - description = "Imposes volume restriction on a single trade"; - compatibleSTVersionRange["lowerBound"] = VersionUtils.pack(uint8(0), uint8(0), uint8(0)); - compatibleSTVersionRange["upperBound"] = VersionUtils.pack(uint8(0), uint8(0), uint8(0)); - } - - /** - * @notice Used to launch the Module with the help of factory - * @return address Contract address of the Module - */ - function deploy(bytes calldata _data) external returns(address) { - address polyToken = _takeFee(); - SingleTradeVolumeRestrictionTM singleTradeVolumeRestrictionManager = new SingleTradeVolumeRestrictionTM(msg.sender, polyToken); - require(Util.getSig(_data) == singleTradeVolumeRestrictionManager.getInitFunction(), "Provided data is not valid"); - bool success; - /*solium-disable-next-line security/no-low-level-calls*/ - (success, ) = address(singleTradeVolumeRestrictionManager).call(_data); - require(success, "Unsuccessful call"); - /*solium-disable-next-line security/no-block-members*/ - emit GenerateModuleFromFactory(address(singleTradeVolumeRestrictionManager), getName(), address(this), msg.sender, setupCost, now); - return address(singleTradeVolumeRestrictionManager); - } - - /** - * @notice Get the types of the Module factory - * @return uint8[] - */ - function getTypes() external view returns(uint8[] memory) { - uint8[] memory res = new uint8[](1); - res[0] = 2; - return res; - } - - /** - * @notice Get the Instructions that help to use the module - * @return string - */ - function getInstructions() external view returns(string memory) { - /*solium-disable-next-line max-len*/ - return "Allows an issuer to impose volume restriction on a single trade. Init function takes two parameters. First parameter is a bool indicating if restriction is in percentage. The second parameter is the value in percentage or amount of tokens"; - } - - /** - * @notice Get the tags related to the module factory - * @return bytes32[] - */ - function getTags() external view returns(bytes32[] memory) { - bytes32[] memory availableTags = new bytes32[](3); - availableTags[0] = "Single Trade"; - availableTags[1] = "Transfer"; - availableTags[2] = "Volume"; - return availableTags; - } - -} diff --git a/contracts/modules/Experimental/Wallet/IWallet.sol b/contracts/modules/Experimental/Wallet/IWallet.sol new file mode 100644 index 000000000..013bcc540 --- /dev/null +++ b/contracts/modules/Experimental/Wallet/IWallet.sol @@ -0,0 +1,19 @@ +pragma solidity ^0.5.0; + +import "../../../Pausable.sol"; +import "../../Module.sol"; + +/** + * @title Interface to be implemented by all Wallet modules + * @dev abstract contract + */ +contract IWallet is Module, Pausable { + + function unpause() public onlyOwner { + super._unpause(); + } + + function pause() public onlyOwner { + super._pause(); + } +} diff --git a/contracts/modules/Experimental/Wallet/VestingEscrowWallet.sol b/contracts/modules/Experimental/Wallet/VestingEscrowWallet.sol new file mode 100644 index 000000000..5b483cdf5 --- /dev/null +++ b/contracts/modules/Experimental/Wallet/VestingEscrowWallet.sol @@ -0,0 +1,566 @@ +pragma solidity ^0.5.0; + +import "openzeppelin-solidity/contracts/math/SafeMath.sol"; +import "../../../storage/VestingEscrowWalletStorage.sol"; +import "./IWallet.sol"; +import "../../../interfaces/ISecurityToken.sol"; + +/** + * @title Wallet for core vesting escrow functionality + */ +contract VestingEscrowWallet is VestingEscrowWalletStorage, IWallet { + using SafeMath for uint256; + + bytes32 public constant ADMIN = "ADMIN"; + + // States used to represent the status of the schedule + enum State {CREATED, STARTED, COMPLETED} + + // Emit when new schedule is added + event AddSchedule( + address indexed _beneficiary, + bytes32 _templateName, + uint256 _startTime + ); + // Emit when schedule is modified + event ModifySchedule( + address indexed _beneficiary, + bytes32 _templateName, + uint256 _startTime + ); + // Emit when all schedules are revoked for user + event RevokeAllSchedules(address indexed _beneficiary); + // Emit when schedule is revoked + event RevokeSchedule(address indexed _beneficiary, bytes32 _templateName); + // Emit when tokes are deposited to wallet + event DepositTokens(uint256 _numberOfTokens, address _sender); + // Emit when all unassigned tokens are sent to treasury + event SendToTreasury(uint256 _numberOfTokens, address _sender); + // Emit when is sent tokes to user + event SendTokens(address indexed _beneficiary, uint256 _numberOfTokens); + // Emit when template is added + event AddTemplate(bytes32 _name, uint256 _numberOfTokens, uint256 _duration, uint256 _frequency); + // Emit when template is removed + event RemoveTemplate(bytes32 _name); + // Emit when the treasury wallet gets changed + event TreasuryWalletChanged(address _newWallet, address _oldWallet); + + /** + * @notice Constructor + * @param _securityToken Address of the security token + * @param _polyAddress Address of the polytoken + */ + constructor (address _securityToken, address _polyAddress) + public + Module(_securityToken, _polyAddress) + { + } + + /** + * @notice This function returns the signature of the configure function + */ + function getInitFunction() public pure returns (bytes4) { + return bytes4(keccak256("configure(address)")); + } + + /** + * @notice Used to initialize the treasury wallet address + * @param _treasuryWallet Address of the treasury wallet + */ + function configure(address _treasuryWallet) public onlyFactory { + require(_treasuryWallet != address(0), "Invalid address"); + treasuryWallet = _treasuryWallet; + } + + /** + * @notice Used to change the treasury wallet address + * @param _newTreasuryWallet Address of the treasury wallet + */ + function changeTreasuryWallet(address _newTreasuryWallet) public onlyOwner { + require(_newTreasuryWallet != address(0)); + emit TreasuryWalletChanged(_newTreasuryWallet, treasuryWallet); + treasuryWallet = _newTreasuryWallet; + } + + /** + * @notice Used to deposit tokens from treasury wallet to the vesting escrow wallet + * @param _numberOfTokens Number of tokens that should be deposited + */ + function depositTokens(uint256 _numberOfTokens) external withPerm(ADMIN) { + _depositTokens(_numberOfTokens); + } + + function _depositTokens(uint256 _numberOfTokens) internal { + require(_numberOfTokens > 0, "Should be > 0"); + require( + ISecurityToken(securityToken).transferFrom(msg.sender, address(this), _numberOfTokens), + "Failed transferFrom due to insufficent Allowance provided" + ); + unassignedTokens = unassignedTokens.add(_numberOfTokens); + emit DepositTokens(_numberOfTokens, msg.sender); + } + + /** + * @notice Sends unassigned tokens to the treasury wallet + * @param _amount Amount of tokens that should be send to the treasury wallet + */ + function sendToTreasury(uint256 _amount) external withPerm(ADMIN) { + require(_amount > 0, "Amount cannot be zero"); + require(_amount <= unassignedTokens, "Amount is greater than unassigned tokens"); + uint256 amount = unassignedTokens; + unassignedTokens = 0; + require(ISecurityToken(securityToken).transfer(treasuryWallet, amount), "Transfer failed"); + emit SendToTreasury(amount, msg.sender); + } + + /** + * @notice Pushes available tokens to the beneficiary's address + * @param _beneficiary Address of the beneficiary who will receive tokens + */ + function pushAvailableTokens(address _beneficiary) public withPerm(ADMIN) { + _sendTokens(_beneficiary); + } + + /** + * @notice Used to withdraw available tokens by beneficiary + */ + function pullAvailableTokens() external { + _sendTokens(msg.sender); + } + + /** + * @notice Adds template that can be used for creating schedule + * @param _name Name of the template will be created + * @param _numberOfTokens Number of tokens that should be assigned to schedule + * @param _duration Duration of the vesting schedule + * @param _frequency Frequency of the vesting schedule + */ + function addTemplate(bytes32 _name, uint256 _numberOfTokens, uint256 _duration, uint256 _frequency) external withPerm(ADMIN) { + _addTemplate(_name, _numberOfTokens, _duration, _frequency); + } + + function _addTemplate(bytes32 _name, uint256 _numberOfTokens, uint256 _duration, uint256 _frequency) internal { + require(_name != bytes32(0), "Invalid name"); + require(!_isTemplateExists(_name), "Already exists"); + _validateTemplate(_numberOfTokens, _duration, _frequency); + templateNames.push(_name); + templates[_name] = Template(_numberOfTokens, _duration, _frequency, templateNames.length - 1); + emit AddTemplate(_name, _numberOfTokens, _duration, _frequency); + } + + /** + * @notice Removes template with a given name + * @param _name Name of the template that will be removed + */ + function removeTemplate(bytes32 _name) external withPerm(ADMIN) { + require(_isTemplateExists(_name), "Template not found"); + require(templateToUsers[_name].length == 0, "Template is used"); + uint256 index = templates[_name].index; + if (index != templateNames.length - 1) { + templateNames[index] = templateNames[templateNames.length - 1]; + templates[templateNames[index]].index = index; + } + templateNames.length--; + // delete template data + delete templates[_name]; + emit RemoveTemplate(_name); + } + + /** + * @notice Returns count of the templates those can be used for creating schedule + * @return Count of the templates + */ + function getTemplateCount() external view returns(uint256) { + return templateNames.length; + } + + /** + * @notice Gets the list of the template names those can be used for creating schedule + * @return bytes32 Array of all template names were created + */ + function getAllTemplateNames() external view returns(bytes32[] memory) { + return templateNames; + } + + /** + * @notice Adds vesting schedules for each of the beneficiary's address + * @param _beneficiary Address of the beneficiary for whom it is scheduled + * @param _templateName Name of the template that will be created + * @param _numberOfTokens Total number of tokens for created schedule + * @param _duration Duration of the created vesting schedule + * @param _frequency Frequency of the created vesting schedule + * @param _startTime Start time of the created vesting schedule + */ + function addSchedule( + address _beneficiary, + bytes32 _templateName, + uint256 _numberOfTokens, + uint256 _duration, + uint256 _frequency, + uint256 _startTime + ) + external + withPerm(ADMIN) + { + _addSchedule(_beneficiary, _templateName, _numberOfTokens, _duration, _frequency, _startTime); + } + + function _addSchedule( + address _beneficiary, + bytes32 _templateName, + uint256 _numberOfTokens, + uint256 _duration, + uint256 _frequency, + uint256 _startTime + ) + internal + { + _addTemplate(_templateName, _numberOfTokens, _duration, _frequency); + _addScheduleFromTemplate(_beneficiary, _templateName, _startTime); + } + + /** + * @notice Adds vesting schedules from template for the beneficiary + * @param _beneficiary Address of the beneficiary for whom it is scheduled + * @param _templateName Name of the exists template + * @param _startTime Start time of the created vesting schedule + */ + function addScheduleFromTemplate(address _beneficiary, bytes32 _templateName, uint256 _startTime) external withPerm(ADMIN) { + _addScheduleFromTemplate(_beneficiary, _templateName, _startTime); + } + + function _addScheduleFromTemplate(address _beneficiary, bytes32 _templateName, uint256 _startTime) internal { + require(_beneficiary != address(0), "Invalid address"); + require(_isTemplateExists(_templateName), "Template not found"); + uint256 index = userToTemplateIndex[_beneficiary][_templateName]; + require( + schedules[_beneficiary].length == 0 || + schedules[_beneficiary][index].templateName != _templateName, + "Already added" + ); + require(_startTime >= now, "Date in the past"); + uint256 numberOfTokens = templates[_templateName].numberOfTokens; + if (numberOfTokens > unassignedTokens) { + _depositTokens(numberOfTokens.sub(unassignedTokens)); + } + unassignedTokens = unassignedTokens.sub(numberOfTokens); + if (!beneficiaryAdded[_beneficiary]) { + beneficiaries.push(_beneficiary); + beneficiaryAdded[_beneficiary] = true; + } + schedules[_beneficiary].push(Schedule(_templateName, 0, _startTime)); + userToTemplates[_beneficiary].push(_templateName); + userToTemplateIndex[_beneficiary][_templateName] = schedules[_beneficiary].length - 1; + templateToUsers[_templateName].push(_beneficiary); + templateToUserIndex[_templateName][_beneficiary] = templateToUsers[_templateName].length - 1; + emit AddSchedule(_beneficiary, _templateName, _startTime); + } + + /** + * @notice Modifies vesting schedules for each of the beneficiary + * @param _beneficiary Address of the beneficiary for whom it is modified + * @param _templateName Name of the template was used for schedule creation + * @param _startTime Start time of the created vesting schedule + */ + function modifySchedule(address _beneficiary, bytes32 _templateName, uint256 _startTime) public withPerm(ADMIN) { + _modifySchedule(_beneficiary, _templateName, _startTime); + } + + function _modifySchedule(address _beneficiary, bytes32 _templateName, uint256 _startTime) internal { + _checkSchedule(_beneficiary, _templateName); + require(_startTime > now, "Date in the past"); + uint256 index = userToTemplateIndex[_beneficiary][_templateName]; + Schedule storage schedule = schedules[_beneficiary][index]; + /*solium-disable-next-line security/no-block-members*/ + require(now < schedule.startTime, "Schedule started"); + schedule.startTime = _startTime; + emit ModifySchedule(_beneficiary, _templateName, _startTime); + } + + /** + * @notice Revokes vesting schedule with given template name for given beneficiary + * @param _beneficiary Address of the beneficiary for whom it is revoked + * @param _templateName Name of the template was used for schedule creation + */ + function revokeSchedule(address _beneficiary, bytes32 _templateName) external withPerm(ADMIN) { + _checkSchedule(_beneficiary, _templateName); + uint256 index = userToTemplateIndex[_beneficiary][_templateName]; + _sendTokensPerSchedule(_beneficiary, index); + uint256 releasedTokens = _getReleasedTokens(_beneficiary, index); + unassignedTokens = unassignedTokens.add(templates[_templateName].numberOfTokens.sub(releasedTokens)); + _deleteUserToTemplates(_beneficiary, _templateName); + _deleteTemplateToUsers(_beneficiary, _templateName); + emit RevokeSchedule(_beneficiary, _templateName); + } + + function _deleteUserToTemplates(address _beneficiary, bytes32 _templateName) internal { + uint256 index = userToTemplateIndex[_beneficiary][_templateName]; + Schedule[] storage userSchedules = schedules[_beneficiary]; + if (index != userSchedules.length - 1) { + userSchedules[index] = userSchedules[userSchedules.length - 1]; + userToTemplates[_beneficiary][index] = userToTemplates[_beneficiary][userToTemplates[_beneficiary].length - 1]; + userToTemplateIndex[_beneficiary][userSchedules[index].templateName] = index; + } + userSchedules.length--; + userToTemplates[_beneficiary].length--; + delete userToTemplateIndex[_beneficiary][_templateName]; + } + + function _deleteTemplateToUsers(address _beneficiary, bytes32 _templateName) internal { + uint256 templateIndex = templateToUserIndex[_templateName][_beneficiary]; + if (templateIndex != templateToUsers[_templateName].length - 1) { + templateToUsers[_templateName][templateIndex] = templateToUsers[_templateName][templateToUsers[_templateName].length - 1]; + templateToUserIndex[_templateName][templateToUsers[_templateName][templateIndex]] = templateIndex; + } + templateToUsers[_templateName].length--; + delete templateToUserIndex[_templateName][_beneficiary]; + } + + /** + * @notice Revokes all vesting schedules for given beneficiary's address + * @param _beneficiary Address of the beneficiary for whom all schedules will be revoked + */ + function revokeAllSchedules(address _beneficiary) public withPerm(ADMIN) { + _revokeAllSchedules(_beneficiary); + } + + function _revokeAllSchedules(address _beneficiary) internal { + require(_beneficiary != address(0), "Invalid address"); + _sendTokens(_beneficiary); + Schedule[] storage userSchedules = schedules[_beneficiary]; + for (uint256 i = 0; i < userSchedules.length; i++) { + uint256 releasedTokens = _getReleasedTokens(_beneficiary, i); + Template memory template = templates[userSchedules[i].templateName]; + unassignedTokens = unassignedTokens.add(template.numberOfTokens.sub(releasedTokens)); + delete userToTemplateIndex[_beneficiary][userSchedules[i].templateName]; + _deleteTemplateToUsers(_beneficiary, userSchedules[i].templateName); + } + delete schedules[_beneficiary]; + delete userToTemplates[_beneficiary]; + emit RevokeAllSchedules(_beneficiary); + } + + /** + * @notice Returns beneficiary's schedule created using template name + * @param _beneficiary Address of the beneficiary who will receive tokens + * @param _templateName Name of the template was used for schedule creation + * @return beneficiary's schedule data (numberOfTokens, duration, frequency, startTime, claimedTokens, State) + */ + function getSchedule(address _beneficiary, bytes32 _templateName) external view returns(uint256, uint256, uint256, uint256, uint256, State) { + _checkSchedule(_beneficiary, _templateName); + uint256 index = userToTemplateIndex[_beneficiary][_templateName]; + Schedule memory schedule = schedules[_beneficiary][index]; + return ( + templates[schedule.templateName].numberOfTokens, + templates[schedule.templateName].duration, + templates[schedule.templateName].frequency, + schedule.startTime, + schedule.claimedTokens, + _getScheduleState(_beneficiary, _templateName) + ); + } + + function _getScheduleState(address _beneficiary, bytes32 _templateName) internal view returns(State) { + _checkSchedule(_beneficiary, _templateName); + uint256 index = userToTemplateIndex[_beneficiary][_templateName]; + Schedule memory schedule = schedules[_beneficiary][index]; + if (now < schedule.startTime) { + return State.CREATED; + } else if (now > schedule.startTime && now < schedule.startTime.add(templates[_templateName].duration)) { + return State.STARTED; + } else { + return State.COMPLETED; + } + } + + /** + * @notice Returns list of the template names for given beneficiary's address + * @param _beneficiary Address of the beneficiary + * @return List of the template names that were used for schedule creation + */ + function getTemplateNames(address _beneficiary) external view returns(bytes32[] memory) { + require(_beneficiary != address(0), "Invalid address"); + return userToTemplates[_beneficiary]; + } + + /** + * @notice Returns count of the schedules were created for given beneficiary + * @param _beneficiary Address of the beneficiary + * @return Count of beneficiary's schedules + */ + function getScheduleCount(address _beneficiary) external view returns(uint256) { + require(_beneficiary != address(0), "Invalid address"); + return schedules[_beneficiary].length; + } + + function _getAvailableTokens(address _beneficiary, uint256 _index) internal view returns(uint256) { + Schedule memory schedule = schedules[_beneficiary][_index]; + uint256 releasedTokens = _getReleasedTokens(_beneficiary, _index); + return releasedTokens.sub(schedule.claimedTokens); + } + + function _getReleasedTokens(address _beneficiary, uint256 _index) internal view returns(uint256) { + Schedule memory schedule = schedules[_beneficiary][_index]; + Template memory template = templates[schedule.templateName]; + /*solium-disable-next-line security/no-block-members*/ + if (now > schedule.startTime) { + uint256 periodCount = template.duration.div(template.frequency); + /*solium-disable-next-line security/no-block-members*/ + uint256 periodNumber = (now.sub(schedule.startTime)).div(template.frequency); + if (periodNumber > periodCount) { + periodNumber = periodCount; + } + return template.numberOfTokens.mul(periodNumber).div(periodCount); + } else { + return 0; + } + } + + /** + * @notice Used to bulk send available tokens for each of the beneficiaries + * @param _fromIndex Start index of array of beneficiary's addresses + * @param _toIndex End index of array of beneficiary's addresses + */ + function pushAvailableTokensMulti(uint256 _fromIndex, uint256 _toIndex) external withPerm(ADMIN) { + require(_toIndex <= beneficiaries.length - 1, "Array out of bound"); + for (uint256 i = _fromIndex; i <= _toIndex; i++) { + if (schedules[beneficiaries[i]].length !=0) + pushAvailableTokens(beneficiaries[i]); + } + } + + /** + * @notice Used to bulk add vesting schedules for each of beneficiary + * @param _beneficiaries Array of the beneficiary's addresses + * @param _templateNames Array of the template names + * @param _numberOfTokens Array of number of tokens should be assigned to schedules + * @param _durations Array of the vesting duration + * @param _frequencies Array of the vesting frequency + * @param _startTimes Array of the vesting start time + */ + function addScheduleMulti( + address[] memory _beneficiaries, + bytes32[] memory _templateNames, + uint256[] memory _numberOfTokens, + uint256[] memory _durations, + uint256[] memory _frequencies, + uint256[] memory _startTimes + ) + public + withPerm(ADMIN) + { + require( + _beneficiaries.length == _templateNames.length && /*solium-disable-line operator-whitespace*/ + _beneficiaries.length == _numberOfTokens.length && /*solium-disable-line operator-whitespace*/ + _beneficiaries.length == _durations.length && /*solium-disable-line operator-whitespace*/ + _beneficiaries.length == _frequencies.length && /*solium-disable-line operator-whitespace*/ + _beneficiaries.length == _startTimes.length, + "Arrays sizes mismatch" + ); + for (uint256 i = 0; i < _beneficiaries.length; i++) { + _addSchedule(_beneficiaries[i], _templateNames[i], _numberOfTokens[i], _durations[i], _frequencies[i], _startTimes[i]); + } + } + + /** + * @notice Used to bulk add vesting schedules from template for each of the beneficiary + * @param _beneficiaries Array of beneficiary's addresses + * @param _templateNames Array of the template names were used for schedule creation + * @param _startTimes Array of the vesting start time + */ + function addScheduleFromTemplateMulti( + address[] calldata _beneficiaries, + bytes32[] calldata _templateNames, + uint256[] calldata _startTimes + ) + external + withPerm(ADMIN) + { + require(_beneficiaries.length == _templateNames.length && _beneficiaries.length == _startTimes.length, "Arrays sizes mismatch"); + for (uint256 i = 0; i < _beneficiaries.length; i++) { + _addScheduleFromTemplate(_beneficiaries[i], _templateNames[i], _startTimes[i]); + } + } + + /** + * @notice Used to bulk revoke vesting schedules for each of the beneficiaries + * @param _beneficiaries Array of the beneficiary's addresses + */ + function revokeSchedulesMulti(address[] calldata _beneficiaries) external withPerm(ADMIN) { + for (uint256 i = 0; i < _beneficiaries.length; i++) { + _revokeAllSchedules(_beneficiaries[i]); + } + } + + /** + * @notice Used to bulk modify vesting schedules for each of the beneficiaries + * @param _beneficiaries Array of the beneficiary's addresses + * @param _templateNames Array of the template names + * @param _startTimes Array of the vesting start time + */ + function modifyScheduleMulti( + address[] memory _beneficiaries, + bytes32[] memory _templateNames, + uint256[] memory _startTimes + ) + public + withPerm(ADMIN) + { + require( + _beneficiaries.length == _templateNames.length && /*solium-disable-line operator-whitespace*/ + _beneficiaries.length == _startTimes.length, + "Arrays sizes mismatch" + ); + for (uint256 i = 0; i < _beneficiaries.length; i++) { + _modifySchedule(_beneficiaries[i], _templateNames[i], _startTimes[i]); + } + } + + function _checkSchedule(address _beneficiary, bytes32 _templateName) internal view { + require(_beneficiary != address(0), "Invalid address"); + uint256 index = userToTemplateIndex[_beneficiary][_templateName]; + require( + index < schedules[_beneficiary].length && + schedules[_beneficiary][index].templateName == _templateName, + "Schedule not found" + ); + } + + function _isTemplateExists(bytes32 _name) internal view returns(bool) { + return templates[_name].numberOfTokens > 0; + } + + function _validateTemplate(uint256 _numberOfTokens, uint256 _duration, uint256 _frequency) internal view { + require(_numberOfTokens > 0, "Zero amount"); + require(_duration % _frequency == 0, "Invalid frequency"); + uint256 periodCount = _duration.div(_frequency); + require(_numberOfTokens % periodCount == 0); + uint256 amountPerPeriod = _numberOfTokens.div(periodCount); + require(amountPerPeriod % ISecurityToken(securityToken).granularity() == 0, "Invalid granularity"); + } + + function _sendTokens(address _beneficiary) internal { + for (uint256 i = 0; i < schedules[_beneficiary].length; i++) { + _sendTokensPerSchedule(_beneficiary, i); + } + } + + function _sendTokensPerSchedule(address _beneficiary, uint256 _index) internal { + uint256 amount = _getAvailableTokens(_beneficiary, _index); + if (amount > 0) { + schedules[_beneficiary][_index].claimedTokens = schedules[_beneficiary][_index].claimedTokens.add(amount); + require(ISecurityToken(securityToken).transfer(_beneficiary, amount), "Transfer failed"); + emit SendTokens(_beneficiary, amount); + } + } + + /** + * @notice Return the permissions flag that are associated with VestingEscrowWallet + */ + function getPermissions() public view returns(bytes32[] memory) { + bytes32[] memory allPermissions = new bytes32[](1); + allPermissions[0] = ADMIN; + return allPermissions; + } + +} diff --git a/contracts/modules/Experimental/Wallet/VestingEscrowWalletFactory.sol b/contracts/modules/Experimental/Wallet/VestingEscrowWalletFactory.sol new file mode 100644 index 000000000..0ddf7123d --- /dev/null +++ b/contracts/modules/Experimental/Wallet/VestingEscrowWalletFactory.sol @@ -0,0 +1,75 @@ +pragma solidity ^0.5.0; + +import "../../../proxy/VestingEscrowWalletProxy.sol"; +import "../../../interfaces/IBoot.sol"; +import "../../ModuleFactory.sol"; +import "../../../libraries/Util.sol"; + +/** + * @title Factory for deploying VestingEscrowWallet module + */ +contract VestingEscrowWalletFactory is ModuleFactory { + + address public logicContract; + /** + * @notice Constructor + */ + constructor (uint256 _setupCost, uint256 _usageCost, uint256 _subscriptionCost, address _logicContract) public + ModuleFactory(_setupCost, _usageCost, _subscriptionCost) + { + require(_logicContract != address(0), "Invalid address"); + version = "1.0.0"; + name = "VestingEscrowWallet"; + title = "Vesting Escrow Wallet"; + description = "Manage vesting schedules to employees / affiliates"; + compatibleSTVersionRange["lowerBound"] = VersionUtils.pack(uint8(0), uint8(0), uint8(0)); + compatibleSTVersionRange["upperBound"] = VersionUtils.pack(uint8(0), uint8(0), uint8(0)); + logicContract = _logicContract; + } + + /** + * @notice Used to launch the Module with the help of factory + * _data Data used for the intialization of the module factory variables + * @return address Contract address of the Module + */ + function deploy(bytes calldata _data) external returns(address) { + address polyToken = _takeFee(); + address vestingEscrowWallet = address(new VestingEscrowWalletProxy(msg.sender, address(polyToken), logicContract)); + //Checks that _data is valid (not calling anything it shouldn't) + require(Util.getSig(_data) == IBoot(vestingEscrowWallet).getInitFunction(), "Invalid data"); + bool success; + /*solium-disable-next-line security/no-low-level-calls*/ + (success, ) = vestingEscrowWallet.call(_data); + require(success, "Unsuccessfull call"); + /*solium-disable-next-line security/no-block-members*/ + emit GenerateModuleFromFactory(vestingEscrowWallet, getName(), address(this), msg.sender, setupCost, now); + return vestingEscrowWallet; + } + + /** + * @notice Type of the Module factory + */ + function getTypes() external view returns(uint8[] memory) { + uint8[] memory res = new uint8[](1); + res[0] = 6; + return res; + } + + /** + * @notice Returns the instructions associated with the module + */ + function getInstructions() external view returns(string memory) { + /*solium-disable-next-line max-len*/ + return "Issuer can deposit tokens to the contract and create the vesting schedule for the given address (Affiliate/Employee). These address can withdraw tokens according to there vesting schedule."; + } + + /** + * @notice Get the tags related to the module factory + */ + function getTags() external view returns(bytes32[] memory) { + bytes32[] memory availableTags = new bytes32[](2); + availableTags[0] = "Vested"; + availableTags[1] = "Escrow Wallet"; + return availableTags; + } +} diff --git a/contracts/modules/Module.sol b/contracts/modules/Module.sol index e2db5f6d9..f6c967f77 100644 --- a/contracts/modules/Module.sol +++ b/contracts/modules/Module.sol @@ -1,8 +1,7 @@ pragma solidity ^0.5.0; -import "../RegistryUpdater.sol"; import "../interfaces/IModule.sol"; -import "../interfaces/ISecurityToken.sol"; +import "../interfaces/ICheckPermission.sol"; import "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol"; import "./ModuleStorage.sol"; import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; @@ -16,8 +15,9 @@ contract Module is IModule, ModuleStorage { * @notice Constructor * @param _securityToken Address of the security token */ - constructor(address _securityToken, address _polyToken) public ModuleStorage(_securityToken, _polyToken) { - + constructor (address _securityToken, address _polyAddress) public + ModuleStorage(_securityToken, _polyAddress) + { } //Allows owner, factory or permissioned delegate @@ -25,7 +25,7 @@ contract Module is IModule, ModuleStorage { bool isOwner = msg.sender == Ownable(securityToken).owner(); bool isFactory = msg.sender == factory; require( - isOwner || isFactory || ISecurityToken(securityToken).checkPermission(msg.sender, address(this), _perm), + isOwner || isFactory || ICheckPermission(securityToken).checkPermission(msg.sender, address(this), _perm), "Permission check failed" ); _; diff --git a/contracts/modules/ModuleFactory.sol b/contracts/modules/ModuleFactory.sol index 9cf1ffcc4..28abaa38a 100644 --- a/contracts/modules/ModuleFactory.sol +++ b/contracts/modules/ModuleFactory.sol @@ -27,18 +27,6 @@ contract ModuleFactory is IModuleFactory, Ownable { // @dev uint24 consists packed value of uint8 _major, uint8 _minor, uint8 _patch mapping(string => uint24) compatibleSTVersionRange; - event ChangeFactorySetupFee(uint256 _oldSetupCost, uint256 _newSetupCost, address _moduleFactory); - event ChangeFactoryUsageFee(uint256 _oldUsageCost, uint256 _newUsageCost, address _moduleFactory); - event ChangeFactorySubscriptionFee(uint256 _oldSubscriptionCost, uint256 _newMonthlySubscriptionCost, address _moduleFactory); - event GenerateModuleFromFactory( - address _module, - bytes32 indexed _moduleName, - address indexed _moduleFactory, - address _creator, - uint256 _timestamp - ); - event ChangeSTVersionBound(string _boundType, uint8 _major, uint8 _minor, uint8 _patch); - /** * @notice Constructor */ diff --git a/contracts/modules/PermissionManager/GeneralPermissionManager.sol b/contracts/modules/PermissionManager/GeneralPermissionManager.sol index 4587785f0..6469ac3bb 100644 --- a/contracts/modules/PermissionManager/GeneralPermissionManager.sol +++ b/contracts/modules/PermissionManager/GeneralPermissionManager.sol @@ -3,6 +3,7 @@ pragma solidity ^0.5.0; import "./IPermissionManager.sol"; import "../Module.sol"; import "./GeneralPermissionManagerStorage.sol"; +import "../../interfaces/ISecurityToken.sol"; /** * @title Permission Manager module for core permissioning functionality @@ -109,9 +110,9 @@ contract GeneralPermissionManager is GeneralPermissionManagerStorage, IPermissio address[] calldata _modules, bytes32[] calldata _perms, bool[] calldata _valids - ) - external - withPerm(CHANGE_PERMISSION) + ) + external + withPerm(CHANGE_PERMISSION) { require(_delegate != address(0), "invalid address"); require(_modules.length > 0, "0 length is not allowed"); diff --git a/contracts/modules/STO/CappedSTO.sol b/contracts/modules/STO/CappedSTO.sol index 3c7adcd0b..596936e37 100644 --- a/contracts/modules/STO/CappedSTO.sol +++ b/contracts/modules/STO/CappedSTO.sol @@ -51,9 +51,9 @@ contract CappedSTO is CappedSTOStorage, STO, ReentrancyGuard { uint256 _rate, FundRaiseType[] memory _fundRaiseTypes, address payable _fundsReceiver - ) - public - onlyFactory + ) + public + onlyFactory { require(endTime == 0, "Already configured"); require(_rate > 0, "Rate of token should be greater than 0"); diff --git a/contracts/modules/STO/CappedSTOFactory.sol b/contracts/modules/STO/CappedSTOFactory.sol index 06090758c..abb32f9d5 100644 --- a/contracts/modules/STO/CappedSTOFactory.sol +++ b/contracts/modules/STO/CappedSTOFactory.sol @@ -29,7 +29,7 @@ contract CappedSTOFactory is ModuleFactory { ModuleFactory(_setupCost, _usageCost, _subscriptionCost) { require(_logicContract != address(0), "Invalid address"); - version = "1.0.0"; + version = "2.1.0"; name = "CappedSTO"; title = "Capped STO"; description = "This smart contract creates a maximum number of tokens (i.e. hard cap) which the total aggregate of tokens acquired by all investors cannot exceed. Security tokens are sent to the investor upon reception of the funds (ETH or POLY), and any security tokens left upon termination of the offering will not be minted."; diff --git a/contracts/modules/STO/ISTOStorage.sol b/contracts/modules/STO/ISTOStorage.sol new file mode 100644 index 000000000..4ff7b9294 --- /dev/null +++ b/contracts/modules/STO/ISTOStorage.sol @@ -0,0 +1,24 @@ +pragma solidity ^0.5.0; + +/** + * @title Storage layout for the ISTO contract + */ +contract ISTOStorage { + + mapping (uint8 => bool) public fundRaiseTypes; + mapping (uint8 => uint256) public fundsRaised; + + // Start time of the STO + uint256 public startTime; + // End time of the STO + uint256 public endTime; + // Time STO was paused + uint256 public pausedTime; + // Number of individual investors + uint256 public investorCount; + // Address where ETH & POLY funds are delivered + address public wallet; + // Final amount of tokens sold + uint256 public totalTokensSold; + +} \ No newline at end of file diff --git a/contracts/modules/STO/STO.sol b/contracts/modules/STO/STO.sol index 088e84951..d3f28c5fd 100644 --- a/contracts/modules/STO/STO.sol +++ b/contracts/modules/STO/STO.sol @@ -13,7 +13,7 @@ import "../../interfaces/ISTO.sol"; contract STO is ISTO, STOStorage, Module, Pausable { using SafeMath for uint256; - enum FundRaiseType {ETH, POLY, DAI} + enum FundRaiseType {ETH, POLY, SC} // Event event SetFundRaiseTypes(FundRaiseType[] _fundRaiseTypes); @@ -37,6 +37,11 @@ contract STO is ISTO, STOStorage, Module, Pausable { return fundsRaised[uint8(_fundRaiseType)]; } + /** + * @notice Returns the total no. of tokens sold + */ + function getTokensSold() external view returns (uint256); + /** * @notice Pause (overridden function) */ @@ -58,7 +63,7 @@ contract STO is ISTO, STOStorage, Module, Pausable { require(_fundRaiseTypes.length > 0, "Raise type is not specified"); fundRaiseTypes[uint8(FundRaiseType.ETH)] = false; fundRaiseTypes[uint8(FundRaiseType.POLY)] = false; - fundRaiseTypes[uint8(FundRaiseType.DAI)] = false; + fundRaiseTypes[uint8(FundRaiseType.SC)] = false; for (uint8 j = 0; j < _fundRaiseTypes.length; j++) { fundRaiseTypes[uint8(_fundRaiseTypes[j])] = true; } diff --git a/contracts/modules/STO/USDTieredSTO.sol b/contracts/modules/STO/USDTieredSTO.sol index cea3a45f5..20cce3e87 100644 --- a/contracts/modules/STO/USDTieredSTO.sol +++ b/contracts/modules/STO/USDTieredSTO.sol @@ -6,17 +6,16 @@ import "../../interfaces/IOracle.sol"; import "../../RegistryUpdater.sol"; import "../../libraries/DecimalMath.sol"; import "openzeppelin-solidity/contracts/math/SafeMath.sol"; -import "openzeppelin-solidity/contracts/utils/ReentrancyGuard.sol"; -import "./USDTieredSTOStorage.sol"; +import "../../storage/USDTieredSTOStorage.sol"; /** * @title STO module for standard capped crowdsale */ -contract USDTieredSTO is USDTieredSTOStorage, STO, ReentrancyGuard { +contract USDTieredSTO is USDTieredSTOStorage, STO { using SafeMath for uint256; - string public constant POLY_ORACLE = "PolyUsdOracle"; - string public constant ETH_ORACLE = "EthUsdOracle"; + string internal constant POLY_ORACLE = "PolyUsdOracle"; + string internal constant ETH_ORACLE = "EthUsdOracle"; //////////// // Events // @@ -43,7 +42,7 @@ contract USDTieredSTO is USDTieredSTOStorage, STO, ReentrancyGuard { uint256 _rate ); event ReserveTokenMint(address indexed _owner, address indexed _wallet, uint256 _tokens, uint256 _latestTier); - event SetAddresses(address indexed _wallet, address indexed _reserveWallet, address indexed _usdToken); + event SetAddresses(address indexed _wallet, address indexed _reserveWallet, address[] _usdTokens); event SetLimits(uint256 _nonAccreditedLimitUSD, uint256 _minimumInvestmentUSD); event SetTimes(uint256 _startTime, uint256 _endTime); event SetTiers( @@ -69,8 +68,8 @@ contract USDTieredSTO is USDTieredSTOStorage, STO, ReentrancyGuard { _; } - modifier validDAI() { - require(fundRaiseTypes[uint8(FundRaiseType.DAI)], "DAI not allowed"); + modifier validSC(address _usdToken) { + require(fundRaiseTypes[uint8(FundRaiseType.SC)] && usdTokenEnabled[_usdToken], "USD not allowed"); _; } @@ -93,7 +92,7 @@ contract USDTieredSTO is USDTieredSTOStorage, STO, ReentrancyGuard { * @param _fundRaiseTypes Types of currency used to collect the funds * @param _wallet Ethereum account address to hold the funds * @param _reserveWallet Ethereum account address to receive unsold tokens - * @param _usdToken Contract address of the stable coin + * @param _usdTokens Contract address of the stable coins */ function configure( uint256 _startTime, @@ -107,10 +106,10 @@ contract USDTieredSTO is USDTieredSTOStorage, STO, ReentrancyGuard { FundRaiseType[] memory _fundRaiseTypes, address payable _wallet, address _reserveWallet, - address _usdToken - ) - public - onlyFactory + address[] memory _usdTokens + ) + public + onlyFactory { oracleKeys[bytes32("ETH")][bytes32("USD")] = ETH_ORACLE; oracleKeys[bytes32("POLY")][bytes32("USD")] = POLY_ORACLE; @@ -119,7 +118,7 @@ contract USDTieredSTO is USDTieredSTOStorage, STO, ReentrancyGuard { _modifyTiers(_ratePerTier, _ratePerTierDiscountPoly, _tokensPerTierTotal, _tokensPerTierDiscountPoly); // NB - _setFundRaiseType must come before modifyAddresses _setFundRaiseType(_fundRaiseTypes); - _modifyAddresses(_wallet, _reserveWallet, _usdToken); + _modifyAddresses(_wallet, _reserveWallet, _usdTokens); _modifyLimits(_nonAccreditedLimitUSD, _minimumInvestmentUSD); } @@ -156,9 +155,9 @@ contract USDTieredSTO is USDTieredSTOStorage, STO, ReentrancyGuard { uint256[] calldata _ratePerTierDiscountPoly, uint256[] calldata _tokensPerTierTotal, uint256[] calldata _tokensPerTierDiscountPoly - ) - external - onlyOwner + ) + external + onlyOwner { /*solium-disable-next-line security/no-block-members*/ require(now < startTime, "STO already started"); @@ -180,12 +179,10 @@ contract USDTieredSTO is USDTieredSTOStorage, STO, ReentrancyGuard { * @dev Modifies addresses used as wallet, reserve wallet and usd token * @param _wallet Address of wallet where funds are sent * @param _reserveWallet Address of wallet where unsold tokens are sent - * @param _usdToken Address of usd token (DAI) + * @param _usdTokens Address of usd tokens */ - function modifyAddresses(address payable _wallet, address _reserveWallet, address _usdToken) external onlyOwner { - /*solium-disable-next-line security/no-block-members*/ - require(now < startTime, "STO already started"); - _modifyAddresses(_wallet, _reserveWallet, _usdToken); + function modifyAddresses(address payable _wallet, address _reserveWallet, address[] calldata _usdTokens) external onlyOwner { + _modifyAddresses(_wallet, _reserveWallet, _usdTokens); } function _modifyLimits(uint256 _nonAccreditedLimitUSD, uint256 _minimumInvestmentUSD) internal { @@ -199,13 +196,15 @@ contract USDTieredSTO is USDTieredSTOStorage, STO, ReentrancyGuard { uint256[] memory _ratePerTierDiscountPoly, uint256[] memory _tokensPerTierTotal, uint256[] memory _tokensPerTierDiscountPoly - ) - internal + ) + internal { - require(_tokensPerTierTotal.length > 0, "No tiers provided"); require( - _ratePerTier.length == _tokensPerTierTotal.length && _ratePerTierDiscountPoly.length == _tokensPerTierTotal.length && _tokensPerTierDiscountPoly.length == _tokensPerTierTotal.length, - "Tier data length mismatch" + _tokensPerTierTotal.length > 0 && + _ratePerTier.length == _tokensPerTierTotal.length && + _ratePerTierDiscountPoly.length == _tokensPerTierTotal.length && + _tokensPerTierDiscountPoly.length == _tokensPerTierTotal.length, + "Invalid Input" ); delete tiers; for (uint256 i = 0; i < _ratePerTier.length; i++) { @@ -226,15 +225,24 @@ contract USDTieredSTO is USDTieredSTOStorage, STO, ReentrancyGuard { emit SetTimes(_startTime, _endTime); } - function _modifyAddresses(address payable _wallet, address _reserveWallet, address _usdToken) internal { + function _modifyAddresses(address payable _wallet, address _reserveWallet, address[] memory _usdTokens) internal { require(_wallet != address(0) && _reserveWallet != address(0), "Invalid wallet"); - if (fundRaiseTypes[uint8(FundRaiseType.DAI)]) { - require(_usdToken != address(0), "Invalid usdToken"); - } wallet = _wallet; reserveWallet = _reserveWallet; - usdToken = IERC20(_usdToken); - emit SetAddresses(_wallet, _reserveWallet, _usdToken); + _modifyUSDTokens(_usdTokens); + } + + function _modifyUSDTokens(address[] memory _usdTokens) internal { + uint256 i; + for(i = 0; i < usdTokens.length; i++) { + usdTokenEnabled[usdTokens[i]] = false; + } + usdTokens = _usdTokens; + for(i = 0; i < _usdTokens.length; i++) { + require(_usdTokens[i] != address(0), "Invalid USD token"); + usdTokenEnabled[_usdTokens[i]] = true; + } + emit SetAddresses(wallet, reserveWallet, _usdTokens); } //////////////////// @@ -246,7 +254,7 @@ contract USDTieredSTO is USDTieredSTOStorage, STO, ReentrancyGuard { * @notice Reserve address must be whitelisted to successfully finalize */ function finalize() public onlyOwner { - require(!isFinalized, "STO is already finalized"); + require(!isFinalized, "STO already finalized"); isFinalized = true; uint256 tempReturned; uint256 tempSold; @@ -259,7 +267,7 @@ contract USDTieredSTO is USDTieredSTOStorage, STO, ReentrancyGuard { tiers[i].mintedTotal = tiers[i].tokenTotal; } } - require(ISecurityToken(securityToken).mint(reserveWallet, tempReturned), "Error in minting"); + require(ISecurityToken(securityToken).mint(reserveWallet, tempReturned), "Minting Failed"); emit ReserveTokenMint(msg.sender, reserveWallet, tempReturned, currentTier); finalAmountReturned = tempReturned; totalTokensSold = tempSold; @@ -273,7 +281,12 @@ contract USDTieredSTO is USDTieredSTOStorage, STO, ReentrancyGuard { function changeAccredited(address[] memory _investors, bool[] memory _accredited) public onlyOwner { require(_investors.length == _accredited.length, "Array length mismatch"); for (uint256 i = 0; i < _investors.length; i++) { - accredited[_investors[i]] = _accredited[i]; + if (_accredited[i]) { + investors[_investors[i]].accredited = uint8(1); + } else { + investors[_investors[i]].accredited = uint8(0); + } + _addToInvestorsList(_investors[i]); emit SetAccredited(_investors[i], _accredited[i]); } } @@ -287,12 +300,36 @@ contract USDTieredSTO is USDTieredSTOStorage, STO, ReentrancyGuard { //nonAccreditedLimitUSDOverride require(_investors.length == _nonAccreditedLimit.length, "Array length mismatch"); for (uint256 i = 0; i < _investors.length; i++) { - require(_nonAccreditedLimit[i] > 0, "Limit = 0"); - nonAccreditedLimitUSDOverride[_investors[i]] = _nonAccreditedLimit[i]; + investors[_investors[i]].nonAccreditedLimitUSDOverride = _nonAccreditedLimit[i]; + _addToInvestorsList(_investors[i]); emit SetNonAccreditedLimit(_investors[i], _nonAccreditedLimit[i]); } } + function _addToInvestorsList(address _investor) internal { + if (investors[_investor].seen == uint8(0)) { + investors[_investor].seen = uint8(1); + investorsList.push(_investor); + } + } + + /** + * @notice Returns investor accredited & non-accredited override informatiomn + * @return address[] list of all configured investors + * @return bool[] whether investor is accredited + * @return uint256[] any USD overrides for non-accredited limits for the investor + */ + function getAccreditedData() external view returns (address[] memory, bool[] memory, uint256[] memory) { + bool[] memory accrediteds = new bool[](investorsList.length); + uint256[] memory nonAccreditedLimitUSDOverrides = new uint256[](investorsList.length); + uint256 i; + for (i = 0; i < investorsList.length; i++) { + accrediteds[i] = (investors[investorsList[i]].accredited == uint8(0)? false: true); + nonAccreditedLimitUSDOverrides[i] = investors[investorsList[i]].nonAccreditedLimitUSDOverride; + } + return (investorsList, accrediteds, nonAccreditedLimitUSDOverrides); + } + /** * @notice Function to set allowBeneficialInvestments (allow beneficiary to be different to funder) * @param _allowBeneficialInvestments Boolean to allow or disallow beneficial investments @@ -315,16 +352,16 @@ contract USDTieredSTO is USDTieredSTOStorage, STO, ReentrancyGuard { } // Buy functions without rate restriction - function buyWithETH(address _beneficiary) external payable { - buyWithETHRateLimited(_beneficiary, 0); + function buyWithETH(address _beneficiary) external payable returns (uint256, uint256, uint256) { + return buyWithETHRateLimited(_beneficiary, 0); } - function buyWithPOLY(address _beneficiary, uint256 _investedPOLY) external { - buyWithPOLYRateLimited(_beneficiary, _investedPOLY, 0); + function buyWithPOLY(address _beneficiary, uint256 _investedPOLY) external returns (uint256, uint256, uint256) { + return buyWithPOLYRateLimited(_beneficiary, _investedPOLY, 0); } - function buyWithUSD(address _beneficiary, uint256 _investedDAI) external { - buyWithUSDRateLimited(_beneficiary, _investedDAI, 0); + function buyWithUSD(address _beneficiary, uint256 _investedSC, IERC20 _usdToken) external returns (uint256, uint256, uint256) { + return buyWithUSDRateLimited(_beneficiary, _investedSC, 0, _usdToken); } /** @@ -332,7 +369,7 @@ contract USDTieredSTO is USDTieredSTOStorage, STO, ReentrancyGuard { * @param _beneficiary Address where security tokens will be sent * @param _minTokens Minumum number of tokens to buy or else revert */ - function buyWithETHRateLimited(address _beneficiary, uint256 _minTokens) public payable validETH { + function buyWithETHRateLimited(address _beneficiary, uint256 _minTokens) public payable validETH returns (uint256, uint256, uint256) { uint256 rate = getRate(FundRaiseType.ETH); uint256 initialMinted = getTokensMinted(); (uint256 spentUSD, uint256 spentValue) = _buyTokens(_beneficiary, msg.value, rate, FundRaiseType.ETH); @@ -345,6 +382,7 @@ contract USDTieredSTO is USDTieredSTOStorage, STO, ReentrancyGuard { // Refund excess ETH to investor wallet msg.sender.transfer(msg.value.sub(spentValue)); emit FundsReceived(msg.sender, _beneficiary, spentUSD, FundRaiseType.ETH, msg.value, spentValue, rate); + return (spentUSD, spentValue, getTokensMinted().sub(initialMinted)); } /** @@ -353,22 +391,24 @@ contract USDTieredSTO is USDTieredSTOStorage, STO, ReentrancyGuard { * @param _investedPOLY Amount of POLY invested * @param _minTokens Minumum number of tokens to buy or else revert */ - function buyWithPOLYRateLimited(address _beneficiary, uint256 _investedPOLY, uint256 _minTokens) public validPOLY { - _buyWithTokens(_beneficiary, _investedPOLY, FundRaiseType.POLY, _minTokens); + function buyWithPOLYRateLimited(address _beneficiary, uint256 _investedPOLY, uint256 _minTokens) public validPOLY returns (uint256, uint256, uint256) { + return _buyWithTokens(_beneficiary, _investedPOLY, FundRaiseType.POLY, _minTokens, polyToken); } /** - * @notice Purchase tokens using DAI + * @notice Purchase tokens using Stable coins * @param _beneficiary Address where security tokens will be sent - * @param _investedDAI Amount of DAI invested + * @param _investedSC Amount of Stable coins invested * @param _minTokens Minumum number of tokens to buy or else revert + * @param _usdToken Address of USD stable coin to buy tokens with */ - function buyWithUSDRateLimited(address _beneficiary, uint256 _investedDAI, uint256 _minTokens) public validDAI { - _buyWithTokens(_beneficiary, _investedDAI, FundRaiseType.DAI, _minTokens); + function buyWithUSDRateLimited(address _beneficiary, uint256 _investedSC, uint256 _minTokens, IERC20 _usdToken) + public validSC(address(_usdToken)) returns (uint256, uint256, uint256) + { + return _buyWithTokens(_beneficiary, _investedSC, FundRaiseType.SC, _minTokens, _usdToken); } - function _buyWithTokens(address _beneficiary, uint256 _tokenAmount, FundRaiseType _fundRaiseType, uint256 _minTokens) internal { - require(_fundRaiseType == FundRaiseType.POLY || _fundRaiseType == FundRaiseType.DAI, "Invalid raise type"); + function _buyWithTokens(address _beneficiary, uint256 _tokenAmount, FundRaiseType _fundRaiseType, uint256 _minTokens, IERC20 _token) internal returns (uint256, uint256, uint256) { uint256 initialMinted = getTokensMinted(); uint256 rate = getRate(_fundRaiseType); (uint256 spentUSD, uint256 spentValue) = _buyTokens(_beneficiary, _tokenAmount, rate, _fundRaiseType); @@ -376,28 +416,29 @@ contract USDTieredSTO is USDTieredSTOStorage, STO, ReentrancyGuard { // Modify storage investorInvested[_beneficiary][uint8(_fundRaiseType)] = investorInvested[_beneficiary][uint8(_fundRaiseType)].add(spentValue); fundsRaised[uint8(_fundRaiseType)] = fundsRaised[uint8(_fundRaiseType)].add(spentValue); - // Forward DAI to issuer wallet - IERC20 token = _fundRaiseType == FundRaiseType.POLY ? polyToken : usdToken; - require(token.transferFrom(msg.sender, wallet, spentValue), "Transfer failed"); + if(address(_token) != address(polyToken)) + stableCoinsRaised[address(_token)] = stableCoinsRaised[address(_token)].add(spentValue); + // Forward coins to issuer wallet + require(_token.transferFrom(msg.sender, wallet, spentValue), "Transfer failed"); emit FundsReceived(msg.sender, _beneficiary, spentUSD, _fundRaiseType, _tokenAmount, spentValue, rate); + return (spentUSD, spentValue, getTokensMinted().sub(initialMinted)); } /** * @notice Low level token purchase * @param _beneficiary Address where security tokens will be sent - * @param _investmentValue Amount of POLY, ETH or DAI invested - * @param _fundRaiseType Fund raise type (POLY, ETH, DAI) + * @param _investmentValue Amount of POLY, ETH or Stable coins invested + * @param _fundRaiseType Fund raise type (POLY, ETH, SC) */ function _buyTokens( address _beneficiary, uint256 _investmentValue, uint256 _rate, FundRaiseType _fundRaiseType - ) - internal - nonReentrant - whenNotPaused - returns(uint256 spentUSD, uint256 spentValue) + ) + internal + whenNotPaused + returns(uint256 spentUSD, uint256 spentValue) { if (!allowBeneficialInvestments) { require(_beneficiary == msg.sender, "Beneficiary != funder"); @@ -410,19 +451,22 @@ contract USDTieredSTO is USDTieredSTOStorage, STO, ReentrancyGuard { bool gotoNextTier; uint256 tempSpentUSD; // Update current tier if needed - if (currentTier != i) currentTier = i; + if (currentTier != i) + currentTier = i; // If there are tokens remaining, process investment if (tiers[i].mintedTotal < tiers[i].tokenTotal) { (tempSpentUSD, gotoNextTier) = _calculateTier(_beneficiary, i, allowedUSD.sub(spentUSD), _fundRaiseType); spentUSD = spentUSD.add(tempSpentUSD); // If all funds have been spent, exit the loop - if (!gotoNextTier) break; + if (!gotoNextTier) + break; } } // Modify storage if (spentUSD > 0) { - if (investorInvestedUSD[_beneficiary] == 0) investorCount = investorCount + 1; + if (investorInvestedUSD[_beneficiary] == 0) + investorCount = investorCount + 1; investorInvestedUSD[_beneficiary] = investorInvestedUSD[_beneficiary].add(spentUSD); fundsRaisedUSD = fundsRaisedUSD.add(spentUSD); } @@ -430,72 +474,39 @@ contract USDTieredSTO is USDTieredSTOStorage, STO, ReentrancyGuard { spentValue = DecimalMath.div(spentUSD, _rate); } - /** - * @notice Getter function for buyer to calculate how many tokens will they get - * @param _beneficiary Address where security tokens are to be sent - * @param _investmentValue Amount of POLY, ETH or DAI invested - * @param _fundRaiseType Fund raise type (POLY, ETH, DAI) - */ - function buyTokensView(address _beneficiary, uint256 _investmentValue, FundRaiseType _fundRaiseType) public view returns( - uint256 spentUSD, - uint256 spentValue, - uint256 tokensMinted - ) { - require( - _fundRaiseType == FundRaiseType.POLY || _fundRaiseType == FundRaiseType.DAI || _fundRaiseType == FundRaiseType.ETH, - "Invalid raise type" - ); - uint256 rate = getRate(_fundRaiseType); - uint256 originalUSD = DecimalMath.mul(rate, _investmentValue); - uint256 allowedUSD = _buyTokensChecks(_beneficiary, _investmentValue, originalUSD); - - // Iterate over each tier and process payment - for (uint256 i = currentTier; i < tiers.length; i++) { - bool gotoNextTier; - uint256 tempSpentUSD; - uint256 tempTokensMinted; - // If there are tokens remaining, process investment - if (tiers[i].mintedTotal < tiers[i].tokenTotal) { - (tempSpentUSD, gotoNextTier, tempTokensMinted) = _calculateTierView(i, allowedUSD.sub(spentUSD), _fundRaiseType); - spentUSD = spentUSD.add(tempSpentUSD); - tokensMinted = tokensMinted.add(tempTokensMinted); - // If all funds have been spent, exit the loop - if (!gotoNextTier) break; - } - } - - spentValue = DecimalMath.div(spentUSD, rate); - } - function _buyTokensChecks( - address _beneficiary, - uint256 _investmentValue, + address _beneficiary, + uint256 _investmentValue, uint256 investedUSD - ) - internal - view - returns(uint256 netInvestedUSD) + ) + internal + view + returns(uint256 netInvestedUSD) { require(isOpen(), "STO not open"); - require(_investmentValue > 0, "No funds were sent"); + require(_investmentValue > 0, "No funds sent"); // Check for minimum investment - require(investedUSD.add(investorInvestedUSD[_beneficiary]) >= minimumInvestmentUSD, "Total investment < minimumInvestmentUSD"); + require(investedUSD.add(investorInvestedUSD[_beneficiary]) >= minimumInvestmentUSD, "Investment < min"); netInvestedUSD = investedUSD; // Check for non-accredited cap - if (!accredited[_beneficiary]) { - uint256 investorLimitUSD = (nonAccreditedLimitUSDOverride[_beneficiary] == 0) ? nonAccreditedLimitUSD : nonAccreditedLimitUSDOverride[_beneficiary]; + if (investors[_beneficiary].accredited == uint8(0)) { + uint256 investorLimitUSD = (investors[_beneficiary].nonAccreditedLimitUSDOverride == 0) ? nonAccreditedLimitUSD : investors[_beneficiary].nonAccreditedLimitUSDOverride; require(investorInvestedUSD[_beneficiary] < investorLimitUSD, "Over Non-accredited investor limit"); - if (investedUSD.add(investorInvestedUSD[_beneficiary]) > investorLimitUSD) netInvestedUSD = investorLimitUSD.sub( - investorInvestedUSD[_beneficiary] - ); + if (investedUSD.add(investorInvestedUSD[_beneficiary]) > investorLimitUSD) + netInvestedUSD = investorLimitUSD.sub(investorInvestedUSD[_beneficiary]); } } - function _calculateTier(address _beneficiary, uint256 _tier, uint256 _investedUSD, FundRaiseType _fundRaiseType) internal returns( - uint256 spentUSD, - bool gotoNextTier - ) { + function _calculateTier( + address _beneficiary, + uint256 _tier, + uint256 _investedUSD, + FundRaiseType _fundRaiseType + ) + internal + returns(uint256 spentUSD, bool gotoNextTier) + { // First purchase any discounted tokens if POLY investment uint256 tierSpentUSD; uint256 tierPurchasedTokens; @@ -505,102 +516,36 @@ contract USDTieredSTO is USDTieredSTOStorage, STO, ReentrancyGuard { if ((_fundRaiseType == FundRaiseType.POLY) && (tierData.tokensDiscountPoly > tierData.mintedDiscountPoly)) { uint256 discountRemaining = tierData.tokensDiscountPoly.sub(tierData.mintedDiscountPoly); uint256 totalRemaining = tierData.tokenTotal.sub(tierData.mintedTotal); - if (totalRemaining < discountRemaining) (spentUSD, tierPurchasedTokens, gotoNextTier) = _purchaseTier( - _beneficiary, - tierData.rateDiscountPoly, - totalRemaining, - investedUSD, - _tier - ); - else (spentUSD, tierPurchasedTokens, gotoNextTier) = _purchaseTier( - _beneficiary, - tierData.rateDiscountPoly, - discountRemaining, - investedUSD, - _tier - ); + if (totalRemaining < discountRemaining) + (spentUSD, tierPurchasedTokens, gotoNextTier) = _purchaseTier(_beneficiary, tierData.rateDiscountPoly, totalRemaining, investedUSD, _tier); + else + (spentUSD, tierPurchasedTokens, gotoNextTier) = _purchaseTier(_beneficiary, tierData.rateDiscountPoly, discountRemaining, investedUSD, _tier); investedUSD = investedUSD.sub(spentUSD); tierData.mintedDiscountPoly = tierData.mintedDiscountPoly.add(tierPurchasedTokens); tierData.minted[uint8(_fundRaiseType)] = tierData.minted[uint8(_fundRaiseType)].add(tierPurchasedTokens); tierData.mintedTotal = tierData.mintedTotal.add(tierPurchasedTokens); } // Now, if there is any remaining USD to be invested, purchase at non-discounted rate - if (investedUSD > 0 && tierData.tokenTotal.sub( - tierData.mintedTotal - ) > 0 && (_fundRaiseType != FundRaiseType.POLY || tierData.tokensDiscountPoly <= tierData.mintedDiscountPoly)) { - (tierSpentUSD, tierPurchasedTokens, gotoNextTier) = _purchaseTier( - _beneficiary, - tierData.rate, - tierData.tokenTotal.sub(tierData.mintedTotal), - investedUSD, - _tier - ); + if (investedUSD > 0 && + tierData.tokenTotal.sub(tierData.mintedTotal) > 0 && + (_fundRaiseType != FundRaiseType.POLY || tierData.tokensDiscountPoly <= tierData.mintedDiscountPoly) + ) { + (tierSpentUSD, tierPurchasedTokens, gotoNextTier) = _purchaseTier(_beneficiary, tierData.rate, tierData.tokenTotal.sub(tierData.mintedTotal), investedUSD, _tier); spentUSD = spentUSD.add(tierSpentUSD); tierData.minted[uint8(_fundRaiseType)] = tierData.minted[uint8(_fundRaiseType)].add(tierPurchasedTokens); tierData.mintedTotal = tierData.mintedTotal.add(tierPurchasedTokens); } } - function _calculateTierView(uint256 _tier, uint256 _investedUSD, FundRaiseType _fundRaiseType) internal view returns( - uint256 spentUSD, - bool gotoNextTier, - uint256 tokensMinted - ) { - // First purchase any discounted tokens if POLY investment - uint256 tierSpentUSD; - uint256 tierPurchasedTokens; - Tier storage tierData = tiers[_tier]; - // Check whether there are any remaining discounted tokens - if ((_fundRaiseType == FundRaiseType.POLY) && (tierData.tokensDiscountPoly > tierData.mintedDiscountPoly)) { - uint256 discountRemaining = tierData.tokensDiscountPoly.sub(tierData.mintedDiscountPoly); - uint256 totalRemaining = tierData.tokenTotal.sub(tierData.mintedTotal); - if (totalRemaining < discountRemaining) (spentUSD, tokensMinted, gotoNextTier) = _purchaseTierAmount( - tierData.rateDiscountPoly, - totalRemaining, - _investedUSD - ); - else (spentUSD, tokensMinted, gotoNextTier) = _purchaseTierAmount(tierData.rateDiscountPoly, discountRemaining, _investedUSD); - _investedUSD = _investedUSD.sub(spentUSD); - } - // Now, if there is any remaining USD to be invested, purchase at non-discounted rate - if (_investedUSD > 0 && tierData.tokenTotal.sub( - tierData.mintedTotal.add(tokensMinted) - ) > 0 && (_fundRaiseType != FundRaiseType.POLY || tierData.tokensDiscountPoly <= tierData.mintedDiscountPoly)) { - (tierSpentUSD, tierPurchasedTokens, gotoNextTier) = _purchaseTierAmount( - tierData.rate, - tierData.tokenTotal.sub(tierData.mintedTotal), - _investedUSD - ); - spentUSD = spentUSD.add(tierSpentUSD); - tokensMinted = tokensMinted.add(tierPurchasedTokens); - } - } - function _purchaseTier( address _beneficiary, uint256 _tierPrice, uint256 _tierRemaining, uint256 _investedUSD, uint256 _tier - ) - internal - returns(uint256 spentUSD, uint256 purchasedTokens, bool gotoNextTier) - { - (spentUSD, purchasedTokens, gotoNextTier) = _purchaseTierAmount(_tierPrice, _tierRemaining, _investedUSD); - if (purchasedTokens > 0) { - require(ISecurityToken(securityToken).mint(_beneficiary, purchasedTokens), "Error in minting"); - emit TokenPurchase(msg.sender, _beneficiary, purchasedTokens, spentUSD, _tierPrice, _tier); - } - } - - function _purchaseTierAmount( - uint256 _tierPrice, - uint256 _tierRemaining, - uint256 _investedUSD - ) - internal - view - returns(uint256 spentUSD,uint256 purchasedTokens,bool gotoNextTier) + ) + internal + returns(uint256 spentUSD, uint256 purchasedTokens, bool gotoNextTier) { uint256 maximumTokens = DecimalMath.div(_investedUSD, _tierPrice); uint256 granularity = ISecurityToken(securityToken).granularity(); @@ -618,6 +563,10 @@ contract USDTieredSTO is USDTieredSTOStorage, STO, ReentrancyGuard { spentUSD = DecimalMath.mul(maximumTokens, _tierPrice); purchasedTokens = maximumTokens; } + if (purchasedTokens > 0) { + require(ISecurityToken(securityToken).mint(_beneficiary, purchasedTokens), "Mint failed"); + emit TokenPurchase(msg.sender, _beneficiary, purchasedTokens, spentUSD, _tierPrice, _tier); + } } ///////////// @@ -629,12 +578,9 @@ contract USDTieredSTO is USDTieredSTOStorage, STO, ReentrancyGuard { * @return bool Whether the STO is accepting investments */ function isOpen() public view returns(bool) { - if (isFinalized) return false; - /*solium-disable-next-line security/no-block-members*/ - if (now < startTime) return false; - /*solium-disable-next-line security/no-block-members*/ - if (now >= endTime) return false; - if (capReached()) return false; + /*solium-disable-next-line security/no-block-members*/ + if (isFinalized || now < startTime || now >= endTime || capReached()) + return false; return true; } @@ -642,7 +588,7 @@ contract USDTieredSTO is USDTieredSTOStorage, STO, ReentrancyGuard { * @notice Checks whether the cap has been reached. * @return bool Whether the cap was reached */ - function capReached() public view returns(bool) { + function capReached() public view returns (bool) { if (isFinalized) { return (finalAmountReturned == 0); } @@ -653,15 +599,13 @@ contract USDTieredSTO is USDTieredSTOStorage, STO, ReentrancyGuard { * @dev returns current conversion rate of funds * @param _fundRaiseType Fund raise type to get rate of */ - function getRate(FundRaiseType _fundRaiseType) public view returns(uint256) { + function getRate(FundRaiseType _fundRaiseType) public view returns (uint256) { if (_fundRaiseType == FundRaiseType.ETH) { return IOracle(_getOracle(bytes32("ETH"), bytes32("USD"))).getPrice(); } else if (_fundRaiseType == FundRaiseType.POLY) { return IOracle(_getOracle(bytes32("POLY"), bytes32("USD"))).getPrice(); - } else if (_fundRaiseType == FundRaiseType.DAI) { - return 1 * 10 ** 18; - } else { - revert("Incorrect funding"); + } else if (_fundRaiseType == FundRaiseType.SC) { + return 10**18; } } @@ -672,8 +616,7 @@ contract USDTieredSTO is USDTieredSTOStorage, STO, ReentrancyGuard { * @return uint256 Value in USD */ function convertToUSD(FundRaiseType _fundRaiseType, uint256 _amount) public view returns(uint256) { - uint256 rate = getRate(_fundRaiseType); - return DecimalMath.mul(_amount, rate); + return DecimalMath.mul(_amount, getRate(_fundRaiseType)); } /** @@ -683,48 +626,38 @@ contract USDTieredSTO is USDTieredSTOStorage, STO, ReentrancyGuard { * @return uint256 Value in ETH or POLY */ function convertFromUSD(FundRaiseType _fundRaiseType, uint256 _amount) public view returns(uint256) { - uint256 rate = getRate(_fundRaiseType); - return DecimalMath.div(_amount, rate); + return DecimalMath.div(_amount, getRate(_fundRaiseType)); } /** * @notice Return the total no. of tokens sold * @return uint256 Total number of tokens sold */ - function getTokensSold() external view returns (uint256) { - return _getTokensSold(); - } - - function _getTokensSold() internal view returns (uint256) { + function getTokensSold() public view returns (uint256) { if (isFinalized) return totalTokensSold; - else - return getTokensMinted(); + return getTokensMinted(); } /** * @notice Return the total no. of tokens minted * @return uint256 Total number of tokens minted */ - function getTokensMinted() public view returns(uint256) { - uint256 tokensMinted; + function getTokensMinted() public view returns (uint256 tokensMinted) { for (uint256 i = 0; i < tiers.length; i++) { tokensMinted = tokensMinted.add(tiers[i].mintedTotal); } - return tokensMinted; } /** * @notice Return the total no. of tokens sold for the given fund raise type - * param _fundRaiseType The fund raising currency (e.g. ETH, POLY, DAI) to calculate sold tokens for + * param _fundRaiseType The fund raising currency (e.g. ETH, POLY, SC) to calculate sold tokens for * @return uint256 Total number of tokens sold for ETH */ - function getTokensSoldFor(FundRaiseType _fundRaiseType) public view returns(uint256) { - uint256 tokensSold; + function getTokensSoldFor(FundRaiseType _fundRaiseType) external view returns (uint256 tokensSold) { for (uint256 i = 0; i < tiers.length; i++) { tokensSold = tokensSold.add(tiers[i].minted[uint8(_fundRaiseType)]); } - return tokensSold; } /** @@ -732,12 +665,11 @@ contract USDTieredSTO is USDTieredSTOStorage, STO, ReentrancyGuard { * param _tier The tier to return minted tokens for * @return uint256[] array of minted tokens in each fund raise type */ - function getTokensMintedByTier(uint256 _tier) public view returns(uint256[] memory) { - require(_tier < tiers.length, "Invalid tier"); + function getTokensMintedByTier(uint256 _tier) external view returns(uint256[] memory) { uint256[] memory tokensMinted = new uint256[](3); tokensMinted[0] = tiers[_tier].minted[uint8(FundRaiseType.ETH)]; tokensMinted[1] = tiers[_tier].minted[uint8(FundRaiseType.POLY)]; - tokensMinted[2] = tiers[_tier].minted[uint8(FundRaiseType.DAI)]; + tokensMinted[2] = tiers[_tier].minted[uint8(FundRaiseType.SC)]; return tokensMinted; } @@ -746,12 +678,11 @@ contract USDTieredSTO is USDTieredSTOStorage, STO, ReentrancyGuard { * param _tier The tier to calculate sold tokens for * @return uint256 Total number of tokens sold in the tier */ - function getTokensSoldByTier(uint256 _tier) public view returns(uint256) { - require(_tier < tiers.length, "Incorrect tier"); + function getTokensSoldByTier(uint256 _tier) external view returns (uint256) { uint256 tokensSold; tokensSold = tokensSold.add(tiers[_tier].minted[uint8(FundRaiseType.ETH)]); tokensSold = tokensSold.add(tiers[_tier].minted[uint8(FundRaiseType.POLY)]); - tokensSold = tokensSold.add(tiers[_tier].minted[uint8(FundRaiseType.DAI)]); + tokensSold = tokensSold.add(tiers[_tier].minted[uint8(FundRaiseType.SC)]); return tokensSold; } @@ -759,15 +690,22 @@ contract USDTieredSTO is USDTieredSTOStorage, STO, ReentrancyGuard { * @notice Return the total no. of tiers * @return uint256 Total number of tiers */ - function getNumberOfTiers() public view returns(uint256) { + function getNumberOfTiers() external view returns (uint256) { return tiers.length; } + /** + * @notice Return the usd tokens accepted by the STO + * @return address[] usd tokens + */ + function getUsdTokens() external view returns (address[] memory) { + return usdTokens; + } + /** * @notice Return the permissions flag that are associated with STO */ - function getPermissions() public view returns(bytes32[] memory) { - bytes32[] memory allPermissions = new bytes32[](0); + function getPermissions() public view returns(bytes32[] memory allPermissions) { return allPermissions; } @@ -781,30 +719,19 @@ contract USDTieredSTO is USDTieredSTOStorage, STO, ReentrancyGuard { * @return Amount of funds raised * @return Number of individual investors this STO have. * @return Amount of tokens sold. - * @return Array of bools to show if funding is allowed in ETH, POLY, DAI respectively + * @return Array of bools to show if funding is allowed in ETH, POLY, SC respectively */ - function getSTODetails() public view returns( - uint256, - uint256, - uint256, - uint256[] memory, - uint256[] memory, - uint256, - uint256, - uint256, - bool[] memory - ) - { + function getSTODetails() external view returns(uint256, uint256, uint256, uint256[] memory, uint256[] memory, uint256, uint256, uint256, bool[] memory) { uint256[] memory cap = new uint256[](tiers.length); uint256[] memory rate = new uint256[](tiers.length); - for (uint256 i = 0; i < tiers.length; i++) { + for(uint256 i = 0; i < tiers.length; i++) { cap[i] = tiers[i].tokenTotal; rate[i] = tiers[i].rate; } bool[] memory _fundRaiseTypes = new bool[](3); _fundRaiseTypes[0] = fundRaiseTypes[uint8(FundRaiseType.ETH)]; _fundRaiseTypes[1] = fundRaiseTypes[uint8(FundRaiseType.POLY)]; - _fundRaiseTypes[2] = fundRaiseTypes[uint8(FundRaiseType.DAI)]; + _fundRaiseTypes[2] = fundRaiseTypes[uint8(FundRaiseType.SC)]; return ( startTime, endTime, @@ -813,7 +740,7 @@ contract USDTieredSTO is USDTieredSTOStorage, STO, ReentrancyGuard { rate, fundsRaisedUSD, investorCount, - _getTokensSold(), + getTokensSold(), _fundRaiseTypes ); } @@ -823,7 +750,7 @@ contract USDTieredSTO is USDTieredSTOStorage, STO, ReentrancyGuard { * @return bytes4 Configure function signature */ function getInitFunction() public pure returns(bytes4) { - return 0xb0ff041e; + return this.configure.selector; } function _getOracle(bytes32 _currency, bytes32 _denominatedCurrency) internal view returns(address) { diff --git a/contracts/modules/STO/USDTieredSTOFactory.sol b/contracts/modules/STO/USDTieredSTOFactory.sol index 4ad558144..e691bce09 100644 --- a/contracts/modules/STO/USDTieredSTOFactory.sol +++ b/contracts/modules/STO/USDTieredSTOFactory.sol @@ -22,9 +22,9 @@ contract USDTieredSTOFactory is ModuleFactory { uint256 _usageCost, uint256 _subscriptionCost, address _logicContract - ) - public - ModuleFactory(_setupCost, _usageCost, _subscriptionCost) + ) + public + ModuleFactory(_setupCost, _usageCost, _subscriptionCost) { require(_logicContract != address(0), "0x address is not allowed"); logicContract = _logicContract; @@ -52,7 +52,7 @@ contract USDTieredSTOFactory is ModuleFactory { require(success, "Unsuccessfull call"); /*solium-disable-next-line security/no-block-members*/ emit GenerateModuleFromFactory(usdTieredSTO, getName(), address(this), msg.sender, setupCost, now); - return address(usdTieredSTO); + return usdTieredSTO; } /** diff --git a/contracts/modules/TransferManager/CountTransferManager.sol b/contracts/modules/TransferManager/CountTransferManager.sol index 756a9ce7c..9901ba714 100644 --- a/contracts/modules/TransferManager/CountTransferManager.sol +++ b/contracts/modules/TransferManager/CountTransferManager.sol @@ -2,6 +2,7 @@ pragma solidity ^0.5.0; import "./TransferManager.sol"; import "./CountTransferManagerStorage.sol"; +import "../../interfaces/ISecurityToken.sol"; /** * @title Transfer Manager for limiting maximum number of token holders @@ -29,9 +30,9 @@ contract CountTransferManager is CountTransferManagerStorage, TransferManager { uint256 _amount, bytes calldata /* _data */, bool /* _isTransfer */ - ) - external - returns(Result) + ) + external + returns(Result) { if (!paused) { if (maxHolderCount < ISecurityToken(securityToken).getInvestorCount()) { diff --git a/contracts/modules/TransferManager/GeneralTransferManager.sol b/contracts/modules/TransferManager/GeneralTransferManager.sol index ec88dadc9..b4889dd36 100644 --- a/contracts/modules/TransferManager/GeneralTransferManager.sol +++ b/contracts/modules/TransferManager/GeneralTransferManager.sol @@ -1,8 +1,9 @@ pragma solidity ^0.5.0; import "./TransferManager.sol"; -import "./GeneralTransferManagerStorage.sol"; +import "../../storage/GeneralTransferManagerStorage.sol"; import "openzeppelin-solidity/contracts/math/SafeMath.sol"; +import "../../interfaces/ISecurityToken.sol"; /** * @title Transfer Manager module for core transfer validation functionality @@ -31,9 +32,9 @@ contract GeneralTransferManager is GeneralTransferManagerStorage, TransferManage // if allowAllWhitelistTransfers is TRUE, then _toTime and _fromTime is ignored when sending or receiving tokens // in any case, any investor sending or receiving tokens, must have a _expiryTime in the future event ModifyWhitelist( - address _investor, + address indexed _investor, uint256 _dateAdded, - address _addedBy, + address indexed _addedBy, uint256 _fromTime, uint256 _toTime, uint256 _expiryTime, @@ -174,7 +175,7 @@ contract GeneralTransferManager is GeneralTransferManagerStorage, TransferManage //Anyone on the whitelist can transfer provided the blocknumber is large enough /*solium-disable-next-line security/no-block-members*/ - return ((_onWhitelist(_from) && (adjustedFromTime <= uint64(now))) && (_onWhitelist(_to) && + return ((_onWhitelist(_from) && (adjustedFromTime <= uint64(now))) && (_onWhitelist(_to) && (adjustedToTime <= uint64(now)))) ? Result.VALID : Result.NA; /*solium-disable-line security/no-block-members*/ } return Result.NA; @@ -194,9 +195,9 @@ contract GeneralTransferManager is GeneralTransferManagerStorage, TransferManage uint256 _toTime, uint256 _expiryTime, bool _canBuyFromSTO - ) - public - withPerm(WHITELIST) + ) + public + withPerm(WHITELIST) { _modifyWhitelist(_investor, _fromTime, _toTime, _expiryTime, _canBuyFromSTO); } @@ -272,8 +273,8 @@ contract GeneralTransferManager is GeneralTransferManagerStorage, TransferManage uint8 _v, bytes32 _r, bytes32 _s - ) - public + ) + public { /*solium-disable-next-line security/no-block-members*/ require(_validFrom <= now, "ValidFrom is too early"); diff --git a/contracts/modules/TransferManager/ManualApprovalTransferManager.sol b/contracts/modules/TransferManager/ManualApprovalTransferManager.sol index 3bc258c60..167c80ac6 100644 --- a/contracts/modules/TransferManager/ManualApprovalTransferManager.sol +++ b/contracts/modules/TransferManager/ManualApprovalTransferManager.sol @@ -5,7 +5,7 @@ import "openzeppelin-solidity/contracts/math/SafeMath.sol"; import "./ManualApprovalTransferManagerStorage.sol"; /** - * @title Transfer Manager module for manually approving or blocking transactions between accounts + * @title Transfer Manager module for manually approving transactions between accounts */ contract ManualApprovalTransferManager is ManualApprovalTransferManagerStorage, TransferManager { using SafeMath for uint256; @@ -15,14 +15,17 @@ contract ManualApprovalTransferManager is ManualApprovalTransferManagerStorage, address indexed _to, uint256 _allowance, uint256 _expiryTime, + bytes32 _description, address indexed _addedBy ); - event AddManualBlocking( + event ModifyManualApproval( address indexed _from, address indexed _to, uint256 _expiryTime, - address indexed _addedBy + uint256 _allowance, + bytes32 _description, + address indexed _edittedBy ); event RevokeManualApproval( @@ -31,12 +34,6 @@ contract ManualApprovalTransferManager is ManualApprovalTransferManagerStorage, address indexed _addedBy ); - event RevokeManualBlocking( - address indexed _from, - address indexed _to, - address indexed _addedBy - ); - /** * @notice Constructor * @param _securityToken Address of the security token @@ -52,7 +49,8 @@ contract ManualApprovalTransferManager is ManualApprovalTransferManagerStorage, return bytes4(0); } - /** @notice Used to verify the transfer transaction and allow a manually approved transqaction to bypass other restrictions + /** + * @notice Used to verify the transfer transaction and allow a manually approved transqaction to bypass other restrictions * @param _from Address of the sender * @param _to Address of the receiver * @param _amount The amount of tokens to transfer @@ -64,22 +62,19 @@ contract ManualApprovalTransferManager is ManualApprovalTransferManagerStorage, uint256 _amount, bytes calldata, /* _data */ bool _isTransfer - ) - external - returns(Result) + ) + external + returns(Result) { // function must only be called by the associated security token if _isTransfer == true require(_isTransfer == false || msg.sender == securityToken, "Sender is not the owner"); - // manual blocking takes precidence over manual approval - if (!paused) { - /*solium-disable-next-line security/no-block-members*/ - if (manualBlockings[_from][_to].expiryTime >= now) { - return Result.INVALID; - } - /*solium-disable-next-line security/no-block-members*/ - if ((manualApprovals[_from][_to].expiryTime >= now) && (manualApprovals[_from][_to].allowance >= _amount)) { + + if (!paused && approvalIndex[_from][_to] != 0) { + uint256 index = approvalIndex[_from][_to] - 1; + ManualApproval storage approval = approvals[index]; + if ((approval.expiryTime >= now) && (approval.allowance >= _amount)) { if (_isTransfer) { - manualApprovals[_from][_to].allowance = manualApprovals[_from][_to].allowance.sub(_amount); + approval.allowance = approval.allowance.sub(_amount); } return Result.VALID; } @@ -93,29 +88,155 @@ contract ManualApprovalTransferManager is ManualApprovalTransferManagerStorage, * @param _to is the address to which transfers are approved * @param _allowance is the approved amount of tokens * @param _expiryTime is the time until which the transfer is allowed + * @param _description Description about the manual approval */ - function addManualApproval(address _from, address _to, uint256 _allowance, uint256 _expiryTime) public withPerm(TRANSFER_APPROVAL) { + function addManualApproval( + address _from, + address _to, + uint256 _allowance, + uint256 _expiryTime, + bytes32 _description + ) + external + withPerm(TRANSFER_APPROVAL) + { + _addManualApproval(_from, _to, _allowance, _expiryTime, _description); + } + + function _addManualApproval(address _from, address _to, uint256 _allowance, uint256 _expiryTime, bytes32 _description) internal { require(_to != address(0), "Invalid to address"); - /*solium-disable-next-line security/no-block-members*/ require(_expiryTime > now, "Invalid expiry time"); - require(manualApprovals[_from][_to].allowance == 0, "Approval already exists"); - manualApprovals[_from][_to] = ManualApproval(_allowance, _expiryTime); - emit AddManualApproval(_from, _to, _allowance, _expiryTime, msg.sender); + require(_allowance > 0, "Invalid allowance"); + if (approvalIndex[_from][_to] != 0) { + uint256 index = approvalIndex[_from][_to] - 1; + require(approvals[index].expiryTime < now || approvals[index].allowance == 0, "Approval already exists"); + _revokeManualApproval(_from, _to); + } + approvals.push(ManualApproval(_from, _to, _allowance, _expiryTime, _description)); + approvalIndex[_from][_to] = approvals.length; + emit AddManualApproval(_from, _to, _allowance, _expiryTime, _description, msg.sender); } /** - * @notice Adds a pair of addresses to manual blockings - * @param _from is the address from which transfers are blocked - * @param _to is the address to which transfers are blocked - * @param _expiryTime is the time until which the transfer is blocked + * @notice Adds mutiple manual approvals in batch + * @param _from is the address array from which transfers are approved + * @param _to is the address array to which transfers are approved + * @param _allowances is the array of approved amounts + * @param _expiryTimes is the array of the times until which eath transfer is allowed + * @param _descriptions is the description array for these manual approvals */ - function addManualBlocking(address _from, address _to, uint256 _expiryTime) public withPerm(TRANSFER_APPROVAL) { + function addManualApprovalMulti( + address[] calldata _from, + address[] calldata _to, + uint256[] calldata _allowances, + uint256[] calldata _expiryTimes, + bytes32[] calldata _descriptions + ) + external + withPerm(TRANSFER_APPROVAL) + { + _checkInputLengthArray(_from, _to, _allowances, _expiryTimes, _descriptions); + for (uint256 i = 0; i < _from.length; i++){ + _addManualApproval(_from[i], _to[i], _allowances[i], _expiryTimes[i], _descriptions[i]); + } + } + + /** + * @notice Modify the existing manual approvals + * @param _from is the address from which transfers are approved + * @param _to is the address to which transfers are approved + * @param _expiryTime is the time until which the transfer is allowed + * @param _changedAllowance is the changed allowance + * @param _description Description about the manual approval + * @param _change uint values which tells whether the allowances will be increased (1) or decreased (0) + * or any value when there is no change in allowances + */ + function modifyManualApproval( + address _from, + address _to, + uint256 _expiryTime, + uint256 _changedAllowance, + bytes32 _description, + uint8 _change + ) + external + withPerm(TRANSFER_APPROVAL) + { + _modifyManualApproval(_from, _to, _expiryTime, _changedAllowance, _description, _change); + } + + function _modifyManualApproval( + address _from, + address _to, + uint256 _expiryTime, + uint256 _changedAllowance, + bytes32 _description, + uint8 _change + ) + internal + { require(_to != address(0), "Invalid to address"); /*solium-disable-next-line security/no-block-members*/ require(_expiryTime > now, "Invalid expiry time"); - require(manualBlockings[_from][_to].expiryTime == 0, "Blocking already exists"); - manualBlockings[_from][_to] = ManualBlocking(_expiryTime); - emit AddManualBlocking(_from, _to, _expiryTime, msg.sender); + require(approvalIndex[_from][_to] != 0, "Approval not present"); + uint256 index = approvalIndex[_from][_to] - 1; + ManualApproval storage approval = approvals[index]; + require(approval.allowance != 0 && approval.expiryTime > now, "Not allowed"); + uint256 currentAllowance = approval.allowance; + uint256 newAllowance; + if (_change == 1) { + // Allowance get increased + newAllowance = currentAllowance.add(_changedAllowance); + approval.allowance = newAllowance; + } else if (_change == 0) { + // Allowance get decreased + if (_changedAllowance > currentAllowance) { + newAllowance = 0; + approval.allowance = newAllowance; + } else { + newAllowance = currentAllowance.sub(_changedAllowance); + approval.allowance = newAllowance; + } + } else { + // No change in the Allowance + newAllowance = currentAllowance; + } + // Greedy storage technique + if (approval.expiryTime != _expiryTime) { + approval.expiryTime = _expiryTime; + } + if (approval.description != _description) { + approval.description = _description; + } + emit ModifyManualApproval(_from, _to, _expiryTime, newAllowance, _description, msg.sender); + } + + /** + * @notice Adds mutiple manual approvals in batch + * @param _from is the address array from which transfers are approved + * @param _to is the address array to which transfers are approved + * @param _expiryTimes is the array of the times until which eath transfer is allowed + * @param _changedAllowances is the array of approved amounts + * @param _descriptions is the description array for these manual approvals + * @param _changes Array of uint values which tells whether the allowances will be increased (1) or decreased (0) + * or any value when there is no change in allowances + */ + function modifyManualApprovalMulti( + address[] memory _from, + address[] memory _to, + uint256[] memory _expiryTimes, + uint256[] memory _changedAllowances, + bytes32[] memory _descriptions, + uint8[] memory _changes + ) + public + withPerm(TRANSFER_APPROVAL) + { + _checkInputLengthArray(_from, _to, _changedAllowances, _expiryTimes, _descriptions); + require(_changes.length == _changedAllowances.length, "Input length array mismatch"); + for (uint256 i = 0; i < _from.length; i++) { + _modifyManualApproval(_from[i], _to[i], _expiryTimes[i], _changedAllowances[i], _descriptions[i], _changes[i]); + } } /** @@ -123,21 +244,151 @@ contract ManualApprovalTransferManager is ManualApprovalTransferManagerStorage, * @param _from is the address from which transfers are approved * @param _to is the address to which transfers are approved */ - function revokeManualApproval(address _from, address _to) public withPerm(TRANSFER_APPROVAL) { - require(_to != address(0), "Invalid to address"); - delete manualApprovals[_from][_to]; + function revokeManualApproval(address _from, address _to) external withPerm(TRANSFER_APPROVAL) { + _revokeManualApproval(_from, _to); + } + + function _revokeManualApproval(address _from, address _to) internal { + require(approvalIndex[_from][_to] != 0, "Approval not exist"); + + // find the record in active approvals array & delete it + uint256 index = approvalIndex[_from][_to] - 1; + if (index != approvals.length -1) { + approvals[index] = approvals[approvals.length -1]; + approvalIndex[approvals[index].from][approvals[index].to] = index + 1; + } + delete approvalIndex[_from][_to]; + approvals.length--; emit RevokeManualApproval(_from, _to, msg.sender); } /** - * @notice Removes a pairs of addresses from manual approvals - * @param _from is the address from which transfers are approved - * @param _to is the address to which transfers are approved + * @notice Removes mutiple pairs of addresses from manual approvals + * @param _from is the address array from which transfers are approved + * @param _to is the address array to which transfers are approved */ - function revokeManualBlocking(address _from, address _to) public withPerm(TRANSFER_APPROVAL) { - require(_to != address(0), "Invalid to address"); - delete manualBlockings[_from][_to]; - emit RevokeManualBlocking(_from, _to, msg.sender); + function revokeManualApprovalMulti(address[] calldata _from, address[] calldata _to) external withPerm(TRANSFER_APPROVAL) { + require(_from.length == _to.length, "Input array length mismatch"); + for(uint256 i = 0; i < _from.length; i++){ + _revokeManualApproval(_from[i], _to[i]); + } + } + + function _checkInputLengthArray( + address[] memory _from, + address[] memory _to, + uint256[] memory _expiryTimes, + uint256[] memory _allowances, + bytes32[] memory _descriptions + ) + internal + pure + { + require(_from.length == _to.length && + _to.length == _allowances.length && + _allowances.length == _expiryTimes.length && + _expiryTimes.length == _descriptions.length, + "Input array length mismatch" + ); + } + + /** + * @notice Returns the all active approvals corresponds to an address + * @param _user Address of the holder corresponds to whom list of manual approvals + * need to return + * @return address[] addresses from + * @return address[] addresses to + * @return uint256[] allowances provided to the approvals + * @return uint256[] expiry times provided to the approvals + * @return bytes32[] descriptions provided to the approvals + */ + function getActiveApprovalsToUser(address _user) external view returns(address[] memory, address[] memory, uint256[] memory, uint256[] memory, bytes32[] memory) { + uint256 counter = 0; + for (uint256 i = 0; i < approvals.length; i++) { + if ((approvals[i].from == _user || approvals[i].to == _user) + && approvals[i].expiryTime >= now) + counter ++; + } + + address[] memory from = new address[](counter); + address[] memory to = new address[](counter); + uint256[] memory allowance = new uint256[](counter); + uint256[] memory expiryTime = new uint256[](counter); + bytes32[] memory description = new bytes32[](counter); + + counter = 0; + for (uint256 i = 0; i < approvals.length; i++) { + if ((approvals[i].from == _user || approvals[i].to == _user) + && approvals[i].expiryTime >= now) { + + from[counter]=approvals[i].from; + to[counter]=approvals[i].to; + allowance[counter]=approvals[i].allowance; + expiryTime[counter]=approvals[i].expiryTime; + description[counter]=approvals[i].description; + counter ++; + } + } + return (from, to, allowance, expiryTime, description); + } + + /** + * @notice Get the details of the approval corresponds to _from & _to addresses + * @param _from Address of the sender + * @param _to Address of the receiver + * @return uint256 expiryTime of the approval + * @return uint256 allowance provided to the approval + * @return uint256 Description provided to the approval + */ + function getApprovalDetails(address _from, address _to) external view returns(uint256, uint256, bytes32) { + if (approvalIndex[_from][_to] != 0) { + uint256 index = approvalIndex[_from][_to] - 1; + if (index < approvals.length) { + ManualApproval storage approval = approvals[index]; + return( + approval.expiryTime, + approval.allowance, + approval.description + ); + } + } + return (uint256(0), uint256(0), bytes32(0)); + } + + /** + * @notice Returns the current number of active approvals + */ + function getTotalApprovalsLength() external view returns(uint256) { + return approvals.length; + } + + /** + * @notice Get the details of all approvals + * @return address[] addresses from + * @return address[] addresses to + * @return uint256[] allowances provided to the approvals + * @return uint256[] expiry times provided to the approvals + * @return bytes32[] descriptions provided to the approvals + */ + function getAllApprovals() external view returns(address[] memory, address[] memory, uint256[] memory, uint256[] memory, bytes32[] memory) { + address[] memory from = new address[](approvals.length); + address[] memory to = new address[](approvals.length); + uint256[] memory allowance = new uint256[](approvals.length); + uint256[] memory expiryTime = new uint256[](approvals.length); + bytes32[] memory description = new bytes32[](approvals.length); + + for (uint256 i = 0; i < approvals.length; i++) { + + from[i]=approvals[i].from; + to[i]=approvals[i].to; + allowance[i]=approvals[i].allowance; + expiryTime[i]=approvals[i].expiryTime; + description[i]=approvals[i].description; + + } + + return (from, to, allowance, expiryTime, description); + } /** diff --git a/contracts/modules/TransferManager/ManualApprovalTransferManagerFactory.sol b/contracts/modules/TransferManager/ManualApprovalTransferManagerFactory.sol index ac4ac109c..6d50e9e2f 100644 --- a/contracts/modules/TransferManager/ManualApprovalTransferManagerFactory.sol +++ b/contracts/modules/TransferManager/ManualApprovalTransferManagerFactory.sol @@ -22,15 +22,15 @@ contract ManualApprovalTransferManagerFactory is ModuleFactory { uint256 _usageCost, uint256 _subscriptionCost, address _logicContract - ) - public - ModuleFactory(_setupCost, _usageCost, _subscriptionCost) + ) + public + ModuleFactory(_setupCost, _usageCost, _subscriptionCost) { require(_logicContract != address(0), "Invalid address"); - version = "2.0.1"; + version = "2.1.0"; name = "ManualApprovalTransferManager"; title = "Manual Approval Transfer Manager"; - description = "Manage transfers using single approvals / blocking"; + description = "Manage transfers using single approvals"; compatibleSTVersionRange["lowerBound"] = VersionUtils.pack(uint8(0), uint8(0), uint8(0)); compatibleSTVersionRange["upperBound"] = VersionUtils.pack(uint8(0), uint8(0), uint8(0)); logicContract = _logicContract; @@ -42,9 +42,9 @@ contract ManualApprovalTransferManagerFactory is ModuleFactory { */ function deploy( bytes calldata /* _data */ - ) - external - returns(address) + ) + external + returns(address) { address polyToken = _takeFee(); ManualApprovalTransferManagerProxy manualTransferManager = new ManualApprovalTransferManagerProxy(msg.sender, polyToken, logicContract); @@ -67,7 +67,7 @@ contract ManualApprovalTransferManagerFactory is ModuleFactory { */ function getInstructions() external view returns(string memory) { /*solium-disable-next-line max-len*/ - return "Allows an issuer to set manual approvals or blocks for specific pairs of addresses and amounts. Init function takes no parameters."; + return "Allows an issuer to set manual approvals for specific pairs of addresses and amounts. Init function takes no parameters."; } /** diff --git a/contracts/modules/TransferManager/ManualApprovalTransferManagerStorage.sol b/contracts/modules/TransferManager/ManualApprovalTransferManagerStorage.sol index 6f94fbac6..29671ff42 100644 --- a/contracts/modules/TransferManager/ManualApprovalTransferManagerStorage.sol +++ b/contracts/modules/TransferManager/ManualApprovalTransferManagerStorage.sol @@ -15,19 +15,15 @@ contract ManualApprovalTransferManagerStorage { //Manual approval is an allowance (that has been approved) with an expiry time struct ManualApproval { + address from; + address to; uint256 allowance; uint256 expiryTime; + bytes32 description; } - //Manual blocking allows you to specify a list of blocked address pairs with an associated expiry time for the block - struct ManualBlocking { - uint256 expiryTime; - } - - //Store mappings of address => address with ManualApprovals - mapping (address => mapping (address => ManualApproval)) public manualApprovals; - - //Store mappings of address => address with ManualBlockings - mapping (address => mapping (address => ManualBlocking)) public manualBlockings; + mapping (address => mapping (address => uint256)) public approvalIndex; + // An array to track all approvals + ManualApproval[] public approvals; } diff --git a/contracts/modules/TransferManager/PercentageTransferManager.sol b/contracts/modules/TransferManager/PercentageTransferManager.sol index 43c4a6f61..cdd0cc073 100644 --- a/contracts/modules/TransferManager/PercentageTransferManager.sol +++ b/contracts/modules/TransferManager/PercentageTransferManager.sol @@ -40,9 +40,9 @@ contract PercentageTransferManager is PercentageTransferManagerStorage, Transfer uint256 _amount, bytes calldata, /* _data */ bool /* _isTransfer */ - ) - external - returns(Result) + ) + external + returns(Result) { if (!paused) { if (_from == address(0) && allowPrimaryIssuance) { @@ -52,8 +52,8 @@ contract PercentageTransferManager is PercentageTransferManagerStorage, Transfer if (whitelist[_to]) { return Result.NA; } - uint256 newBalance = ISecurityToken(securityToken).balanceOf(_to).add(_amount); - if (newBalance.mul(uint256(10) ** 18).div(ISecurityToken(securityToken).totalSupply()) > maxHolderPercentage) { + uint256 newBalance = IERC20(securityToken).balanceOf(_to).add(_amount); + if (newBalance.mul(uint256(10) ** 18).div(IERC20(securityToken).totalSupply()) > maxHolderPercentage) { return Result.INVALID; } return Result.NA; diff --git a/contracts/modules/TransferManager/VolumeRestrictionTM.sol b/contracts/modules/TransferManager/VolumeRestrictionTM.sol new file mode 100644 index 000000000..2a9bd7803 --- /dev/null +++ b/contracts/modules/TransferManager/VolumeRestrictionTM.sol @@ -0,0 +1,1095 @@ +pragma solidity ^0.5.0; + +import "openzeppelin-solidity/contracts/math/SafeMath.sol"; +import "../../libraries/BokkyPooBahsDateTimeLibrary.sol"; +import "../../libraries/VolumeRestrictionLib.sol"; +import "./TransferManager.sol"; + +contract VolumeRestrictionTM is VolumeRestrictionTMStorage, TransferManager { + using SafeMath for uint256; + + // permission definition + bytes32 public constant ADMIN = "ADMIN"; + + // Emit when the token holder is added/removed from the exemption list + event ChangedExemptWalletList(address indexed _wallet, bool _change); + // Emit when the new individual restriction is added corresponds to new token holders + event AddIndividualRestriction( + address indexed _holder, + uint256 _allowedTokens, + uint256 _startTime, + uint256 _rollingPeriodInDays, + uint256 _endTime, + uint256 _typeOfRestriction + ); + // Emit when the new daily (Individual) restriction is added + event AddIndividualDailyRestriction( + address indexed _holder, + uint256 _allowedTokens, + uint256 _startTime, + uint256 _rollingPeriodInDays, + uint256 _endTime, + uint256 _typeOfRestriction + ); + // Emit when the individual restriction is modified for a given address + event ModifyIndividualRestriction( + address indexed _holder, + uint256 _allowedTokens, + uint256 _startTime, + uint256 _rollingPeriodInDays, + uint256 _endTime, + uint256 _typeOfRestriction + ); + // Emit when individual daily restriction get modified + event ModifyIndividualDailyRestriction( + address indexed _holder, + uint256 _allowedTokens, + uint256 _startTime, + uint256 _rollingPeriodInDays, + uint256 _endTime, + uint256 _typeOfRestriction + ); + // Emit when the new global restriction is added + event AddDefaultRestriction( + uint256 _allowedTokens, + uint256 _startTime, + uint256 _rollingPeriodInDays, + uint256 _endTime, + uint256 _typeOfRestriction + ); + // Emit when the new daily (Default) restriction is added + event AddDefaultDailyRestriction( + uint256 _allowedTokens, + uint256 _startTime, + uint256 _rollingPeriodInDays, + uint256 _endTime, + uint256 _typeOfRestriction + ); + // Emit when default restriction get modified + event ModifyDefaultRestriction( + uint256 _allowedTokens, + uint256 _startTime, + uint256 _rollingPeriodInDays, + uint256 _endTime, + uint256 _typeOfRestriction + ); + // Emit when daily default restriction get modified + event ModifyDefaultDailyRestriction( + uint256 _allowedTokens, + uint256 _startTime, + uint256 _rollingPeriodInDays, + uint256 _endTime, + uint256 _typeOfRestriction + ); + // Emit when the individual restriction gets removed + event IndividualRestrictionRemoved(address indexed _holder); + // Emit when individual daily restriction removed + event IndividualDailyRestrictionRemoved(address indexed _holder); + // Emit when the default restriction gets removed + event DefaultRestrictionRemoved(); + // Emit when the daily default restriction gets removed + event DefaultDailyRestrictionRemoved(); + + /** + * @notice Constructor + * @param _securityToken Address of the security token + * @param _polyAddress Address of the polytoken + */ + constructor (address _securityToken, address _polyAddress) + public + Module(_securityToken, _polyAddress) + { + + } + + /** + * @notice Used to verify the transfer/transferFrom transaction and prevent tranaction + * whose volume of tokens will voilate the maximum volume transfer restriction + * @param _from Address of the sender + * @param _amount The amount of tokens to transfer + * @param _isTransfer Whether or not this is an actual transfer or just a test to see if the tokens would be transferrable + */ + function verifyTransfer(address _from, address /*_to */, uint256 _amount, bytes memory /*_data*/, bool _isTransfer) public returns (Result) { + // If `_from` is present in the exemptionList or it is `0x0` address then it will not follow the vol restriction + if (!paused && _from != address(0) && exemptions.exemptIndex[_from] == 0) { + // Function must only be called by the associated security token if _isTransfer == true + require(msg.sender == securityToken || !_isTransfer); + // Checking the individual restriction if the `_from` comes in the individual category + if ((individualRestrictions.individualRestriction[_from].endTime >= now && individualRestrictions.individualRestriction[_from].startTime <= now) + || (individualRestrictions.individualDailyRestriction[_from].endTime >= now && individualRestrictions.individualDailyRestriction[_from].startTime <= now)) { + + return _individualRestrictionCheck(_from, _amount, _isTransfer); + // If the `_from` doesn't fall under the individual category. It will processed with in the global category automatically + } else if ((globalRestrictions.defaultRestriction.endTime >= now && globalRestrictions.defaultRestriction.startTime <= now) + || (globalRestrictions.defaultDailyRestriction.endTime >= now && globalRestrictions.defaultDailyRestriction.startTime <= now)) { + + return _defaultRestrictionCheck(_from, _amount, _isTransfer); + } + } + return Result.NA; + } + + /** + * @notice Add/Remove wallet address from the exempt list + * @param _wallet Ethereum wallet/contract address that need to be exempted + * @param _change Boolean value used to add (i.e true) or remove (i.e false) from the list + */ + function changeExemptWalletList(address _wallet, bool _change) public withPerm(ADMIN) { + require(_wallet != address(0)); + require((exemptions.exemptIndex[_wallet] == 0) == _change); + if (_change) { + exemptions.exemptAddresses.push(_wallet); + exemptions.exemptIndex[_wallet] = exemptions.exemptAddresses.length; + } else { + exemptions.exemptAddresses[exemptions.exemptIndex[_wallet] - 1] = exemptions.exemptAddresses[exemptions.exemptAddresses.length - 1]; + exemptions.exemptIndex[exemptions.exemptAddresses[exemptions.exemptIndex[_wallet] - 1]] = exemptions.exemptIndex[_wallet]; + delete exemptions.exemptIndex[_wallet]; + exemptions.exemptAddresses.length --; + } + emit ChangedExemptWalletList(_wallet, _change); + } + + /** + * @notice Use to add the new individual restriction for a given token holder + * @param _holder Address of the token holder, whom restriction will be implied + * @param _allowedTokens Amount of tokens allowed to be trade for a given address. + * @param _startTime Unix timestamp at which restriction get into effect + * @param _rollingPeriodInDays Rolling period in days (Minimum value should be 1 day) + * @param _endTime Unix timestamp at which restriction effects will gets end. + * @param _restrictionType It will be 0 or 1 (i.e 0 for fixed while 1 for Percentage) + */ + function addIndividualRestriction( + address _holder, + uint256 _allowedTokens, + uint256 _startTime, + uint256 _rollingPeriodInDays, + uint256 _endTime, + uint256 _restrictionType + ) + public + withPerm(ADMIN) + { + require( + individualRestrictions.individualRestriction[_holder].endTime < now, + "Not Allowed" + ); + if (_startTime == 0) { + _startTime = now; + } + require(_holder != address(0) && exemptions.exemptIndex[_holder] == 0, "Invalid address"); + _checkInputParams(_allowedTokens, _startTime, _rollingPeriodInDays, _endTime, _restrictionType, now); + + if (individualRestrictions.individualRestriction[_holder].endTime != 0) { + removeIndividualRestriction(_holder); + } + individualRestrictions.individualRestriction[_holder] = VolumeRestriction( + _allowedTokens, + _startTime, + _rollingPeriodInDays, + _endTime, + RestrictionType(_restrictionType) + ); + VolumeRestrictionLib.addRestrictionData(holderData, _holder, uint8(TypeOfPeriod.MultipleDays), individualRestrictions.individualRestriction[_holder].endTime); + emit AddIndividualRestriction( + _holder, + _allowedTokens, + _startTime, + _rollingPeriodInDays, + _endTime, + _restrictionType + ); + } + + /** + * @notice Use to add the new individual daily restriction for all token holder + * @param _holder Address of the token holder, whom restriction will be implied + * @param _allowedTokens Amount of tokens allowed to be traded for all token holder. + * @param _startTime Unix timestamp at which restriction get into effect + * @param _endTime Unix timestamp at which restriction effects will gets end. + * @param _restrictionType It will be 0 or 1 (i.e 0 for fixed while 1 for Percentage) + */ + function addIndividualDailyRestriction( + address _holder, + uint256 _allowedTokens, + uint256 _startTime, + uint256 _endTime, + uint256 _restrictionType + ) + public + withPerm(ADMIN) + { + if (_startTime == 0) { + _startTime = now; + } + require( + individualRestrictions.individualDailyRestriction[_holder].endTime < now, + "Not Allowed" + ); + _checkInputParams(_allowedTokens, _startTime, 1, _endTime, _restrictionType, now); + if (individualRestrictions.individualDailyRestriction[_holder].endTime != 0) { + removeIndividualDailyRestriction(_holder); + } + individualRestrictions.individualDailyRestriction[_holder] = VolumeRestriction( + _allowedTokens, + _startTime, + 1, + _endTime, + RestrictionType(_restrictionType) + ); + VolumeRestrictionLib.addRestrictionData(holderData, _holder, uint8(TypeOfPeriod.OneDay), individualRestrictions.individualRestriction[_holder].endTime); + emit AddIndividualDailyRestriction( + _holder, + _allowedTokens, + _startTime, + 1, + _endTime, + _restrictionType + ); + } + + /** + * @notice Use to add the new individual daily restriction for multiple token holders + * @param _holders Array of address of the token holders, whom restriction will be implied + * @param _allowedTokens Array of amount of tokens allowed to be trade for a given address. + * @param _startTimes Array of unix timestamps at which restrictions get into effect + * @param _endTimes Array of unix timestamps at which restriction effects will gets end. + * @param _restrictionTypes Array of restriction types value will be 0 or 1 (i.e 0 for fixed while 1 for Percentage) + */ + function addIndividualDailyRestrictionMulti( + address[] memory _holders, + uint256[] memory _allowedTokens, + uint256[] memory _startTimes, + uint256[] memory _endTimes, + uint256[] memory _restrictionTypes + ) + public + { + //NB - we duplicate _startTimes below to allow function reuse + VolumeRestrictionLib._checkLengthOfArray(_holders, _allowedTokens, _startTimes, _startTimes, _endTimes, _restrictionTypes); + for (uint256 i = 0; i < _holders.length; i++) { + addIndividualDailyRestriction( + _holders[i], + _allowedTokens[i], + _startTimes[i], + _endTimes[i], + _restrictionTypes[i] + ); + } + } + + /** + * @notice Use to add the new individual restriction for multiple token holders + * @param _holders Array of address of the token holders, whom restriction will be implied + * @param _allowedTokens Array of amount of tokens allowed to be trade for a given address. + * @param _startTimes Array of unix timestamps at which restrictions get into effect + * @param _rollingPeriodInDays Array of rolling period in days (Minimum value should be 1 day) + * @param _endTimes Array of unix timestamps at which restriction effects will gets end. + * @param _restrictionTypes Array of restriction types value will be 0 or 1 (i.e 0 for fixed while 1 for Percentage) + */ + function addIndividualRestrictionMulti( + address[] memory _holders, + uint256[] memory _allowedTokens, + uint256[] memory _startTimes, + uint256[] memory _rollingPeriodInDays, + uint256[] memory _endTimes, + uint256[] memory _restrictionTypes + ) + public + { + VolumeRestrictionLib._checkLengthOfArray(_holders, _allowedTokens, _startTimes, _rollingPeriodInDays, _endTimes, _restrictionTypes); + for (uint256 i = 0; i < _holders.length; i++) { + addIndividualRestriction( + _holders[i], + _allowedTokens[i], + _startTimes[i], + _rollingPeriodInDays[i], + _endTimes[i], + _restrictionTypes[i] + ); + } + } + + /** + * @notice Use to add the new default restriction for all token holder + * @param _allowedTokens Amount of tokens allowed to be traded for all token holder. + * @param _startTime Unix timestamp at which restriction get into effect + * @param _rollingPeriodInDays Rolling period in days (Minimum value should be 1 day) + * @param _endTime Unix timestamp at which restriction effects will gets end. + * @param _restrictionType It will be 0 or 1 (i.e 0 for fixed while 1 for Percentage) + */ + function addDefaultRestriction( + uint256 _allowedTokens, + uint256 _startTime, + uint256 _rollingPeriodInDays, + uint256 _endTime, + uint256 _restrictionType + ) + external + withPerm(ADMIN) + { + uint256 startTime = _startTime; + if (_startTime == 0) { + startTime = now; + } + require( + globalRestrictions.defaultRestriction.endTime < now, + "Not Allowed" + ); + _checkInputParams(_allowedTokens, startTime, _rollingPeriodInDays, _endTime, _restrictionType, now); + globalRestrictions.defaultRestriction = VolumeRestriction( + _allowedTokens, + startTime, + _rollingPeriodInDays, + _endTime, + RestrictionType(_restrictionType) + ); + emit AddDefaultRestriction( + _allowedTokens, + startTime, + _rollingPeriodInDays, + _endTime, + _restrictionType + ); + } + + /** + * @notice Use to add the new default daily restriction for all token holder + * @param _allowedTokens Amount of tokens allowed to be traded for all token holder. + * @param _startTime Unix timestamp at which restriction get into effect + * @param _endTime Unix timestamp at which restriction effects will gets end. + * @param _restrictionType It will be 0 or 1 (i.e 0 for fixed while 1 for Percentage) + */ + function addDefaultDailyRestriction( + uint256 _allowedTokens, + uint256 _startTime, + uint256 _endTime, + uint256 _restrictionType + ) + external + withPerm(ADMIN) + { + uint256 startTime = _startTime; + if (_startTime == 0) { + startTime = now; + } + require( + globalRestrictions.defaultDailyRestriction.endTime < now, + "Not Allowed" + ); + _checkInputParams(_allowedTokens, startTime, 1, _endTime, _restrictionType, now); + globalRestrictions.defaultDailyRestriction = VolumeRestriction( + _allowedTokens, + startTime, + 1, + _endTime, + RestrictionType(_restrictionType) + ); + emit AddDefaultDailyRestriction( + _allowedTokens, + startTime, + 1, + _endTime, + _restrictionType + ); + } + + /** + * @notice use to remove the individual restriction for a given address + * @param _holder Address of the user + */ + function removeIndividualRestriction(address _holder) public withPerm(ADMIN) { + require(_holder != address(0)); + require(individualRestrictions.individualRestriction[_holder].endTime != 0); + individualRestrictions.individualRestriction[_holder] = VolumeRestriction(0, 0, 0, 0, RestrictionType(0)); + VolumeRestrictionLib.deleteHolderFromList(holderData, _holder, uint8(TypeOfPeriod.OneDay)); + bucketData.userToBucket[_holder].lastTradedDayTime = 0; + bucketData.userToBucket[_holder].sumOfLastPeriod = 0; + bucketData.userToBucket[_holder].daysCovered = 0; + emit IndividualRestrictionRemoved(_holder); + } + + /** + * @notice use to remove the individual restriction for a given address + * @param _holders Array of address of the user + */ + function removeIndividualRestrictionMulti(address[] calldata _holders) external { + for (uint256 i = 0; i < _holders.length; i++) { + removeIndividualRestriction(_holders[i]); + } + } + + /** + * @notice use to remove the individual daily restriction for a given address + * @param _holder Address of the user + */ + function removeIndividualDailyRestriction(address _holder) public withPerm(ADMIN) { + require(_holder != address(0)); + require(individualRestrictions.individualDailyRestriction[_holder].endTime != 0); + individualRestrictions.individualDailyRestriction[_holder] = VolumeRestriction(0, 0, 0, 0, RestrictionType(0)); + VolumeRestrictionLib.deleteHolderFromList(holderData, _holder, uint8(TypeOfPeriod.MultipleDays)); + bucketData.userToBucket[_holder].dailyLastTradedDayTime = 0; + emit IndividualDailyRestrictionRemoved(_holder); + } + + /** + * @notice use to remove the individual daily restriction for a given address + * @param _holders Array of address of the user + */ + function removeIndividualDailyRestrictionMulti(address[] calldata _holders) external { + for (uint256 i = 0; i < _holders.length; i++) { + removeIndividualDailyRestriction(_holders[i]); + } + } + + /** + * @notice Use to remove the default restriction + */ + function removeDefaultRestriction() public withPerm(ADMIN) { + require(globalRestrictions.defaultRestriction.endTime != 0); + globalRestrictions.defaultRestriction = VolumeRestriction(0, 0, 0, 0, RestrictionType(0)); + emit DefaultRestrictionRemoved(); + } + + /** + * @notice Use to remove the daily default restriction + */ + function removeDefaultDailyRestriction() external withPerm(ADMIN) { + require(globalRestrictions.defaultDailyRestriction.endTime != 0); + globalRestrictions.defaultDailyRestriction = VolumeRestriction(0, 0, 0, 0, RestrictionType(0)); + emit DefaultDailyRestrictionRemoved(); + } + + /** + * @notice Use to modify the existing individual restriction for a given token holder + * @param _holder Address of the token holder, whom restriction will be implied + * @param _allowedTokens Amount of tokens allowed to be trade for a given address. + * @param _startTime Unix timestamp at which restriction get into effect + * @param _rollingPeriodInDays Rolling period in days (Minimum value should be 1 day) + * @param _endTime Unix timestamp at which restriction effects will gets end. + * @param _restrictionType It will be 0 or 1 (i.e 0 for fixed while 1 for Percentage) + */ + function modifyIndividualRestriction( + address _holder, + uint256 _allowedTokens, + uint256 _startTime, + uint256 _rollingPeriodInDays, + uint256 _endTime, + uint256 _restrictionType + ) + public + withPerm(ADMIN) + { + if (_startTime == 0) { + _startTime = now; + } + require(individualRestrictions.individualRestriction[_holder].startTime > now, "Not Allowed"); + _checkInputParams(_allowedTokens, _startTime, _rollingPeriodInDays, _endTime, _restrictionType, now); + individualRestrictions.individualRestriction[_holder] = VolumeRestriction( + _allowedTokens, + _startTime, + _rollingPeriodInDays, + _endTime, + RestrictionType(_restrictionType) + ); + emit ModifyIndividualRestriction( + _holder, + _allowedTokens, + _startTime, + _rollingPeriodInDays, + _endTime, + _restrictionType + ); + } + + /** + * @notice Use to modify the existing individual daily restriction for a given token holder + * @dev Changing of startTime will affect the 24 hrs span. i.e if in earlier restriction days start with + * morning and end on midnight while after the change day may start with afternoon and end with other day afternoon + * @param _holder Address of the token holder, whom restriction will be implied + * @param _allowedTokens Amount of tokens allowed to be trade for a given address. + * @param _startTime Unix timestamp at which restriction get into effect + * @param _endTime Unix timestamp at which restriction effects will gets end. + * @param _restrictionType It will be 0 or 1 (i.e 0 for fixed while 1 for Percentage) + */ + function modifyIndividualDailyRestriction( + address _holder, + uint256 _allowedTokens, + uint256 _startTime, + uint256 _endTime, + uint256 _restrictionType + ) + public + withPerm(ADMIN) + { + if (_startTime == 0) { + _startTime = now; + } + uint checkTime = (individualRestrictions.individualDailyRestriction[_holder].startTime <= now ? individualRestrictions.individualDailyRestriction[_holder].startTime : now); + _checkInputParams(_allowedTokens, _startTime, 1, _endTime, _restrictionType, checkTime); + individualRestrictions.individualDailyRestriction[_holder] = VolumeRestriction( + _allowedTokens, + _startTime, + 1, + _endTime, + RestrictionType(_restrictionType) + ); + emit ModifyIndividualDailyRestriction( + _holder, + _allowedTokens, + _startTime, + 1, + _endTime, + _restrictionType + ); + } + + /** + * @notice Use to modify the existing individual daily restriction for multiple token holders + * @param _holders Array of address of the token holders, whom restriction will be implied + * @param _allowedTokens Array of amount of tokens allowed to be trade for a given address. + * @param _startTimes Array of unix timestamps at which restrictions get into effect + * @param _endTimes Array of unix timestamps at which restriction effects will gets end. + * @param _restrictionTypes Array of restriction types value will be 0 or 1 (i.e 0 for fixed while 1 for Percentage) + */ + function modifyIndividualDailyRestrictionMulti( + address[] memory _holders, + uint256[] memory _allowedTokens, + uint256[] memory _startTimes, + uint256[] memory _endTimes, + uint256[] memory _restrictionTypes + ) + public + { + //NB - we duplicate _startTimes below to allow function reuse + VolumeRestrictionLib._checkLengthOfArray(_holders, _allowedTokens, _startTimes, _startTimes, _endTimes, _restrictionTypes); + for (uint256 i = 0; i < _holders.length; i++) { + modifyIndividualDailyRestriction( + _holders[i], + _allowedTokens[i], + _startTimes[i], + _endTimes[i], + _restrictionTypes[i] + ); + } + } + + /** + * @notice Use to modify the existing individual restriction for multiple token holders + * @param _holders Array of address of the token holders, whom restriction will be implied + * @param _allowedTokens Array of amount of tokens allowed to be trade for a given address. + * @param _startTimes Array of unix timestamps at which restrictions get into effect + * @param _rollingPeriodInDays Array of rolling period in days (Minimum value should be 1 day) + * @param _endTimes Array of unix timestamps at which restriction effects will gets end. + * @param _restrictionTypes Array of restriction types value will be 0 or 1 (i.e 0 for fixed while 1 for Percentage) + */ + function modifyIndividualRestrictionMulti( + address[] memory _holders, + uint256[] memory _allowedTokens, + uint256[] memory _startTimes, + uint256[] memory _rollingPeriodInDays, + uint256[] memory _endTimes, + uint256[] memory _restrictionTypes + ) + public + { + VolumeRestrictionLib._checkLengthOfArray(_holders, _allowedTokens, _startTimes, _rollingPeriodInDays, _endTimes, _restrictionTypes); + for (uint256 i = 0; i < _holders.length; i++) { + modifyIndividualRestriction( + _holders[i], + _allowedTokens[i], + _startTimes[i], + _rollingPeriodInDays[i], + _endTimes[i], + _restrictionTypes[i] + ); + } + } + + /** + * @notice Use to modify the global restriction for all token holder + * @param _allowedTokens Amount of tokens allowed to be traded for all token holder. + * @param _startTime Unix timestamp at which restriction get into effect + * @param _rollingPeriodInDays Rolling period in days (Minimum value should be 1 day) + * @param _endTime Unix timestamp at which restriction effects will gets end. + * @param _restrictionType It will be 0 or 1 (i.e 0 for fixed while 1 for Percentage) + */ + function modifyDefaultRestriction( + uint256 _allowedTokens, + uint256 _startTime, + uint256 _rollingPeriodInDays, + uint256 _endTime, + uint256 _restrictionType + ) + external + withPerm(ADMIN) + { + require(globalRestrictions.defaultRestriction.startTime > now, "Not Allowed"); + uint256 startTime = _startTime; + if (_startTime == 0) { + startTime = now; + } + _checkInputParams(_allowedTokens, startTime, _rollingPeriodInDays, _endTime, _restrictionType, now); + globalRestrictions.defaultRestriction = VolumeRestriction( + _allowedTokens, + startTime, + _rollingPeriodInDays, + _endTime, + RestrictionType(_restrictionType) + ); + emit ModifyDefaultRestriction( + _allowedTokens, + startTime, + _rollingPeriodInDays, + _endTime, + _restrictionType + ); + } + + /** + * @notice Use to modify the daily default restriction for all token holder + * @dev Changing of startTime will affect the 24 hrs span. i.e if in earlier restriction days start with + * morning and end on midnight while after the change day may start with afternoon and end with other day afternoon. + * @param _allowedTokens Amount of tokens allowed to be traded for all token holder. + * @param _startTime Unix timestamp at which restriction get into effect + * @param _endTime Unix timestamp at which restriction effects will gets end. + * @param _restrictionType It will be 0 or 1 (i.e 0 for fixed while 1 for Percentage) + */ + function modifyDefaultDailyRestriction( + uint256 _allowedTokens, + uint256 _startTime, + uint256 _endTime, + uint256 _restrictionType + ) + external + withPerm(ADMIN) + { + uint256 startTime = _startTime; + if (_startTime == 0) { + startTime = now; + } + // If old startTime is already passed then new startTime should be greater than or equal to the + // old startTime otherwise any past startTime can be allowed in compare to earlier startTime. + _checkInputParams(_allowedTokens, startTime, 1, _endTime, _restrictionType, + (globalRestrictions.defaultDailyRestriction.startTime <= now ? globalRestrictions.defaultDailyRestriction.startTime : now) + ); + globalRestrictions.defaultDailyRestriction = VolumeRestriction( + _allowedTokens, + startTime, + 1, + _endTime, + RestrictionType(_restrictionType) + ); + emit ModifyDefaultDailyRestriction( + _allowedTokens, + startTime, + 1, + _endTime, + _restrictionType + ); + } + + /** + * @notice Internal function used to validate the transaction for a given address + * If it validates then it also update the storage corressponds to the default restriction + */ + function _defaultRestrictionCheck(address _from, uint256 _amount, bool _isTransfer) internal returns (Result) { + // using the variable to avoid stack too deep error + BucketDetails storage bucketDetails = bucketData.defaultUserToBucket[_from]; + uint256 daysCovered = globalRestrictions.defaultRestriction.rollingPeriodInDays; + uint256 fromTimestamp = 0; + uint256 sumOfLastPeriod = 0; + uint256 dailyTime = 0; + bool allowedDefault = true; + bool allowedDaily; + if (globalRestrictions.defaultRestriction.endTime >= now && globalRestrictions.defaultRestriction.startTime <= now) { + if (bucketDetails.lastTradedDayTime < globalRestrictions.defaultRestriction.startTime) { + // It will execute when the txn is performed first time after the addition of individual restriction + fromTimestamp = globalRestrictions.defaultRestriction.startTime; + } else { + // Picking up the last timestamp + fromTimestamp = bucketDetails.lastTradedDayTime; + } + + // Check with the bucket and parse all the new timestamps to calculate the sumOfLastPeriod + // re-using the local variables to avoid the stack too deep error. + (sumOfLastPeriod, fromTimestamp, daysCovered) = _bucketCheck( + fromTimestamp, + BokkyPooBahsDateTimeLibrary.diffDays(fromTimestamp, now), + _from, + daysCovered, + bucketDetails + ); + // validation of the transaction amount + if (!_checkValidAmountToTransact(sumOfLastPeriod, _amount, globalRestrictions.defaultRestriction)) { + allowedDefault = false; + } + } + (allowedDaily, dailyTime) = _dailyTxCheck(_from, _amount, bucketDetails.dailyLastTradedDayTime, globalRestrictions.defaultDailyRestriction); + + if (_isTransfer) { + _updateStorage( + _from, + _amount, + fromTimestamp, + sumOfLastPeriod, + daysCovered, + dailyTime, + globalRestrictions.defaultDailyRestriction.endTime, + true + ); + } + return ((allowedDaily && allowedDefault) == true ? Result.NA : Result.INVALID); + } + + /** + * @notice Internal function used to validate the transaction for a given address + * If it validates then it also update the storage corressponds to the individual restriction + */ + function _individualRestrictionCheck(address _from, uint256 _amount, bool _isTransfer) internal returns (Result) { + // using the variable to avoid stack too deep error + BucketDetails memory bucketDetails = bucketData.userToBucket[_from]; + VolumeRestriction memory dailyRestriction = individualRestrictions.individualDailyRestriction[_from]; + VolumeRestriction memory restriction = individualRestrictions.individualRestriction[_from]; + uint256 daysCovered = restriction.rollingPeriodInDays; + uint256 fromTimestamp = 0; + uint256 sumOfLastPeriod = 0; + uint256 dailyTime = 0; + bool allowedIndividual = true; + bool allowedDaily; + if (restriction.endTime >= now && restriction.startTime <= now) { + if (bucketDetails.lastTradedDayTime < restriction.startTime) { + // It will execute when the txn is performed first time after the addition of individual restriction + fromTimestamp = restriction.startTime; + } else { + // Picking up the last timestamp + fromTimestamp = bucketDetails.lastTradedDayTime; + } + + // Check with the bucket and parse all the new timestamps to calculate the sumOfLastPeriod + // re-using the local variables to avoid the stack too deep error. + (sumOfLastPeriod, fromTimestamp, daysCovered) = _bucketCheck( + fromTimestamp, + BokkyPooBahsDateTimeLibrary.diffDays(fromTimestamp, now), + _from, + daysCovered, + bucketDetails + ); + // validation of the transaction amount + if (!_checkValidAmountToTransact(sumOfLastPeriod, _amount, restriction)) { + allowedIndividual = false; + } + } + (allowedDaily, dailyTime) = _dailyTxCheck(_from, _amount, bucketDetails.dailyLastTradedDayTime, dailyRestriction); + if (_isTransfer) { + _updateStorage( + _from, + _amount, + fromTimestamp, + sumOfLastPeriod, + daysCovered, + dailyTime, + dailyRestriction.endTime, + false + ); + } + + return ((allowedDaily && allowedIndividual) ? Result.NA : Result.INVALID); + } + + function _dailyTxCheck( + address from, + uint256 amount, + uint256 dailyLastTradedDayTime, + VolumeRestriction memory restriction + ) + internal + view + returns(bool, uint256) + { + // Checking whether the daily restriction is added or not if yes then calculate + // the total amount get traded on a particular day (~ _fromTime) + if ( now <= restriction.endTime && now >= restriction.startTime) { + uint256 txSumOfDay = 0; + if (dailyLastTradedDayTime == 0 || dailyLastTradedDayTime < restriction.startTime) + // This if condition will be executed when the individual daily restriction executed first time + dailyLastTradedDayTime = restriction.startTime.add(BokkyPooBahsDateTimeLibrary.diffDays(restriction.startTime, now).mul(1 days)); + else if (now.sub(dailyLastTradedDayTime) >= 1 days) + dailyLastTradedDayTime = dailyLastTradedDayTime.add(BokkyPooBahsDateTimeLibrary.diffDays(dailyLastTradedDayTime, now).mul(1 days)); + // Assgining total sum traded on dailyLastTradedDayTime timestamp + txSumOfDay = bucketData.bucket[from][dailyLastTradedDayTime]; + return (_checkValidAmountToTransact(txSumOfDay, amount, restriction), dailyLastTradedDayTime); + } + return (true, dailyLastTradedDayTime); + } + + /// Internal function for the bucket check + function _bucketCheck( + uint256 _fromTime, + uint256 _diffDays, + address _from, + uint256 _rollingPeriodInDays, + BucketDetails memory _bucketDetails + ) + internal + view + returns (uint256, uint256, uint256) + { + uint256 counter = _bucketDetails.daysCovered; + uint256 sumOfLastPeriod = _bucketDetails.sumOfLastPeriod; + uint256 i = 0; + if (_diffDays >= _rollingPeriodInDays) { + // If the difference of days is greater than the rollingPeriod then sumOfLastPeriod will always be zero + sumOfLastPeriod = 0; + counter = counter.add(_diffDays); + } else { + for (i = 0; i < _diffDays; i++) { + counter++; + // This condition is to check whether the first rolling period is covered or not + // if not then it continues and adding 0 value into sumOfLastPeriod without subtracting + // the earlier value at that index + if (counter >= _rollingPeriodInDays) { + // Subtracting the former value(Sum of all the txn amount of that day) from the sumOfLastPeriod + // The below line subtracts (the traded volume on days no longer covered by rolling period) from sumOfLastPeriod. + // Every loop execution subtracts one day's trade volume. + // Loop starts from the first day covered in sumOfLastPeriod upto the day that is covered by rolling period. + uint256 temp = _bucketDetails.daysCovered.sub(counter.sub(_rollingPeriodInDays)); + temp = _bucketDetails.lastTradedDayTime.sub(temp.mul(1 days)); + sumOfLastPeriod = sumOfLastPeriod.sub(bucketData.bucket[_from][temp]); + } + // Adding the last amount that is transacted on the `_fromTime` not actually doing it but left written to understand + // the alogrithm + //_bucketDetails.sumOfLastPeriod = _bucketDetails.sumOfLastPeriod.add(uint256(0)); + } + } + // calculating the timestamp that will used as an index of the next bucket + // i.e buckets period will be look like this T1 to T2-1, T2 to T3-1 .... + // where T1,T2,T3 are timestamps having 24 hrs difference + _fromTime = _fromTime.add(_diffDays.mul(1 days)); + return (sumOfLastPeriod, _fromTime, counter); + } + + function _checkValidAmountToTransact( + uint256 _sumOfLastPeriod, + uint256 _amountToTransact, + VolumeRestriction memory _restriction + ) + internal + view + returns (bool) + { + uint256 _allowedAmount = 0; + if (_restriction.typeOfRestriction == RestrictionType.Percentage) { + _allowedAmount = (_restriction.allowedTokens.mul(IERC20(securityToken).totalSupply())) / uint256(10) ** 18; + } else { + _allowedAmount = _restriction.allowedTokens; + } + // Validation on the amount to transact + return (_allowedAmount >= _sumOfLastPeriod.add(_amountToTransact)); + } + + function _updateStorage( + address _from, + uint256 _amount, + uint256 _lastTradedDayTime, + uint256 _sumOfLastPeriod, + uint256 _daysCovered, + uint256 _dailyLastTradedDayTime, + uint256 _endTime, + bool isDefault + ) + internal + { + + if (isDefault){ + BucketDetails storage defaultUserToBucketDetails = bucketData.defaultUserToBucket[_from]; + _updateStorageActual(_from, _amount, _lastTradedDayTime, _sumOfLastPeriod, _daysCovered, _dailyLastTradedDayTime, _endTime, defaultUserToBucketDetails); + } + else { + BucketDetails storage userToBucketDetails = bucketData.userToBucket[_from]; + _updateStorageActual(_from, _amount, _lastTradedDayTime, _sumOfLastPeriod, _daysCovered, _dailyLastTradedDayTime, _endTime, userToBucketDetails); + } + } + + function _updateStorageActual( + address _from, + uint256 _amount, + uint256 _lastTradedDayTime, + uint256 _sumOfLastPeriod, + uint256 _daysCovered, + uint256 _dailyLastTradedDayTime, + uint256 _endTime, + BucketDetails storage details + ) + internal + { + // Cheap storage technique + if (details.lastTradedDayTime != _lastTradedDayTime) { + // Assigning the latest transaction timestamp of the day + details.lastTradedDayTime = _lastTradedDayTime; + } + if (details.dailyLastTradedDayTime != _dailyLastTradedDayTime) { + // Assigning the latest transaction timestamp of the day + details.dailyLastTradedDayTime = _dailyLastTradedDayTime; + } + if (details.daysCovered != _daysCovered) { + details.daysCovered = _daysCovered; + } + + if (_amount != 0) { + if (_lastTradedDayTime !=0) { + details.sumOfLastPeriod = _sumOfLastPeriod.add(_amount); + // Increasing the total amount of the day by `_amount` + bucketData.bucket[_from][_lastTradedDayTime] = bucketData.bucket[_from][_lastTradedDayTime].add(_amount); + } + if ((_dailyLastTradedDayTime != _lastTradedDayTime) && _dailyLastTradedDayTime != 0 && now <= _endTime) { + // Increasing the total amount of the day by `_amount` + bucketData.bucket[_from][_dailyLastTradedDayTime] = bucketData.bucket[_from][_dailyLastTradedDayTime].add(_amount); + } + } + } + + function _checkInputParams( + uint256 _allowedTokens, + uint256 _startTime, + uint256 _rollingPeriodDays, + uint256 _endTime, + uint256 _restrictionType, + uint256 _earliestStartTime + ) + internal + pure + { + require(_restrictionType == 0 || _restrictionType == 1, "Invalid type"); + require(_startTime >= _earliestStartTime, "Invalid startTime"); + if (_restrictionType == uint256(RestrictionType.Fixed)) { + require(_allowedTokens > 0, "Invalid value"); + } else { + require( + _allowedTokens > 0 && _allowedTokens <= 100 * 10 ** 16, + "Invalid value" + ); + } + // Maximum limit for the rollingPeriod is 365 days + require(_rollingPeriodDays >= 1 && _rollingPeriodDays <= 365, "Invalid rollingperiod"); + require( + BokkyPooBahsDateTimeLibrary.diffDays(_startTime, _endTime) >= _rollingPeriodDays && _endTime > _startTime, + "Invalid times" + ); + } + + /** + * @notice Use to get the bucket details for a given address + * @param _user Address of the token holder for whom the bucket details has queried + * @return uint256 lastTradedDayTime + * @return uint256 sumOfLastPeriod + * @return uint256 days covered + * @return uint256 24h lastTradedDayTime + */ + function getIndividualBucketDetailsToUser(address _user) public view returns(uint256, uint256, uint256, uint256) { + return( + bucketData.userToBucket[_user].lastTradedDayTime, + bucketData.userToBucket[_user].sumOfLastPeriod, + bucketData.userToBucket[_user].daysCovered, + bucketData.userToBucket[_user].dailyLastTradedDayTime + ); + } + + /** + * @notice Use to get the bucket details for a given address + * @param _user Address of the token holder for whom the bucket details has queried + * @return uint256 lastTradedDayTime + * @return uint256 sumOfLastPeriod + * @return uint256 days covered + * @return uint256 24h lastTradedDayTime + */ + function getDefaultBucketDetailsToUser(address _user) public view returns(uint256, uint256, uint256, uint256) { + return( + bucketData.defaultUserToBucket[_user].lastTradedDayTime, + bucketData.defaultUserToBucket[_user].sumOfLastPeriod, + bucketData.defaultUserToBucket[_user].daysCovered, + bucketData.defaultUserToBucket[_user].dailyLastTradedDayTime + ); + } + + /** + * @notice Use to get the volume of token that being traded at a particular day (`_at` + 24 hours) for a given user + * @param _user Address of the token holder + * @param _at Timestamp + */ + function getTotalTradedByUser(address _user, uint256 _at) external view returns(uint256) { + return bucketData.bucket[_user][_at]; + } + + /** + * @notice This function returns the signature of configure function + */ + function getInitFunction() public pure returns(bytes4) { + return bytes4(0); + } + + /** + * @notice Use to return the list of exempted addresses + */ + function getExemptAddress() external view returns(address[] memory) { + return exemptions.exemptAddresses; + } + + function getIndividualRestriction(address _investor) external view returns(uint256, uint256, uint256, uint256, RestrictionType) { + return _volumeRestrictionSplay(individualRestrictions.individualRestriction[_investor]); + } + + function getIndividualDailyRestriction(address _investor) external view returns(uint256, uint256, uint256, uint256, RestrictionType) { + return _volumeRestrictionSplay(individualRestrictions.individualDailyRestriction[_investor]); + } + + function getDefaultRestriction() external view returns(uint256, uint256, uint256, uint256, RestrictionType) { + return _volumeRestrictionSplay(globalRestrictions.defaultRestriction); + } + + function getDefaultDailyRestriction() external view returns(uint256, uint256, uint256, uint256, RestrictionType) { + return _volumeRestrictionSplay(globalRestrictions.defaultDailyRestriction); + } + + function _volumeRestrictionSplay(VolumeRestriction memory _volumeRestriction) internal pure returns(uint256, uint256, uint256, uint256, RestrictionType) { + return ( + _volumeRestriction.allowedTokens, + _volumeRestriction.startTime, + _volumeRestriction.rollingPeriodInDays, + _volumeRestriction.endTime, + _volumeRestriction.typeOfRestriction + ); + } + + /** + * @notice Provide the restriction details of all the restricted addresses + * @return address List of the restricted addresses + * @return uint256 List of the tokens allowed to the restricted addresses corresponds to restricted address + * @return uint256 List of the start time of the restriction corresponds to restricted address + * @return uint256 List of the rolling period in days for a restriction corresponds to restricted address. + * @return uint256 List of the end time of the restriction corresponds to restricted address. + * @return uint8 List of the type of restriction to validate the value of the `allowedTokens` + * of the restriction corresponds to restricted address + */ + function getRestrictionData() external view returns( + address[] memory allAddresses, + uint256[] memory allowedTokens, + uint256[] memory startTime, + uint256[] memory rollingPeriodInDays, + uint256[] memory endTime, + uint8[] memory typeOfRestriction + ) { + return VolumeRestrictionLib.getRestrictionData(holderData, individualRestrictions); + } + + + + /** + * @notice Returns the permissions flag that are associated with Percentage transfer Manager + */ + function getPermissions() public view returns(bytes32[] memory ) { + bytes32[] memory allPermissions = new bytes32[](1); + allPermissions[0] = ADMIN; + return allPermissions; + } + +} diff --git a/contracts/modules/TransferManager/VolumeRestrictionTMFactory.sol b/contracts/modules/TransferManager/VolumeRestrictionTMFactory.sol new file mode 100644 index 000000000..b105dba6f --- /dev/null +++ b/contracts/modules/TransferManager/VolumeRestrictionTMFactory.sol @@ -0,0 +1,73 @@ +pragma solidity ^0.5.0; + +import "../../proxy/VolumeRestrictionTMProxy.sol"; +import "../ModuleFactory.sol"; + +/** + * @title Factory for deploying VolumeRestrictionTM module + */ +contract VolumeRestrictionTMFactory is ModuleFactory { + + address public logicContract; + + /** + * @notice Constructor + */ + constructor (uint256 _setupCost, uint256 _usageCost, uint256 _subscriptionCost, address _logicContract) public + ModuleFactory(_setupCost, _usageCost, _subscriptionCost) + { + require(_logicContract != address(0), "Invalid address"); + version = "1.0.0"; + name = "VolumeRestrictionTM"; + title = "Volume Restriction Transfer Manager"; + description = "Manage transfers based on the volume of tokens that needs to be transact"; + compatibleSTVersionRange["lowerBound"] = VersionUtils.pack(uint8(0), uint8(0), uint8(0)); + compatibleSTVersionRange["upperBound"] = VersionUtils.pack(uint8(0), uint8(0), uint8(0)); + logicContract = _logicContract; + } + + + /** + * @notice Used to launch the Module with the help of factory + * @return address Contract address of the Module + */ + function deploy(bytes calldata /* _data */) external returns(address) { + address polyToken = _takeFee(); + address volumeRestrictionTransferManager = address(new VolumeRestrictionTMProxy(msg.sender, address(polyToken), logicContract)); + /*solium-disable-next-line security/no-block-members*/ + emit GenerateModuleFromFactory(volumeRestrictionTransferManager, getName(), address(this), msg.sender, setupCost, now); + return volumeRestrictionTransferManager; + } + + + /** + * @notice Type of the Module factory + */ + function getTypes() external view returns(uint8[] memory) { + uint8[] memory res = new uint8[](1); + res[0] = 2; + return res; + } + + /** + * @notice Returns the instructions associated with the module + */ + function getInstructions() external view returns(string memory) { + /*solium-disable-next-line max-len*/ + return "Module used to restrict the volume of tokens traded by the token holders"; + } + + /** + * @notice Get the tags related to the module factory + */ + function getTags() public view returns(bytes32[] memory) { + bytes32[] memory availableTags = new bytes32[](5); + availableTags[0] = "Maximum Volume"; + availableTags[1] = "Transfer Restriction"; + availableTags[2] = "Daily Restriction"; + availableTags[3] = "Individual Restriction"; + availableTags[4] = "Default Restriction"; + return availableTags; + } + +} diff --git a/contracts/proxy/ERC20DividendCheckpointProxy.sol b/contracts/proxy/ERC20DividendCheckpointProxy.sol index efd4a1445..5dc0c8e63 100644 --- a/contracts/proxy/ERC20DividendCheckpointProxy.sol +++ b/contracts/proxy/ERC20DividendCheckpointProxy.sol @@ -17,12 +17,12 @@ contract ERC20DividendCheckpointProxy is ERC20DividendCheckpointStorage, Dividen * @param _implementation representing the address of the new implementation to be set */ constructor( - address _securityToken, - address _polyAddress, + address _securityToken, + address _polyAddress, address _implementation - ) - public - ModuleStorage(_securityToken, _polyAddress) + ) + public + ModuleStorage(_securityToken, _polyAddress) { require(_implementation != address(0), "Implementation address should not be 0x"); __implementation = _implementation; diff --git a/contracts/proxy/EtherDividendCheckpointProxy.sol b/contracts/proxy/EtherDividendCheckpointProxy.sol index e03ccbbad..381d71d27 100644 --- a/contracts/proxy/EtherDividendCheckpointProxy.sol +++ b/contracts/proxy/EtherDividendCheckpointProxy.sol @@ -16,12 +16,12 @@ contract EtherDividendCheckpointProxy is DividendCheckpointStorage, ModuleStorag * @param _implementation representing the address of the new implementation to be set */ constructor ( - address _securityToken, - address _polyAddress, + address _securityToken, + address _polyAddress, address _implementation - ) - public - ModuleStorage(_securityToken, _polyAddress) + ) + public + ModuleStorage(_securityToken, _polyAddress) { require(_implementation != address(0), "Implementation address should not be 0x"); __implementation = _implementation; diff --git a/contracts/proxy/GeneralTransferManagerProxy.sol b/contracts/proxy/GeneralTransferManagerProxy.sol index c8c8ddcc1..0a4adbc08 100644 --- a/contracts/proxy/GeneralTransferManagerProxy.sol +++ b/contracts/proxy/GeneralTransferManagerProxy.sol @@ -1,6 +1,6 @@ pragma solidity ^0.5.0; -import "../modules/TransferManager/GeneralTransferManagerStorage.sol"; +import "../storage/GeneralTransferManagerStorage.sol"; import "./OwnedProxy.sol"; import "../Pausable.sol"; import "../modules/ModuleStorage.sol"; @@ -16,12 +16,12 @@ contract GeneralTransferManagerProxy is GeneralTransferManagerStorage, ModuleSto * @param _implementation representing the address of the new implementation to be set */ constructor( - address _securityToken, - address _polyAddress, + address _securityToken, + address _polyAddress, address _implementation - ) - public - ModuleStorage(_securityToken, _polyAddress) + ) + public + ModuleStorage(_securityToken, _polyAddress) { require(_implementation != address(0), "Implementation address should not be 0x"); __implementation = _implementation; diff --git a/contracts/proxy/USDTieredSTOProxy.sol b/contracts/proxy/USDTieredSTOProxy.sol index 290cea83d..1f5a1087a 100644 --- a/contracts/proxy/USDTieredSTOProxy.sol +++ b/contracts/proxy/USDTieredSTOProxy.sol @@ -1,6 +1,6 @@ pragma solidity ^0.5.0; -import "../modules/STO/USDTieredSTOStorage.sol"; +import "../storage/USDTieredSTOStorage.sol"; import "./OwnedProxy.sol"; import "../Pausable.sol"; import "openzeppelin-solidity/contracts/utils/ReentrancyGuard.sol"; diff --git a/contracts/proxy/VestingEscrowWalletProxy.sol b/contracts/proxy/VestingEscrowWalletProxy.sol new file mode 100644 index 000000000..a69463084 --- /dev/null +++ b/contracts/proxy/VestingEscrowWalletProxy.sol @@ -0,0 +1,27 @@ +pragma solidity ^0.5.0; + +import "../storage/VestingEscrowWalletStorage.sol"; +import "./OwnedProxy.sol"; +import "../Pausable.sol"; +import "../modules/ModuleStorage.sol"; + /** + * @title Escrow wallet module for vesting functionality + */ +contract VestingEscrowWalletProxy is VestingEscrowWalletStorage, ModuleStorage, Pausable, OwnedProxy { + /** + * @notice Constructor + * @param _securityToken Address of the security token + * @param _polyAddress Address of the polytoken + * @param _implementation representing the address of the new implementation to be set + */ + constructor (address _securityToken, address _polyAddress, address _implementation) + public + ModuleStorage(_securityToken, _polyAddress) + { + require( + _implementation != address(0), + "Implementation address should not be 0x" + ); + __implementation = _implementation; + } + } \ No newline at end of file diff --git a/contracts/proxy/VolumeRestrictionTMProxy.sol b/contracts/proxy/VolumeRestrictionTMProxy.sol new file mode 100644 index 000000000..ce1356904 --- /dev/null +++ b/contracts/proxy/VolumeRestrictionTMProxy.sol @@ -0,0 +1,30 @@ +pragma solidity ^0.5.0; + +import "../storage/VolumeRestrictionTMStorage.sol"; +import "./OwnedProxy.sol"; +import "../Pausable.sol"; +import "../modules/ModuleStorage.sol"; + +/** + * @title Transfer Manager module for core transfer validation functionality + */ +contract VolumeRestrictionTMProxy is VolumeRestrictionTMStorage, ModuleStorage, Pausable, OwnedProxy { + + /** + * @notice Constructor + * @param _securityToken Address of the security token + * @param _polyAddress Address of the polytoken + * @param _implementation representing the address of the new implementation to be set + */ + constructor (address _securityToken, address _polyAddress, address _implementation) + public + ModuleStorage(_securityToken, _polyAddress) + { + require( + _implementation != address(0), + "Implementation address should not be 0x" + ); + __implementation = _implementation; + } + +} diff --git a/contracts/storage/EternalStorage.sol b/contracts/storage/EternalStorage.sol index 114f0125c..40175f31d 100644 --- a/contracts/storage/EternalStorage.sol +++ b/contracts/storage/EternalStorage.sol @@ -56,38 +56,6 @@ contract EternalStorage { stringStorage[_key] = _value; } - //////////////////// - /// get functions - //////////////////// - /// @notice Get function use to get the value of the singleton state variables - /// Ex1- string public version = "0.0.1"; - /// string _version = getString(keccak256(abi.encodePacked("version")); - /// Ex2 - assert(temp1 == temp2); replace to - /// assert(getUint(keccak256(abi.encodePacked(temp1)) == getUint(keccak256(abi.encodePacked(temp2)); - /// Ex3 - mapping(string => SymbolDetails) registeredSymbols; where SymbolDetails is the structure having different type of values as - /// {uint256 date, string name, address owner} etc. - /// string _name = getString(keccak256(abi.encodePacked("registeredSymbols_name", "TOKEN")); - - function getBool(bytes32 _key) internal view returns(bool) { - return boolStorage[_key]; - } - - function getUint(bytes32 _key) internal view returns(uint256) { - return uintStorage[_key]; - } - - function getAddress(bytes32 _key) internal view returns(address) { - return addressStorage[_key]; - } - - function getString(bytes32 _key) internal view returns(string memory) { - return stringStorage[_key]; - } - - function getBytes32(bytes32 _key) internal view returns(bytes32) { - return bytes32Storage[_key]; - } - //////////////////////////// // deleteArray functions //////////////////////////// @@ -189,19 +157,15 @@ contract EternalStorage { /// Ex2- uint256 _len = tokensOwnedByOwner[0x1].length; replace with /// getArrayBytes32(keccak256(abi.encodePacked("tokensOwnedByOwner", 0x1)).length; - function getArrayAddress(bytes32 _key) internal view returns(address[] memory) { + function getArrayAddress(bytes32 _key) public view returns(address[] memory) { return addressArrayStorage[_key]; } - function getArrayBytes32(bytes32 _key) internal view returns(bytes32[] memory) { + function getArrayBytes32(bytes32 _key) public view returns(bytes32[] memory) { return bytes32ArrayStorage[_key]; } - function getArrayString(bytes32 _key) internal view returns(string[] memory) { - return stringArrayStorage[_key]; - } - - function getArrayUint(bytes32 _key) internal view returns(uint[] memory) { + function getArrayUint(bytes32 _key) public view returns(uint[] memory) { return uintArrayStorage[_key]; } @@ -229,31 +193,37 @@ contract EternalStorage { stringArrayStorage[_key][_index] = _value; } - ///////////////////////////// /// Public getters functions - ///////////////////////////// + /////////////////////// @notice Get function use to get the value of the singleton state variables + /// Ex1- string public version = "0.0.1"; + /// string _version = getString(keccak256(abi.encodePacked("version")); + /// Ex2 - assert(temp1 == temp2); replace to + /// assert(getUint(keccak256(abi.encodePacked(temp1)) == getUint(keccak256(abi.encodePacked(temp2)); + /// Ex3 - mapping(string => SymbolDetails) registeredSymbols; where SymbolDetails is the structure having different type of values as + /// {uint256 date, string name, address owner} etc. + /// string _name = getString(keccak256(abi.encodePacked("registeredSymbols_name", "TOKEN")); - function getUintValues(bytes32 _variable) public view returns(uint256) { + function getUintValue(bytes32 _variable) public view returns(uint256) { return uintStorage[_variable]; } - function getBoolValues(bytes32 _variable) public view returns(bool) { + function getBoolValue(bytes32 _variable) public view returns(bool) { return boolStorage[_variable]; } - function getStringValues(bytes32 _variable) public view returns(string memory) { + function getStringValue(bytes32 _variable) public view returns(string memory) { return stringStorage[_variable]; } - function getAddressValues(bytes32 _variable) public view returns(address) { + function getAddressValue(bytes32 _variable) public view returns(address) { return addressStorage[_variable]; } - function getBytes32Values(bytes32 _variable) public view returns(bytes32) { + function getBytes32Value(bytes32 _variable) public view returns(bytes32) { return bytes32Storage[_variable]; } - function getBytesValues(bytes32 _variable) public view returns(bytes memory) { + function getBytesValue(bytes32 _variable) public view returns(bytes memory) { return bytesStorage[_variable]; } diff --git a/contracts/modules/TransferManager/GeneralTransferManagerStorage.sol b/contracts/storage/GeneralTransferManagerStorage.sol similarity index 96% rename from contracts/modules/TransferManager/GeneralTransferManagerStorage.sol rename to contracts/storage/GeneralTransferManagerStorage.sol index b9add9a57..83418fc2a 100644 --- a/contracts/modules/TransferManager/GeneralTransferManagerStorage.sol +++ b/contracts/storage/GeneralTransferManagerStorage.sol @@ -36,7 +36,7 @@ contract GeneralTransferManagerStorage { // An address can only send / receive tokens once their corresponding uint256 > block.number // (unless allowAllTransfers == true or allowAllWhitelistTransfers == true) - mapping(address => TimeRestriction) public whitelist; + mapping (address => TimeRestriction) public whitelist; // Map of used nonces by customer mapping(address => mapping(uint256 => bool)) public nonceMap; diff --git a/contracts/modules/STO/USDTieredSTOStorage.sol b/contracts/storage/USDTieredSTOStorage.sol similarity index 67% rename from contracts/modules/STO/USDTieredSTOStorage.sol rename to contracts/storage/USDTieredSTOStorage.sol index a1c6f88ba..267df4f58 100644 --- a/contracts/modules/STO/USDTieredSTOStorage.sol +++ b/contracts/storage/USDTieredSTOStorage.sol @@ -10,6 +10,7 @@ contract USDTieredSTOStorage { // Storage // ///////////// struct Tier { + // NB rates mentioned below are actually price and are used like price in the logic. // How many token units a buyer gets per USD in this tier (multiplied by 10**18) uint256 rate; // How many token units a buyer gets per USD in this tier (multiplied by 10**18) when investing in POLY up to tokensDiscountPoly @@ -26,43 +27,57 @@ contract USDTieredSTOStorage { uint256 mintedDiscountPoly; } - mapping(bytes32 => mapping(bytes32 => string)) oracleKeys; + struct Investor { + // Whether investor is accredited (0 = non-accredited, 1 = accredited) + uint8 accredited; + // Whether we have seen the investor before (already added to investors list) + uint8 seen; + // Overrides for default limit in USD for non-accredited investors multiplied by 10**18 (0 = no override) + uint256 nonAccreditedLimitUSDOverride; + } - IERC20 public usdToken; + mapping(bytes32 => mapping(bytes32 => string)) oracleKeys; // Determine whether users can invest on behalf of a beneficiary - bool public allowBeneficialInvestments = false; + bool public allowBeneficialInvestments; // Whether or not the STO has been finalized bool public isFinalized; - // Address where ETH, POLY & DAI funds are delivered - address payable public wallet; - // Address of issuer reserve wallet for unsold tokens address public reserveWallet; + // List of stable coin addresses + address[] internal usdTokens; + // Current tier uint256 public currentTier; // Amount of USD funds raised uint256 public fundsRaisedUSD; + // Amount of stable coins raised + mapping (address => uint256) public stableCoinsRaised; + // Amount in USD invested by each address mapping(address => uint256) public investorInvestedUSD; // Amount in fund raise type invested by each investor mapping(address => mapping(uint8 => uint256)) public investorInvested; - // List of accredited investors - mapping(address => bool) public accredited; + // Accredited & non-accredited investor data + mapping (address => Investor) public investors; + + // List of active stable coin addresses + mapping (address => bool) internal usdTokenEnabled; + + // List of all addresses that have been added as accredited or non-accredited without + // the default limit + address[] public investorsList; // Default limit in USD for non-accredited investors multiplied by 10**18 uint256 public nonAccreditedLimitUSD; - // Overrides for default limit in USD for non-accredited investors multiplied by 10**18 - mapping(address => uint256) public nonAccreditedLimitUSDOverride; - // Minimum investable amount in USD uint256 public minimumInvestmentUSD; diff --git a/contracts/storage/VestingEscrowWalletStorage.sol b/contracts/storage/VestingEscrowWalletStorage.sol new file mode 100644 index 000000000..f3752ad7f --- /dev/null +++ b/contracts/storage/VestingEscrowWalletStorage.sol @@ -0,0 +1,54 @@ +pragma solidity ^0.5.0; + +/** + * @title Wallet for core vesting escrow functionality + */ +contract VestingEscrowWalletStorage { + + struct Schedule { + // Name of the template + bytes32 templateName; + // Tokens that were already claimed + uint256 claimedTokens; + // Start time of the schedule + uint256 startTime; + } + + struct Template { + // Total amount of tokens + uint256 numberOfTokens; + // Schedule duration (How long the schedule will last) + uint256 duration; + // Schedule frequency (It is a cliff time period) + uint256 frequency; + // Index of the template in an array template names + uint256 index; + } + + // Number of tokens that are hold by the `this` contract but are unassigned to any schedule + uint256 public unassignedTokens; + // Address of the Treasury wallet. All of the unassigned token will transfer to that address. + address public treasuryWallet; + // List of all beneficiaries who have the schedules running/completed/created + address[] public beneficiaries; + // Flag whether beneficiary has been already added or not + mapping(address => bool) internal beneficiaryAdded; + + // Holds schedules array corresponds to the affiliate/employee address + mapping(address => Schedule[]) public schedules; + // Holds template names array corresponds to the affiliate/employee address + mapping(address => bytes32[]) internal userToTemplates; + // Mapping use to store the indexes for different template names for a user. + // affiliate/employee address => template name => index + mapping(address => mapping(bytes32 => uint256)) internal userToTemplateIndex; + // Holds affiliate/employee addresses coressponds to the template name + mapping(bytes32 => address[]) internal templateToUsers; + // Mapping use to store the indexes for different users for a template. + // template name => affiliate/employee address => index + mapping(bytes32 => mapping(address => uint256)) internal templateToUserIndex; + // Store the template details corresponds to the template name + mapping(bytes32 => Template) templates; + + // List of all template names + bytes32[] public templateNames; +} \ No newline at end of file diff --git a/contracts/storage/VolumeRestrictionTMStorage.sol b/contracts/storage/VolumeRestrictionTMStorage.sol new file mode 100644 index 000000000..0c88cbd65 --- /dev/null +++ b/contracts/storage/VolumeRestrictionTMStorage.sol @@ -0,0 +1,86 @@ +pragma solidity ^0.5.0; + +/** + * @title Storage layout for VolumeRestrictionTM + */ +contract VolumeRestrictionTMStorage { + + enum RestrictionType { Fixed, Percentage } + + enum TypeOfPeriod { MultipleDays, OneDay, Both } + + struct RestrictedHolder { + // 1 represent true & 0 for false + uint8 seen; + // Type of period will be enum index of TypeOfPeriod enum + uint8 typeOfPeriod; + // Index of the array where the holder address lives + uint128 index; + } + + struct RestrictedData { + mapping(address => RestrictedHolder) restrictedHolders; + address[] restrictedAddresses; + } + + // Restricted data (refernce from the VolumeRestrictionLib library ) + RestrictedData holderData; + + struct VolumeRestriction { + // If typeOfRestriction is `Percentage` then allowedTokens will be in + // the % (w.r.t to totalSupply) with a multiplier of 10**16 . else it + // will be fixed amount of tokens + uint256 allowedTokens; + uint256 startTime; + uint256 rollingPeriodInDays; + uint256 endTime; + RestrictionType typeOfRestriction; + } + + struct IndividualRestrictions { + // Restriction stored corresponds to a particular token holder + mapping(address => VolumeRestriction) individualRestriction; + // Daily restriction stored corresponds to a particular token holder + mapping(address => VolumeRestriction) individualDailyRestriction; + } + + // Individual and daily restrictions for investors + IndividualRestrictions individualRestrictions; + + struct GlobalRestrictions { + // Global restriction that applies to all token holders + VolumeRestriction defaultRestriction; + // Daily global restriction that applies to all token holders (Total ST traded daily is restricted) + VolumeRestriction defaultDailyRestriction; + } + + // Individual and daily restrictions for investors + GlobalRestrictions globalRestrictions; + + struct BucketDetails { + uint256 lastTradedDayTime; + uint256 sumOfLastPeriod; // It is the sum of transacted amount within the last rollingPeriodDays + uint256 daysCovered; // No of days covered till (from the startTime of VolumeRestriction) + uint256 dailyLastTradedDayTime; + } + + struct BucketData { + // Storing _from => day's timestamp => total amount transact in a day --individual + mapping(address => mapping(uint256 => uint256)) bucket; + // Storing the information that used to validate the transaction + mapping(address => BucketDetails) userToBucket; + // Storing the information related to default restriction + mapping(address => BucketDetails) defaultUserToBucket; + } + + BucketData bucketData; + + // Hold exempt index + struct Exemptions { + mapping(address => uint256) exemptIndex; + address[] exemptAddresses; + } + + Exemptions exemptions; + +} diff --git a/contracts/tokens/SecurityToken.sol b/contracts/tokens/SecurityToken.sol index 505a403ee..ae0c26d21 100644 --- a/contracts/tokens/SecurityToken.sol +++ b/contracts/tokens/SecurityToken.sol @@ -459,7 +459,8 @@ contract SecurityToken is ERC20, ERC20Detailed, ReentrancyGuard, RegistryUpdater } /** - * @notice Internal - adjusts token holder balance at checkpoint after a token transfer + /** + * @notice Internal - adjusts token holder balance at checkpoint before a token transfer * @param _investor address of the token holder affected */ function _adjustBalanceCheckpoints(address _investor) internal { diff --git a/docs/permissions_list.md b/docs/permissions_list.md index b49c95a0e..9e7a213db 100644 --- a/docs/permissions_list.md +++ b/docs/permissions_list.md @@ -143,7 +143,7 @@ allocateTokensMulti() - TransferManager + TransferManager CountTransferManager changeHolderCount() withPerm(ADMIN) @@ -176,19 +176,13 @@ modifyWhitelistMulti() - ManualApprovalTransferManager + ManualApprovalTransferManager addManualApproval() - withPerm(TRANSFER_APPROVAL) - - - addManualBlocking() + withPerm(TRANSFER_APPROVAL) revokeManualApproval() - - revokeManualBlocking() - PercentageTransferManager modifyWhitelist() @@ -205,72 +199,142 @@ changeHolderPercentage() - LockupVolumeRestrictionTM - addLockup() - withPerm(ADMIN) + VolumeRestrictionTM + changeExemptWalletList() + withPerm(ADMIN) - addLockUpMulti() + addIndividualRestriction() - removeLockUp() + addIndividualRestrictionMulti() - modifyLockUp() + addGlobalRestriction() - SingleTradeVolumeRestrictionTM - setAllowPrimaryIssuance() - withPerm(ADMIN) + addDailyGlobalRestriction() + + + removeIndividualRestriction() + + + removeIndividualRestrictionMulti() + + + removeGlobalRestriction() + + + removeDailyGlobalRestriction() + + + modifyIndividualRestriction() + + + modifyIndividualRestrictionMulti() + + + modifyGlobalRestriction() + + + modifyDailyGlobalRestriction() + + + BlacklistTransferManager + addBlacklistType() + withPerm(ADMIN) + + + addBlacklistTypeMulti() - changeTransferLimitToPercentage() + modifyBlacklistType() - changeTransferLimitToTokens() + modifyBlacklistTypeMulti() - changeGlobalLimitInTokens() + deleteBlacklistType() - changeGlobalLimitInPercentage() + deleteBlacklistTypeMulti() - addExemptWallet() + addInvestorToBlacklist() - removeExemptWallet() + addInvestorToBlacklistMulti() - addExemptWalletMulti() + addMultiInvestorToBlacklistMulti() - removeExemptWalletMulti() + addInvestorToNewBlacklist() - setTransferLimitInTokens() + deleteInvestorFromAllBlacklist() - setTransferLimitInPercentage() + deleteInvestorFromAllBlacklistMulti() - removeTransferLimitInPercentage() + deleteInvestorFromBlacklist() - removeTransferLimitInTokens() + deleteMultiInvestorsFromBlacklistMulti() - setTransferLimitInTokensMulti() + Wallet + VestingEscrowWallet + changeTreasuryWallet() + onlyOwner - setTransferLimitInPercentageMulti() + depositTokens() + withPerm(ADMIN) - removeTransferLimitInTokensMulti() + sendToTreasury() - removeTransferLimitInPercentageMulti + pushAvailableTokens() + + addTemplate() + + + removeTemplate() + + + addSchedule() + + + addScheduleFromTemplate() + + + modifySchedule() + + + revokeSchedule() + + + revokeAllSchedules() + + + pushAvailableTokensMulti() + + + addScheduleMulti() + + + addScheduleFromTemplateMulti() + + + revokeSchedulesMulti() + + + modifyScheduleMulti() + diff --git a/migrations/2_deploy_contracts.js b/migrations/2_deploy_contracts.js index 9618425c3..47e2ef484 100644 --- a/migrations/2_deploy_contracts.js +++ b/migrations/2_deploy_contracts.js @@ -28,7 +28,9 @@ const MockOracle = artifacts.require("./MockOracle.sol"); const TokenLib = artifacts.require("./TokenLib.sol"); const SecurityToken = artifacts.require("./tokens/SecurityToken.sol"); const STRGetter = artifacts.require('./STRGetter.sol'); - +const VolumeRestrictionTMFactory = artifacts.require('./VolumeRestrictionTMFactory.sol') +const VolumeRestrictionTMLogic = artifacts.require('./VolumeRestrictionTM.sol'); +const VolumeRestrictionLib = artifacts.require('./VolumeRestrictionLib.sol'); const Web3 = require("web3"); let BN = Web3.utils.BN; @@ -176,8 +178,12 @@ module.exports = function(deployer, network, accounts) { // Deploy libraries return deployer.deploy(TokenLib, { from: PolymathAccount }); }) + .then(() => { + return deployer.deploy(VolumeRestrictionLib, { from: PolymathAccount }); + }) .then(() => { // Link libraries + deployer.link(VolumeRestrictionLib, VolumeRestrictionTMLogic); deployer.link(TokenLib, SecurityToken); deployer.link(TokenLib, STFactory); // A) Deploy the ModuleRegistry Contract (It contains the list of verified ModuleFactory) @@ -241,6 +247,11 @@ module.exports = function(deployer, network, accounts) { // manager attach with the securityToken contract at the time of deployment) return deployer.deploy(USDTieredSTOLogic, nullAddress, nullAddress, { from: PolymathAccount }); }) + .then(() => { + // B) Deploy the VolumeRestrictionTMLogic Contract (Factory used to generate the VolumeRestrictionTM contract and this + // manager attach with the securityToken contract at the time of deployment) + return deployer.deploy(VolumeRestrictionTMLogic, "0x0000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000", { from: PolymathAccount }); + }) .then(() => { // B) Deploy the CappedSTOLogic Contract (Factory used to generate the CappedSTO contract and this // manager attach with the securityToken contract at the time of deployment) @@ -288,6 +299,11 @@ module.exports = function(deployer, network, accounts) { from: PolymathAccount }); }) + .then(() => { + // D) Deploy the VolumeRestrictionTMFactory Contract (Factory used to generate the VolumeRestrictionTM contract use + // to provide the functionality of restricting the token volume) + return deployer.deploy(VolumeRestrictionTMFactory, new BN(0), new BN(0), new BN(0), VolumeRestrictionTMLogic.address, { from: PolymathAccount }); + }) .then(() => { // D) Deploy the ManualApprovalTransferManagerFactory Contract (Factory used to generate the ManualApprovalTransferManager contract use // to manual approve the transfer that will overcome the other transfer restrictions) @@ -366,6 +382,11 @@ module.exports = function(deployer, network, accounts) { // So any securityToken can use that factory to generate the GeneralPermissionManager contract. return moduleRegistry.registerModule(EtherDividendCheckpointFactory.address, { from: PolymathAccount }); }) + .then(() => { + // D) Register the VolumeRestrictionTMFactory in the ModuleRegistry to make the factory available at the protocol level. + // So any securityToken can use that factory to generate the VolumeRestrictionTM contract. + return moduleRegistry.registerModule(VolumeRestrictionTMFactory.address, { from: PolymathAccount }); + }) .then(() => { // D) Register the ManualApprovalTransferManagerFactory in the ModuleRegistry to make the factory available at the protocol level. // So any securityToken can use that factory to generate the ManualApprovalTransferManager contract. @@ -412,6 +433,12 @@ module.exports = function(deployer, network, accounts) { // Here it gets verified because it is deployed by the third party account (Polymath Account) not with the issuer accounts. return moduleRegistry.verifyModule(ERC20DividendCheckpointFactory.address, true, { from: PolymathAccount }); }) + .then(() => { + // G) Once the VolumeRestrictionTMFactory registered with the ModuleRegistry contract then for making them accessble to the securityToken + // contract, Factory should comes under the verified list of factories or those factories deployed by the securityToken issuers only. + // Here it gets verified because it is deployed by the third party account (Polymath Account) not with the issuer accounts. + return moduleRegistry.verifyModule(VolumeRestrictionTMFactory.address, true, { from: PolymathAccount }); + }) .then(() => { // G) Once the ManualApprovalTransferManagerFactory registered with the ModuleRegistry contract then for making them accessble to the securityToken // contract, Factory should comes under the verified list of factories or those factories deployed by the securityToken issuers only. @@ -460,6 +487,7 @@ module.exports = function(deployer, network, accounts) { .then(() => { console.log("\n"); console.log(` + ----------------------- Polymath Network Smart Contracts: ----------------------- PolymathRegistry: ${PolymathRegistry.address} SecurityTokenRegistry (Proxy): ${SecurityTokenRegistryProxy.address} @@ -491,6 +519,8 @@ module.exports = function(deployer, network, accounts) { ERC20DividendCheckpointLogic: ${ERC20DividendCheckpointLogic.address} EtherDividendCheckpointFactory: ${EtherDividendCheckpointFactory.address} ERC20DividendCheckpointFactory: ${ERC20DividendCheckpointFactory.address} + VolumeRestrictionTMFactory: ${VolumeRestrictionTMFactory.address} + VolumeRestrictionTMLogic: ${VolumeRestrictionTMLogic.address} --------------------------------------------------------------------------------- `); console.log("\n"); diff --git a/module-labeling b/module-labeling new file mode 100644 index 000000000..0026fe372 --- /dev/null +++ b/module-labeling @@ -0,0 +1 @@ +Branch 'module-labeling' set up to track remote branch 'module-labeling' from 'origin'. diff --git a/scripts/test.sh b/scripts/test.sh index 8396cb5a3..039cb43bb 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -96,7 +96,7 @@ if [ "$COVERAGE" = true ]; then cat coverage/lcov.info | node_modules/.bin/coveralls || echo 'Failed to report coverage to Coveralls' fi else - if [ "$CIRCLECI" = true ]; then # using mocha junit reporter for parallelism in CircleCI + if [ "$CIRCLECI" = true ]; then # using mocha junit reporter for parallelism in CircleCI mkdir test-results mkdir test-results/mocha rm truffle-config.js diff --git a/scripts/tokenInfo-v1.js b/scripts/tokenInfo-v1.js index 490fccfee..ea14a7efe 100644 --- a/scripts/tokenInfo-v1.js +++ b/scripts/tokenInfo-v1.js @@ -1,31 +1,18 @@ const Web3 = require("web3"); const web3 = new Web3(new Web3.providers.HttpProvider("https://mainnet.infura.io/")); -var request = require("request-promise"); +var request = require('request-promise') -const securityTokenABI = JSON.parse( - require("fs") - .readFileSync("../CLI/data/SecurityToken1-4-0.json") - .toString() -).abi; -const generalTransferManagerABI = JSON.parse( - require("fs") - .readFileSync("../CLI/data/GeneralTransferManager1-4-0.json") - .toString() -).abi; +const securityTokenABI = JSON.parse(require('fs').readFileSync('../CLI/data/SecurityToken1-4-0.json').toString()).abi; +const generalTransferManagerABI = JSON.parse(require('fs').readFileSync('../CLI/data/GeneralTransferManager1-4-0.json').toString()).abi; async function getTokens() { const securityTokenRegistryAddress = "0xEf58491224958d978fACF55D2120c55A24516B98"; const securityTokenRegistryABI = await getABIfromEtherscan(securityTokenRegistryAddress); const securityTokenRegistry = new web3.eth.Contract(securityTokenRegistryABI, securityTokenRegistryAddress); - let logs = await getLogsFromEtherscan( - securityTokenRegistry.options.address, - web3.utils.hexToNumber("0x5C5C18"), - "latest", - "LogNewSecurityToken(string,address,address)" - ); + let logs = await getLogsFromEtherscan(securityTokenRegistry.options.address, web3.utils.hexToNumber('0x5C5C18'), 'latest', 'LogNewSecurityToken(string,address,address)'); for (let i = 0; i < logs.length; i++) { - let tokenAddress = "0x" + logs[i].topics[1].slice(26, 66); + let tokenAddress = '0x' + logs[i].topics[1].slice(26, 66) await getInfo(tokenAddress); } } @@ -34,22 +21,17 @@ async function getInfo(tokenAddress) { let token = new web3.eth.Contract(securityTokenABI, tokenAddress); console.log("Token - " + tokenAddress); console.log("----------------------"); - console.log("Owner: " + (await token.methods.owner().call())); - console.log("Name: " + (await token.methods.name().call())); - console.log("Symbol: " + (await token.methods.symbol().call())); - console.log("Total Supply: " + (await token.methods.totalSupply().call())); - console.log("Frozen: " + (await token.methods.freeze().call())); - console.log("Investors: " + (await token.methods.investorCount().call())); - console.log("Latest Checkpoint: " + (await token.methods.currentCheckpointId().call())); - console.log("Finished Issuer Minting: " + (await token.methods.finishedIssuerMinting().call())); - console.log("Finished STO Minting: " + (await token.methods.finishedSTOMinting().call())); + console.log("Owner: " + await token.methods.owner().call()); + console.log("Name: " + await token.methods.name().call()); + console.log("Symbol: " + await token.methods.symbol().call()); + console.log("Total Supply: " + await token.methods.totalSupply().call()); + console.log("Frozen: " + await token.methods.freeze().call()); + console.log("Investors: " + await token.methods.investorCount().call()); + console.log("Latest Checkpoint: " + await token.methods.currentCheckpointId().call()); + console.log("Finished Issuer Minting: " + await token.methods.finishedIssuerMinting().call()); + console.log("Finished STO Minting: " + await token.methods.finishedSTOMinting().call()); let gtmRes = await token.methods.modules(2, 0).call(); - let gtmEvents = await getLogsFromEtherscan( - gtmRes.moduleAddress, - web3.utils.hexToNumber("0x5C5C18"), - "latest", - "LogModifyWhitelist(address,uint256,address,uint256,uint256,uint256,bool)" - ); + let gtmEvents = await getLogsFromEtherscan(gtmRes.moduleAddress, web3.utils.hexToNumber('0x5C5C18'), 'latest', 'LogModifyWhitelist(address,uint256,address,uint256,uint256,uint256,bool)'); console.log("Count of GeneralTransferManager Events: " + gtmEvents.length); console.log("Modules Attached (TransferManager):"); await getModules(2, token); @@ -59,7 +41,7 @@ async function getInfo(tokenAddress) { await getModules(3, token); console.log("Modules Attached (Checkpoint):"); await getModules(4, token); - console.log(""); + console.log("") console.log(); console.log(); } @@ -83,19 +65,19 @@ async function getModules(type, token) { } async function getLogsFromEtherscan(_address, _fromBlock, _toBlock, _eventSignature) { - let urlDomain = "api"; + let urlDomain = 'api'; const options = { url: `https://${urlDomain}.etherscan.io/api`, qs: { - module: "logs", - action: "getLogs", + module: 'logs', + action: 'getLogs', fromBlock: _fromBlock, toBlock: _toBlock, address: _address, topic0: web3.utils.sha3(_eventSignature), - apikey: "THM9IUVC2DJJ6J5MTICDE6H1HGQK14X559" + apikey: 'THM9IUVC2DJJ6J5MTICDE6H1HGQK14X559' }, - method: "GET", + method: 'GET', json: true }; let data = await request(options); @@ -103,20 +85,20 @@ async function getLogsFromEtherscan(_address, _fromBlock, _toBlock, _eventSignat } async function getABIfromEtherscan(_address) { - let urlDomain = "api"; + let urlDomain = 'api'; const options = { url: `https://${urlDomain}.etherscan.io/api`, qs: { - module: "contract", - action: "getabi", + module: 'contract', + action: 'getabi', address: _address, - apikey: "THM9IUVC2DJJ6J5MTICDE6H1HGQK14X559" + apikey: 'THM9IUVC2DJJ6J5MTICDE6H1HGQK14X559' }, - method: "GET", + method: 'GET', json: true }; let data = await request(options); return JSON.parse(data.result); } -getTokens(); +getTokens(); \ No newline at end of file diff --git a/test/b_capped_sto.js b/test/b_capped_sto.js index 94d0d4368..d98271c7e 100644 --- a/test/b_capped_sto.js +++ b/test/b_capped_sto.js @@ -128,13 +128,12 @@ contract("CappedSTO", async (accounts) => { I_STFactory, I_SecurityTokenRegistry, I_SecurityTokenRegistryProxy, - I_STRProxied, + I_STRProxied, I_STRGetter ] = instances; // STEP 5: Deploy the GeneralDelegateManagerFactory [I_GeneralPermissionManagerFactory] = await deployGPMAndVerifyed(account_polymath, I_MRProxied, new BN(0)); - // STEP 6: Deploy the CappedSTOFactory [I_CappedSTOFactory] = await deployCappedSTOAndVerifyed(account_polymath, I_MRProxied, cappedSTOSetupCost); @@ -168,7 +167,7 @@ contract("CappedSTO", async (accounts) => { it("Should generate the new security token with the same symbol as registered above", async () => { await I_PolyToken.approve(I_STRProxied.address, initRegFee, { from: token_owner }); - + let tx = await I_STRProxied.generateSecurityToken(name, symbol, tokenDetails, false, { from: token_owner }); // Verify the successful generation of the security token @@ -330,7 +329,7 @@ contract("CappedSTO", async (accounts) => { it("Should Buy the tokens", async () => { balanceOfReceiver = new BN(await web3.eth.getBalance(account_fundsReceiver)); - + await I_CappedSTO_Array_ETH[0].buyTokens(account_investor1, { from: account_investor1, value: web3.utils.toWei("1", "ether") @@ -616,14 +615,14 @@ contract("CappedSTO", async (accounts) => { it("POLY: Should generate the new security token with the same symbol as registered above", async () => { await I_PolyToken.approve(I_STRProxied.address, initRegFee, { from: token_owner }); - + let tx = await I_STRProxied.generateSecurityToken(P_name, P_symbol, P_tokenDetails, false, { from: token_owner }); // Verify the successful generation of the security token assert.equal(tx.logs[2].args._ticker, P_symbol, "SecurityToken doesn't get deployed"); I_SecurityToken_POLY = await SecurityToken.at(tx.logs[2].args._securityTokenAddress); - + const log = (await I_SecurityToken_POLY.getPastEvents('ModuleAdded', {filter: {from: blockNo}}))[0]; // Verify that GeneralTransferManager module get added successfully or not @@ -837,7 +836,7 @@ contract("CappedSTO", async (accounts) => { ); let tags = await I_CappedSTOFactory.getTags.call(); assert.equal(web3.utils.hexToString(tags[0]), "Capped"); - assert.equal(await I_CappedSTOFactory.version.call(), "1.0.0"); + assert.equal(await I_CappedSTOFactory.version.call(), "2.1.0"); }); it("Should fail to change the title -- bad owner", async () => { diff --git a/test/d_count_transfer_manager.js b/test/d_count_transfer_manager.js index 18594249d..d6de7c581 100644 --- a/test/d_count_transfer_manager.js +++ b/test/d_count_transfer_manager.js @@ -136,7 +136,7 @@ contract("CountTransferManager", async (accounts) => { it("Should generate the new security token with the same symbol as registered above", async () => { await I_PolyToken.approve(I_STRProxied.address, initRegFee, { from: token_owner }); - + let tx = await I_STRProxied.generateSecurityToken(name, symbol, tokenDetails, false, { from: token_owner }); // Verify the successful generation of the security token assert.equal(tx.logs[2].args._ticker, symbol.toUpperCase(), "SecurityToken doesn't get deployed"); @@ -360,7 +360,7 @@ contract("CountTransferManager", async (accounts) => { let tx = await I_STRProxied.registerTicker(token_owner, symbol2, contact, { from: token_owner }); await I_PolyToken.approve(I_STRProxied.address, initRegFee, { from: token_owner }); - + let tx2 = await I_STRProxied.generateSecurityToken(name, symbol2, tokenDetails, false, { from: token_owner }); I_SecurityToken2 = await SecurityToken.at(tx2.logs[2].args._securityTokenAddress); diff --git a/test/e_erc20_dividends.js b/test/e_erc20_dividends.js index 49472fc7a..f17bd200a 100644 --- a/test/e_erc20_dividends.js +++ b/test/e_erc20_dividends.js @@ -3,6 +3,7 @@ import { duration, promisifyLogWatch, latestBlock } from "./helpers/utils"; import takeSnapshot, { increaseTime, revertToSnapshot } from "./helpers/time"; import { catchRevert } from "./helpers/exceptions"; import { setUpPolymathNetwork, deployERC20DividendAndVerifyed, deployGPMAndVerifyed } from "./helpers/createInstances"; +import { encodeModuleCall } from "./helpers/encodeCall"; const SecurityToken = artifacts.require("./SecurityToken.sol"); const GeneralTransferManager = artifacts.require("./GeneralTransferManager"); @@ -18,6 +19,7 @@ contract("ERC20DividendCheckpoint", async (accounts) => { let account_polymath; let account_issuer; let token_owner; + let wallet; let account_investor1; let account_investor2; let account_investor3; @@ -75,6 +77,8 @@ contract("ERC20DividendCheckpoint", async (accounts) => { let currentTime; + const DividendParameters = ["address"]; + before(async () => { currentTime = new BN(await latestTime()); account_polymath = accounts[0]; @@ -88,6 +92,7 @@ contract("ERC20DividendCheckpoint", async (accounts) => { account_investor4 = accounts[9]; account_temp = accounts[2]; account_manager = accounts[5]; + wallet = accounts[3]; // Step 1: Deploy the genral PM ecosystem let instances = await setUpPolymathNetwork(account_polymath, token_owner); @@ -141,7 +146,7 @@ contract("ERC20DividendCheckpoint", async (accounts) => { it("Should generate the new security token with the same symbol as registered above", async () => { await I_PolyToken.approve(I_STRProxied.address, initRegFee, { from: token_owner }); - + let tx = await I_STRProxied.generateSecurityToken(name, symbol, tokenDetails, false, { from: token_owner }); // Verify the successful generation of the security token @@ -161,8 +166,9 @@ contract("ERC20DividendCheckpoint", async (accounts) => { }); it("Should successfully attach the ERC20DividendCheckpoint with the security token - fail insufficient payment", async () => { + let bytesDividend = encodeModuleCall(DividendParameters, [wallet]); await catchRevert( - I_SecurityToken.addModule(P_ERC20DividendCheckpointFactory.address, "0x0", new BN(web3.utils.toWei("500", "ether")), new BN(0), { + I_SecurityToken.addModule(P_ERC20DividendCheckpointFactory.address, bytesDividend, new BN(web3.utils.toWei("500", "ether")), new BN(0), { from: token_owner }) ); @@ -172,7 +178,8 @@ contract("ERC20DividendCheckpoint", async (accounts) => { let snapId = await takeSnapshot(); await I_PolyToken.getTokens(new BN(web3.utils.toWei("500", "ether")), token_owner); await I_PolyToken.transfer(I_SecurityToken.address, new BN(web3.utils.toWei("500", "ether")), { from: token_owner }); - const tx = await I_SecurityToken.addModule(P_ERC20DividendCheckpointFactory.address, "0x0", new BN(web3.utils.toWei("500", "ether")), new BN(0), { + let bytesDividend = encodeModuleCall(DividendParameters, [wallet]); + const tx = await I_SecurityToken.addModule(P_ERC20DividendCheckpointFactory.address, bytesDividend, new BN(web3.utils.toWei("500", "ether")), new BN(0), { from: token_owner }); assert.equal(tx.logs[3].args._types[0].toNumber(), checkpointKey, "ERC20DividendCheckpoint doesn't get deployed"); @@ -186,7 +193,8 @@ contract("ERC20DividendCheckpoint", async (accounts) => { }); it("Should successfully attach the ERC20DividendCheckpoint with the security token", async () => { - const tx = await I_SecurityToken.addModule(I_ERC20DividendCheckpointFactory.address, "0x0", new BN(0), new BN(0), { from: token_owner }); + let bytesDividend = encodeModuleCall(DividendParameters, [wallet]); + const tx = await I_SecurityToken.addModule(I_ERC20DividendCheckpointFactory.address, bytesDividend, new BN(0), new BN(0), { from: token_owner }); assert.equal(tx.logs[2].args._types[0].toNumber(), checkpointKey, "ERC20DividendCheckpoint doesn't get deployed"); assert.equal( web3.utils.toAscii(tx.logs[2].args._name).replace(/\u0000/g, ""), @@ -334,6 +342,12 @@ contract("ERC20DividendCheckpoint", async (accounts) => { ); assert.equal(tx.logs[0].args._checkpointId.toNumber(), 1, "Dividend should be created at checkpoint 1"); assert.equal(tx.logs[0].args._name.toString(), dividendName, "Dividend name incorrect in event"); + let data = await I_ERC20DividendCheckpoint.getDividendsData(); + assert.equal(data[1][0].toNumber(), maturity, "maturity match"); + assert.equal(data[2][0].toNumber(), expiry, "expiry match"); + assert.equal(data[3][0].toString(), new BN(web3.utils.toWei("1.5", "ether")).toString(), "amount match"); + assert.equal(data[4][0].toNumber(), 0, "claimed match"); + assert.equal(data[5][0], dividendName, "dividendName match"); }); it("Investor 1 transfers his token balance to investor 2", async () => { @@ -416,6 +430,7 @@ contract("ERC20DividendCheckpoint", async (accounts) => { dividendName, { from: token_owner } ); + console.log("Gas used w/ no exclusions: " + tx.receipt.gasUsed); assert.equal(tx.logs[0].args._checkpointId.toNumber(), 2, "Dividend should be created at checkpoint 1"); }); @@ -462,7 +477,9 @@ contract("ERC20DividendCheckpoint", async (accounts) => { limit = limit.toNumber() + 42; let addresses = []; addresses.push(account_temp); - while (--limit > 42) addresses.push(web3.utils.toChecksumAddress('0x00000000000000000000000000000000000000' + limit)); + let tempAdd = '0x0000000000000000000000000000000000000000'; + while (--limit > 42) addresses.push(web3.utils.toChecksumAddress(tempAdd.substring(0, 42 - limit.toString().length) + limit)); + //'0x00000000000000000000000000000000000000' + limit)); await I_ERC20DividendCheckpoint.setDefaultExcluded(addresses, { from: token_owner }); let excluded = await I_ERC20DividendCheckpoint.getDefaultExcluded(); assert.equal(excluded[0], account_temp); @@ -493,7 +510,11 @@ contract("ERC20DividendCheckpoint", async (accounts) => { limit = limit.toNumber() + 42; let addresses = []; addresses.push(account_temp); - while (limit-- > 42) addresses.push(web3.utils.toChecksumAddress('0x00000000000000000000000000000000000000' + limit)); + let tempAdd = '0x0000000000000000000000000000000000000000'; + while (limit-- > 42) addresses.push(web3.utils.toChecksumAddress(tempAdd.substring(0, 42 - limit.toString().length) + limit)); + + // while (limit-- > 42) addresses.push(web3.utils.toChecksumAddress('0x00000000000000000000000000000000000000' + limit)); + console.log(addresses.length); await catchRevert(I_ERC20DividendCheckpoint.setDefaultExcluded(addresses, { from: token_owner })); }); @@ -510,7 +531,9 @@ contract("ERC20DividendCheckpoint", async (accounts) => { dividendName, { from: token_owner } ); + console.log("Gas used w/ max exclusions - default: " + tx.receipt.gasUsed); assert.equal(tx.logs[0].args._checkpointId.toNumber(), 3, "Dividend should be created at checkpoint 2"); + assert.equal((await I_ERC20DividendCheckpoint.isExcluded.call(account_temp, tx.logs[0].args._dividendIndex)), true, "account_temp is excluded"); }); it("should investor 3 claims dividend - fail bad index", async () => { @@ -529,6 +552,16 @@ contract("ERC20DividendCheckpoint", async (accounts) => { assert.equal(investor1BalanceAfter1.sub(investor1Balance).toNumber(), 0); assert.equal(investor2BalanceAfter1.sub(investor2Balance).toNumber(), 0); assert.equal(investor3BalanceAfter1.sub(investor3Balance).toString(), new BN(web3.utils.toWei("7", "ether")).toString()); + let info = await I_ERC20DividendCheckpoint.getDividendProgress.call(2); + console.log(info); + assert.equal(info[0][1], account_temp, "account_temp"); + assert.equal(info[1][1], false, "account_temp is not claimed"); + assert.equal(info[2][1], true, "account_temp is excluded"); + assert.equal(info[3][1], 0, "account_temp is not withheld"); + assert.equal(info[0][2], account_investor3, "account_investor3"); + assert.equal(info[1][2], true, "account_investor3 is claimed"); + assert.equal(info[2][2], false, "account_investor3 is claimed"); + assert.equal(info[3][2], 0, "account_investor3 is not withheld"); }); it("should investor 3 claims dividend - fails already claimed", async () => { @@ -667,7 +700,9 @@ contract("ERC20DividendCheckpoint", async (accounts) => { let addresses = []; addresses.push(account_temp); addresses.push(token_owner); - while (--limit > 42) addresses.push(web3.utils.toChecksumAddress('0x00000000000000000000000000000000000000' + limit)); + let tempAdd = '0x0000000000000000000000000000000000000000'; + while (--limit > 42) addresses.push(web3.utils.toChecksumAddress(tempAdd.substring(0, 42 - limit.toString().length) + limit)); + // while (--limit > 42) addresses.push(web3.utils.toChecksumAddress('0x00000000000000000000000000000000000000' + limit)); await catchRevert( I_ERC20DividendCheckpoint.createDividendWithCheckpointAndExclusions( maturity, @@ -697,7 +732,7 @@ contract("ERC20DividendCheckpoint", async (accounts) => { dividendName, { from: token_owner } ); - assert.equal(tx.logs[0].args._checkpointId.toNumber(), 4, "Dividend should be created at checkpoint 3"); + assert.equal(tx.logs[0].args._checkpointId.toNumber(), 4, "Dividend should be created at checkpoint 4"); }); it("Should not create new dividend with duplicate exclusion", async () => { @@ -825,10 +860,80 @@ contract("ERC20DividendCheckpoint", async (accounts) => { }); it("Issuer reclaims withholding tax", async () => { - let issuerBalance = new BN(await I_PolyToken.balanceOf(token_owner)); - await I_ERC20DividendCheckpoint.withdrawWithholding(3, { from: token_owner, gasPrice: 0 }); - let issuerBalanceAfter = new BN(await I_PolyToken.balanceOf(token_owner)); + let info = await I_ERC20DividendCheckpoint.getDividendProgress.call(3); + + console.log("Address:"); + console.log(info[0][0]); + console.log(info[0][1]); + console.log(info[0][2]); + console.log(info[0][3]); + + console.log("Claimed:"); + console.log(info[1][0]); + console.log(info[1][1]); + console.log(info[1][2]); + console.log(info[1][3]); + + console.log("Excluded:"); + console.log(info[2][0]); + console.log(info[2][1]); + console.log(info[2][2]); + console.log(info[2][3]); + + console.log("Withheld:"); + console.log(info[3][0].toString()); + console.log(info[3][1].toString()); + console.log(info[3][2].toString()); + console.log(info[3][3].toString()); + + console.log("Claimed:"); + console.log(info[4][0].toString()); + console.log(info[4][1].toString()); + console.log(info[4][2].toString()); + console.log(info[4][3].toString()); + + console.log("Balance:"); + console.log(info[5][0].toString()); + console.log(info[5][1].toString()); + console.log(info[5][2].toString()); + console.log(info[5][3].toString()); + + assert.equal(info[0][0], account_investor1, "account match"); + assert.equal(info[0][1], account_investor2, "account match"); + assert.equal(info[0][2], account_temp, "account match"); + assert.equal(info[0][3], account_investor3, "account match"); + + assert.equal(info[3][0].toString(), 0, "withheld match"); + assert.equal(info[3][1].toString(), new BN(web3.utils.toWei("0.2", "ether")).toString(), "withheld match"); + assert.equal(info[3][2].toString(), new BN(web3.utils.toWei("0.2", "ether")).toString(), "withheld match"); + assert.equal(info[3][3].toString(), 0, "withheld match"); + + assert.equal(info[4][0].toString(), 0, "excluded"); + assert.equal(info[4][1].toString(), new BN(web3.utils.toWei("1.8", "ether")).toString(), "claim match"); + assert.equal(info[4][2].toString(), new BN(web3.utils.toWei("0.8", "ether")).toString(), "claim match"); + assert.equal(info[4][3].toString(), new BN(web3.utils.toWei("7", "ether")).toString(), "claim match"); + + assert.equal(info[5][0].toString(), (await I_SecurityToken.balanceOfAt(account_investor1, new BN(4))).toString(), "balance match"); + assert.equal(info[5][1].toString(), (await I_SecurityToken.balanceOfAt(account_investor2, new BN(4))).toString(), "balance match"); + assert.equal(info[5][2].toString(), (await I_SecurityToken.balanceOfAt(account_temp, new BN(4))).toString(), "balance match"); + assert.equal(info[5][3].toString(), (await I_SecurityToken.balanceOfAt(account_investor3, new BN(4))).toString(), "balance match"); + + + let issuerBalance = new BN(await I_PolyToken.balanceOf(wallet)); + await I_ERC20DividendCheckpoint.withdrawWithholding(new BN(3), { from: token_owner, gasPrice: 0 }); + let issuerBalanceAfter = new BN(await I_PolyToken.balanceOf(wallet)); assert.equal(issuerBalanceAfter.sub(issuerBalance).toString(), new BN(web3.utils.toWei("0.4", "ether")).toString()); + + }); + + it("Issuer changes wallet address", async () => { + await catchRevert(I_ERC20DividendCheckpoint.changeWallet(token_owner, { from: wallet })); + await I_ERC20DividendCheckpoint.changeWallet(token_owner, {from: token_owner}); + let newWallet = await I_ERC20DividendCheckpoint.wallet.call(); + assert.equal(newWallet, token_owner, "Wallets match"); + await I_ERC20DividendCheckpoint.changeWallet(wallet, {from: token_owner}); + newWallet = await I_ERC20DividendCheckpoint.wallet.call(); + assert.equal(newWallet, wallet, "Wallets match"); }); it("Issuer unable to reclaim dividend (expiry not passed)", async () => { @@ -845,9 +950,9 @@ contract("ERC20DividendCheckpoint", async (accounts) => { }); it("Issuer is able to reclaim dividend after expiry", async () => { - let tokenOwnerBalance = new BN(await I_PolyToken.balanceOf(token_owner)); + let tokenOwnerBalance = new BN(await I_PolyToken.balanceOf(wallet)); await I_ERC20DividendCheckpoint.reclaimDividend(3, { from: token_owner, gasPrice: 0 }); - let tokenOwnerAfter = new BN(await I_PolyToken.balanceOf(token_owner)); + let tokenOwnerAfter = new BN(await I_PolyToken.balanceOf(wallet)); assert.equal(tokenOwnerAfter.sub(tokenOwnerBalance).toString(), new BN(web3.utils.toWei("7", "ether")).toString()); }); @@ -869,11 +974,6 @@ contract("ERC20DividendCheckpoint", async (accounts) => { assert.equal(index.length, 0); }); - it("Get the init data", async () => { - let tx = await I_ERC20DividendCheckpoint.getInitFunction.call(); - assert.equal(web3.utils.toAscii(tx).replace(/\u0000/g, ""), 0); - }); - it("Should get the listed permissions", async () => { let tx = await I_ERC20DividendCheckpoint.getPermissions.call(); assert.equal(tx.length, 2); @@ -1036,13 +1136,27 @@ contract("ERC20DividendCheckpoint", async (accounts) => { dividendName, { from: account_manager } ); - assert.equal(tx.logs[0].args._checkpointId.toNumber(), 8); + let info = await I_ERC20DividendCheckpoint.getCheckpointData.call(checkpointID); + + assert.equal(info[0][0], account_investor1, "account match"); + assert.equal(info[0][1], account_investor2, "account match"); + assert.equal(info[0][2], account_temp, "account match"); + assert.equal(info[0][3], account_investor3, "account match"); + assert.equal(info[1][0].toString(), (await I_SecurityToken.balanceOfAt.call(account_investor1, checkpointID)).toString(), "balance match"); + assert.equal(info[1][1].toString(), (await I_SecurityToken.balanceOfAt.call(account_investor2, checkpointID)).toString(), "balance match"); + assert.equal(info[1][2].toString(), (await I_SecurityToken.balanceOfAt.call(account_temp, checkpointID)).toString(), "balance match"); + assert.equal(info[1][3].toString(), (await I_SecurityToken.balanceOfAt.call(account_investor3, checkpointID)).toString(), "balance match"); + assert.equal(info[2][0].toNumber(), 0, "withholding match"); + assert.equal(info[2][1].toString(), new BN((10 * 10 ** 16).toString()).toString(), "withholding match"); + assert.equal(info[2][2].toString(), new BN((20 * 10 ** 16).toString()).toString(), "withholding match"); + assert.equal(info[2][3].toNumber(), 0, "withholding match"); + assert.equal(tx.logs[0].args._checkpointId.toNumber(), checkpointID); }); it("should allow manager with permission to create dividend with exclusion", async () => { - let maturity = await latestTime() + duration.days(1); - let expiry = await latestTime() + duration.days(10); - let exclusions = [one_address]; + let maturity = (await latestTime()) + duration.days(1); + let expiry = (await latestTime()) + duration.days(10); + let exclusions = [account_temp]; let tx = await I_ERC20DividendCheckpoint.createDividendWithExclusions( maturity, expiry, @@ -1053,6 +1167,10 @@ contract("ERC20DividendCheckpoint", async (accounts) => { { from: account_manager } ); assert.equal(tx.logs[0].args._checkpointId.toNumber(), 9); + console.log("Gas used w/ max exclusions - non-default: " + tx.receipt.gasUsed); + let info = await I_ERC20DividendCheckpoint.getDividendProgress.call(tx.logs[0].args._dividendIndex); + assert.equal(info[0][2], account_temp, "account_temp is excluded"); + assert.equal(info[2][2], true, "account_temp is excluded"); }); it("should allow manager with permission to create dividend with checkpoint and exclusion", async () => { diff --git a/test/f_ether_dividends.js b/test/f_ether_dividends.js index 8d9582283..dae87c572 100644 --- a/test/f_ether_dividends.js +++ b/test/f_ether_dividends.js @@ -4,6 +4,7 @@ import takeSnapshot, { increaseTime, revertToSnapshot } from "./helpers/time"; import { encodeProxyCall } from "./helpers/encodeCall"; import { catchRevert } from "./helpers/exceptions"; import { setUpPolymathNetwork, deployEtherDividendAndVerifyed, deployGPMAndVerifyed } from "./helpers/createInstances"; +import { encodeModuleCall } from "./helpers/encodeCall"; const SecurityToken = artifacts.require("./SecurityToken.sol"); const GeneralTransferManager = artifacts.require("./GeneralTransferManager"); @@ -19,6 +20,7 @@ contract("EtherDividendCheckpoint", async (accounts) => { let account_polymath; let account_issuer; let token_owner; + let wallet; let account_investor1; let account_investor2; let account_investor3; @@ -66,6 +68,7 @@ contract("EtherDividendCheckpoint", async (accounts) => { const transferManagerKey = 2; const stoKey = 3; const checkpointKey = 4; + const DividendParameters = ["address"]; // Initial fee for ticker registry and security token registry const initRegFee = new BN(web3.utils.toWei("250")); @@ -85,6 +88,7 @@ contract("EtherDividendCheckpoint", async (accounts) => { account_investor4 = accounts[9]; account_manager = accounts[5]; account_temp = accounts[2]; + wallet = accounts[3]; // Step 1: Deploy the genral PM ecosystem let instances = await setUpPolymathNetwork(account_polymath, token_owner); @@ -135,7 +139,7 @@ contract("EtherDividendCheckpoint", async (accounts) => { it("Should generate the new security token with the same symbol as registered above", async () => { await I_PolyToken.approve(I_STRProxied.address, initRegFee, { from: token_owner }); - + let tx = await I_STRProxied.generateSecurityToken(name, symbol, tokenDetails, false, { from: token_owner }); // Verify the successful generation of the security token @@ -157,8 +161,9 @@ contract("EtherDividendCheckpoint", async (accounts) => { it("Should successfully attach the ERC20DividendCheckpoint with the security token", async () => { await I_PolyToken.getTokens(new BN(web3.utils.toWei("500", "ether")), token_owner); + let bytesDividend = encodeModuleCall(DividendParameters, [wallet]); await catchRevert( - I_SecurityToken.addModule(P_EtherDividendCheckpointFactory.address, "0x0", new BN(web3.utils.toWei("500", "ether")), new BN(0), { + I_SecurityToken.addModule(P_EtherDividendCheckpointFactory.address, bytesDividend, new BN(web3.utils.toWei("500", "ether")), new BN(0), { from: token_owner }) ); @@ -167,7 +172,8 @@ contract("EtherDividendCheckpoint", async (accounts) => { it("Should successfully attach the EtherDividendCheckpoint with the security token", async () => { let snapId = await takeSnapshot(); await I_PolyToken.transfer(I_SecurityToken.address, new BN(web3.utils.toWei("500", "ether")), { from: token_owner }); - const tx = await I_SecurityToken.addModule(P_EtherDividendCheckpointFactory.address, "0x0", new BN(web3.utils.toWei("500", "ether")), new BN(0), { + let bytesDividend = encodeModuleCall(DividendParameters, [wallet]); + const tx = await I_SecurityToken.addModule(P_EtherDividendCheckpointFactory.address, bytesDividend, new BN(web3.utils.toWei("500", "ether")), new BN(0), { from: token_owner }); assert.equal(tx.logs[3].args._types[0].toNumber(), checkpointKey, "EtherDividendCheckpoint doesn't get deployed"); @@ -181,7 +187,8 @@ contract("EtherDividendCheckpoint", async (accounts) => { }); it("Should successfully attach the EtherDividendCheckpoint with the security token", async () => { - const tx = await I_SecurityToken.addModule(I_EtherDividendCheckpointFactory.address, "0x0", new BN(0), new BN(0), { from: token_owner }); + let bytesDividend = encodeModuleCall(DividendParameters, [wallet]); + const tx = await I_SecurityToken.addModule(I_EtherDividendCheckpointFactory.address, bytesDividend, new BN(0), new BN(0), { from: token_owner }); assert.equal(tx.logs[2].args._types[0].toNumber(), checkpointKey, "EtherDividendCheckpoint doesn't get deployed"); assert.equal( web3.utils.toAscii(tx.logs[2].args._name).replace(/\u0000/g, ""), @@ -344,9 +351,9 @@ contract("EtherDividendCheckpoint", async (accounts) => { }); it("Issuer reclaims withholding tax", async () => { - let issuerBalance = new BN(await web3.eth.getBalance(token_owner)); + let issuerBalance = new BN(await web3.eth.getBalance(wallet)); await I_EtherDividendCheckpoint.withdrawWithholding(0, { from: token_owner, gasPrice: 0 }); - let issuerBalanceAfter = new BN(await web3.eth.getBalance(token_owner)); + let issuerBalanceAfter = new BN(await web3.eth.getBalance(wallet)); assert.equal(issuerBalanceAfter.sub(issuerBalance).toString(), new BN(web3.utils.toWei("0.2", "ether")).toString()); }); @@ -492,9 +499,9 @@ contract("EtherDividendCheckpoint", async (accounts) => { }); it("Issuer withdraws new withholding tax", async () => { - let issuerBalance = new BN(await web3.eth.getBalance(token_owner)); + let issuerBalance = new BN(await web3.eth.getBalance(wallet)); await I_EtherDividendCheckpoint.withdrawWithholding(2, { from: token_owner, gasPrice: 0 }); - let issuerBalanceAfter = new BN(await web3.eth.getBalance(token_owner)); + let issuerBalanceAfter = new BN(await web3.eth.getBalance(wallet)); assert.equal(issuerBalanceAfter.sub(issuerBalance).toString(), new BN(web3.utils.toWei("0.6", "ether")).toString()); }); @@ -556,7 +563,9 @@ contract("EtherDividendCheckpoint", async (accounts) => { let addresses = []; addresses.push(account_temp); addresses.push(token_owner); - while (--limit > 42) addresses.push(web3.utils.toChecksumAddress('0x00000000000000000000000000000000000000' + limit)); + let tempAdd = '0x0000000000000000000000000000000000000000'; + while (--limit > 42) addresses.push(web3.utils.toChecksumAddress(tempAdd.substring(0, 42 - limit.toString().length) + limit)); + // while (--limit > 42) addresses.push(web3.utils.toChecksumAddress('0x00000000000000000000000000000000000000' + limit)); await catchRevert( I_EtherDividendCheckpoint.createDividendWithCheckpointAndExclusions(maturity, expiry, 4, addresses, dividendName, { from: token_owner, @@ -700,9 +709,9 @@ contract("EtherDividendCheckpoint", async (accounts) => { }); it("Issuer is able to reclaim dividend after expiry", async () => { - let tokenOwnerBalance = new BN(await web3.eth.getBalance(token_owner)); + let tokenOwnerBalance = new BN(await web3.eth.getBalance(wallet)); await I_EtherDividendCheckpoint.reclaimDividend(3, { from: token_owner, gasPrice: 0 }); - let tokenOwnerAfter = new BN(await web3.eth.getBalance(token_owner)); + let tokenOwnerAfter = new BN(await web3.eth.getBalance(wallet)); assert.equal(tokenOwnerAfter.sub(tokenOwnerBalance).toString(), new BN(web3.utils.toWei("7", "ether")).toString()); }); @@ -782,11 +791,6 @@ contract("EtherDividendCheckpoint", async (accounts) => { assert.equal(index.length, 0); }); - it("Get the init data", async () => { - let tx = await I_EtherDividendCheckpoint.getInitFunction.call(); - assert.equal(web3.utils.toAscii(tx).replace(/\u0000/g, ""), 0); - }); - it("Should get the listed permissions", async () => { let tx = await I_EtherDividendCheckpoint.getPermissions.call(); assert.equal(tx.length, 2); diff --git a/test/h_general_transfer_manager.js b/test/h_general_transfer_manager.js index 4876765fc..83f297ae7 100644 --- a/test/h_general_transfer_manager.js +++ b/test/h_general_transfer_manager.js @@ -160,7 +160,7 @@ contract("GeneralTransferManager", async (accounts) => { it("Should generate the new security token with the same symbol as registered above", async () => { await I_PolyToken.approve(I_STRProxied.address, initRegFee, { from: token_owner }); - + let tx = await I_STRProxied.generateSecurityToken(name, symbol, tokenDetails, false, { from: token_owner }); // Verify the successful generation of the security token @@ -941,3 +941,6 @@ function range1(i) { function rangeB(i) { return i ? rangeB(i - 1).concat(0) : []; } + +function range1(i) {return i?range1(i-1).concat(i):[]} +function rangeB(i) {return i?rangeB(i-1).concat(0):[]} diff --git a/test/helpers/createInstances.js b/test/helpers/createInstances.js index 4c9f6090c..130f0ff62 100644 --- a/test/helpers/createInstances.js +++ b/test/helpers/createInstances.js @@ -18,6 +18,7 @@ const ManualApprovalTransferManagerFactory = artifacts.require("./ManualApproval const TrackedRedemptionFactory = artifacts.require("./TrackedRedemptionFactory.sol"); const PercentageTransferManagerFactory = artifacts.require("./PercentageTransferManagerFactory.sol"); const PercentageTransferManager = artifacts.require("./PercentageTransferManager.sol"); +const BlacklistTransferManagerFactory = artifacts.require("./BlacklistTransferManagerFactory.sol"); const ScheduledCheckpointFactory = artifacts.require('./ScheduledCheckpointFactory.sol'); const USDTieredSTOFactory = artifacts.require("./USDTieredSTOFactory.sol"); const USDTieredSTO = artifacts.require("./USDTieredSTO"); @@ -29,7 +30,7 @@ const GeneralPermissionManager = artifacts.require("./GeneralPermissionManager.s const GeneralPermissionManagerFactory = artifacts.require("./GeneralPermissionManagerFactory.sol"); const CountTransferManager = artifacts.require("./CountTransferManager.sol"); const CountTransferManagerFactory = artifacts.require("./CountTransferManagerFactory.sol"); -const VolumeRestrictionTransferManagerFactory = artifacts.require("./LockupVolumeRestrictionTMFactory"); +const LockUpTransferManagerFactory = artifacts.require("./LockUpTransferManagerFactory"); const PreSaleSTOFactory = artifacts.require("./PreSaleSTOFactory.sol"); const PreSaleSTO = artifacts.require("./PreSaleSTO.sol"); const PolyToken = artifacts.require("./PolyToken.sol"); @@ -39,6 +40,10 @@ const DummySTO = artifacts.require("./DummySTO.sol"); const MockBurnFactory = artifacts.require("./MockBurnFactory.sol"); const STRGetter = artifacts.require("./STRGetter.sol"); const MockWrongTypeFactory = artifacts.require("./MockWrongTypeFactory.sol"); +const VolumeRestrictionTMFactory = artifacts.require("./VolumeRestrictionTMFactory.sol"); +const VolumeRestrictionTM = artifacts.require("./VolumeRestrictionTM.sol"); +const VestingEscrowWalletFactory = artifacts.require("./VestingEscrowWalletFactory.sol"); +const VestingEscrowWallet = artifacts.require("./VestingEscrowWallet.sol"); const Web3 = require("web3"); let BN = Web3.utils.BN; @@ -63,10 +68,13 @@ let I_CountTransferManagerFactory; let I_ERC20DividendCheckpointLogic; let I_ERC20DividendCheckpointFactory; let I_GeneralPermissionManagerLogic; +let I_VolumeRestrictionTMFactory; let I_GeneralPermissionManagerFactory; let I_GeneralTransferManagerLogic; let I_GeneralTransferManagerFactory; +let I_VestingEscrowWalletFactory; let I_GeneralTransferManager; +let I_VolumeRestrictionTMLogic; let I_ModuleRegistryProxy; let I_PreSaleSTOLogic; let I_PreSaleSTOFactory; @@ -83,6 +91,8 @@ let I_STFactory; let I_USDTieredSTOLogic; let I_PolymathRegistry; let I_SecurityTokenRegistryProxy; +let I_BlacklistTransferManagerFactory; +let I_VestingEscrowWalletLogic; let I_STRProxied; let I_MRProxied; let I_STRGetter; @@ -132,7 +142,7 @@ export async function setUpPolymathNetwork(account_polymath, token_owner) { return Promise.all(tempArray); } -async function deployPolyRegistryAndPolyToken(account_polymath, token_owner) { +export async function deployPolyRegistryAndPolyToken(account_polymath, token_owner) { // Step 0: Deploy the PolymathRegistry I_PolymathRegistry = await PolymathRegistry.new({ from: account_polymath }); @@ -265,6 +275,22 @@ export async function deployGTMAndVerifyed(accountPolymath, MRProxyInstance, set return Promise.all(new Array(I_GeneralTransferManagerFactory)); } +export async function deployVRTMAndVerifyed(accountPolymath, MRProxyInstance, setupCost) { + I_VolumeRestrictionTMLogic = await VolumeRestrictionTM.new("0x0000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000", { from: accountPolymath }); + + I_VolumeRestrictionTMFactory = await VolumeRestrictionTMFactory.new(setupCost, new BN(0), new BN(0), I_VolumeRestrictionTMLogic.address, { from: accountPolymath }); + + assert.notEqual( + I_VolumeRestrictionTMFactory.address.valueOf(), + "0x0000000000000000000000000000000000000000", + "VolumeRestrictionTMFactory contract was not deployed" + ); + + // (B) : Register the GeneralDelegateManagerFactory + await registerAndVerifyByMR(I_VolumeRestrictionTMFactory.address, accountPolymath, MRProxyInstance); + return new Array(I_VolumeRestrictionTMFactory); +} + export async function deployCountTMAndVerifyed(accountPolymath, MRProxyInstance, setupCost) { I_CountTransferManagerLogic = await CountTransferManager.new("0x0000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000", { from: accountPolymath }); I_CountTransferManagerFactory = await CountTransferManagerFactory.new(setupCost, new BN(0), new BN(0), I_CountTransferManagerLogic.address, { from: accountPolymath }); @@ -305,14 +331,27 @@ export async function deployPercentageTMAndVerified(accountPolymath, MRProxyInst return Promise.all(new Array(I_PercentageTransferManagerFactory)); } +export async function deployBlacklistTMAndVerified(accountPolymath, MRProxyInstance, setupCost) { + + I_BlacklistTransferManagerFactory = await BlacklistTransferManagerFactory.new(setupCost, new BN(0), new BN(0), { from: accountPolymath }); + assert.notEqual( + I_BlacklistTransferManagerFactory.address.valueOf(), + "0x0000000000000000000000000000000000000000", + "BlacklistTransferManagerFactory contract was not deployed" + ); + + await registerAndVerifyByMR(I_BlacklistTransferManagerFactory.address, accountPolymath, MRProxyInstance); + return new Array(I_BlacklistTransferManagerFactory); +} + export async function deployLockupVolumeRTMAndVerified(accountPolymath, MRProxyInstance, setupCost) { - I_VolumeRestrictionTransferManagerFactory = await VolumeRestrictionTransferManagerFactory.new(setupCost, new BN(0), new BN(0), { + I_VolumeRestrictionTransferManagerFactory = await LockUpTransferManagerFactory.new(setupCost, new BN(0), new BN(0), { from: accountPolymath }); assert.notEqual( I_VolumeRestrictionTransferManagerFactory.address.valueOf(), "0x0000000000000000000000000000000000000000", - "VolumeRestrictionTransferManagerFactory contract was not deployed" + "LockUpTransferManagerFactory contract was not deployed" ); await registerAndVerifyByMR(I_VolumeRestrictionTransferManagerFactory.address, accountPolymath, MRProxyInstance); @@ -465,6 +504,20 @@ export async function deployRedemptionAndVerifyed(accountPolymath, MRProxyInstan return Promise.all(new Array(I_TrackedRedemptionFactory)); } +export async function deployVestingEscrowWalletAndVerifyed(accountPolymath, MRProxyInstance, setupCost) { + I_VestingEscrowWalletLogic = await VestingEscrowWallet.new("0x0000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000", { from: accountPolymath }); + I_VestingEscrowWalletFactory = await VestingEscrowWalletFactory.new(setupCost, new BN(0), new BN(0), I_VestingEscrowWalletLogic.address, { from: accountPolymath }); + + assert.notEqual( + I_VestingEscrowWalletFactory.address.valueOf(), + "0x0000000000000000000000000000000000000000", + "VestingEscrowWalletFactory contract was not deployed" + ); + + await registerAndVerifyByMR(I_VestingEscrowWalletFactory.address, accountPolymath, MRProxyInstance); + return new Array(I_VestingEscrowWalletFactory); +} + export async function deployMockRedemptionAndVerifyed(accountPolymath, MRProxyInstance, setupCost) { I_MockBurnFactory = await MockBurnFactory.new(setupCost, new BN(0), new BN(0), { from: accountPolymath }); diff --git a/test/helpers/exceptions.js b/test/helpers/exceptions.js index 22c05be07..ea0327af8 100644 --- a/test/helpers/exceptions.js +++ b/test/helpers/exceptions.js @@ -25,6 +25,9 @@ module.exports = { catchRevert: async function(promise) { await tryCatch(promise, "revert"); }, + catchPermission: async function(promise) { + await tryCatch(promise, "revert Permission check failed"); + }, catchOutOfGas: async function(promise) { await tryCatch(promise, "out of gas"); }, diff --git a/test/j_manual_approval_transfer_manager.js b/test/j_manual_approval_transfer_manager.js index 7ac1bd4cf..26580b093 100644 --- a/test/j_manual_approval_transfer_manager.js +++ b/test/j_manual_approval_transfer_manager.js @@ -3,12 +3,7 @@ import { duration, ensureException, promisifyLogWatch, latestBlock } from "./hel import takeSnapshot, { increaseTime, revertToSnapshot } from "./helpers/time"; import { encodeProxyCall } from "./helpers/encodeCall"; import { catchRevert } from "./helpers/exceptions"; -import { - setUpPolymathNetwork, - deployManualApprovalTMAndVerifyed, - deployGPMAndVerifyed, - deployCountTMAndVerifyed -} from "./helpers/createInstances"; +import { setUpPolymathNetwork, deployManualApprovalTMAndVerifyed, deployGPMAndVerifyed, deployCountTMAndVerifyed } from "./helpers/createInstances"; const SecurityToken = artifacts.require("./SecurityToken.sol"); const GeneralTransferManager = artifacts.require("./GeneralTransferManager"); @@ -20,7 +15,7 @@ const Web3 = require("web3"); let BN = Web3.utils.BN; const web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545")); // Hardcoded development port -contract("ManualApprovalTransferManager", async (accounts) => { +contract("ManualApprovalTransferManager", accounts => { // Accounts Variable declaration let account_polymath; let account_issuer; @@ -31,6 +26,11 @@ contract("ManualApprovalTransferManager", async (accounts) => { let account_investor4; let account_investor5; + // investor Details + let fromTime = latestTime(); + let toTime = latestTime(); + let expiryTime = toTime + duration.days(15); + let message = "Transaction Should Fail!"; // Contract Instance Declaration @@ -55,7 +55,6 @@ contract("ManualApprovalTransferManager", async (accounts) => { let I_SecurityToken; let I_PolyToken; let I_PolymathRegistry; - let I_STRGetter; // SecurityToken Details const name = "Team"; @@ -69,17 +68,17 @@ contract("ManualApprovalTransferManager", async (accounts) => { const transferManagerKey = 2; const stoKey = 3; + let expiryTimeMA; + let approvalTime; + // Initial fee for ticker registry and security token registry - const initRegFee = new BN(web3.utils.toWei("250")); + const initRegFee = web3.utils.toWei("250"); const STOParameters = ["uint256", "uint256", "uint256", "uint256", "uint8[]", "address"]; - let currentTime; - const address_zero = "0x0000000000000000000000000000000000000000"; - const one_address = "0x0000000000000000000000000000000000000001"; - before(async () => { currentTime = new BN(await latestTime()); + // Accounts setup account_polymath = accounts[0]; account_issuer = accounts[1]; @@ -105,8 +104,7 @@ contract("ManualApprovalTransferManager", async (accounts) => { I_STFactory, I_SecurityTokenRegistry, I_SecurityTokenRegistryProxy, - I_STRProxied, - I_STRGetter + I_STRProxied ] = instances; // STEP 2: Deploy the GeneralDelegateManagerFactory @@ -114,11 +112,7 @@ contract("ManualApprovalTransferManager", async (accounts) => { // STEP 3: Deploy the ManualApprovalTransferManagerFactory [I_ManualApprovalTransferManagerFactory] = await deployManualApprovalTMAndVerifyed(account_polymath, I_MRProxied, 0); // STEP 4: Deploy the Paid ManualApprovalTransferManagerFactory - [P_ManualApprovalTransferManagerFactory] = await deployManualApprovalTMAndVerifyed( - account_polymath, - I_MRProxied, - new BN(web3.utils.toWei("500", "ether")) - ); + [P_ManualApprovalTransferManagerFactory] = await deployManualApprovalTMAndVerifyed(account_polymath, I_MRProxied, web3.utils.toWei("500", "ether")); // STEP 5: Deploy the CountTransferManagerFactory [I_CountTransferManagerFactory] = await deployCountTMAndVerifyed(account_polymath, I_MRProxied, 0); @@ -151,7 +145,7 @@ contract("ManualApprovalTransferManager", async (accounts) => { it("Should generate the new security token with the same symbol as registered above", async () => { await I_PolyToken.approve(I_STRProxied.address, initRegFee, { from: token_owner }); - + let tx = await I_STRProxied.generateSecurityToken(name, symbol, tokenDetails, false, { from: token_owner }); // Verify the successful generation of the security token @@ -162,7 +156,7 @@ contract("ManualApprovalTransferManager", async (accounts) => { const log = (await I_SecurityToken.getPastEvents('ModuleAdded', {filter: {transactionHash: tx.transactionHash}}))[0]; // Verify that GeneralTransferManager module get added successfully or not - assert.equal(log.args._types[0].toNumber(), 2); + assert.equal(log.args._types[0].toString(), 2); assert.equal(web3.utils.toUtf8(log.args._name), "GeneralTransferManager"); }); @@ -180,7 +174,7 @@ contract("ManualApprovalTransferManager", async (accounts) => { account_investor1, currentTime, currentTime, - currentTime.add(new BN(duration.days(10))), + currentTime.add(new BN(duration.days(30))), true, { from: account_issuer, @@ -196,11 +190,11 @@ contract("ManualApprovalTransferManager", async (accounts) => { // Jump time await increaseTime(5000); - + currentTime = new BN(await latestTime()); // Mint some tokens - await I_SecurityToken.mint(account_investor1, new BN(web3.utils.toWei("4", "ether")), { from: token_owner }); + await I_SecurityToken.mint(account_investor1, web3.utils.toWei("30", "ether"), { from: token_owner }); - assert.equal((await I_SecurityToken.balanceOf(account_investor1)).toString(), new BN(web3.utils.toWei("4", "ether")).toString()); + assert.equal((await I_SecurityToken.balanceOf(account_investor1)).toString(), web3.utils.toWei("30", "ether")); }); it("Should Buy some more tokens", async () => { @@ -210,7 +204,7 @@ contract("ManualApprovalTransferManager", async (accounts) => { account_investor2, currentTime, currentTime, - currentTime.add(new BN(duration.days(10))), + currentTime.add(new BN(duration.days(30))), true, { from: account_issuer, @@ -225,15 +219,15 @@ contract("ManualApprovalTransferManager", async (accounts) => { ); // Mint some tokens - await I_SecurityToken.mint(account_investor2, new BN(web3.utils.toWei("1", "ether")), { from: token_owner }); + await I_SecurityToken.mint(account_investor2, web3.utils.toWei("10", "ether"), { from: token_owner }); - assert.equal((await I_SecurityToken.balanceOf(account_investor2)).toString(), new BN(web3.utils.toWei("1", "ether")).toString()); + assert.equal((await I_SecurityToken.balanceOf(account_investor2)).toString(), web3.utils.toWei("10", "ether")); }); it("Should successfully attach the ManualApprovalTransferManager with the security token", async () => { - await I_PolyToken.getTokens(new BN(web3.utils.toWei("500", "ether")), token_owner); + await I_PolyToken.getTokens(web3.utils.toWei("500", "ether"), token_owner); await catchRevert( - I_SecurityToken.addModule(P_ManualApprovalTransferManagerFactory.address, "0x", new BN(web3.utils.toWei("500", "ether")), new BN(0), { + I_SecurityToken.addModule(P_ManualApprovalTransferManagerFactory.address, "0x0", web3.utils.toWei("500", "ether"), 0, { from: token_owner }) ); @@ -241,15 +235,15 @@ contract("ManualApprovalTransferManager", async (accounts) => { it("Should successfully attach the General permission manager factory with the security token", async () => { let snapId = await takeSnapshot(); - await I_PolyToken.transfer(I_SecurityToken.address, new BN(web3.utils.toWei("500", "ether")), { from: token_owner }); + await I_PolyToken.transfer(I_SecurityToken.address, web3.utils.toWei("500", "ether"), { from: token_owner }); const tx = await I_SecurityToken.addModule( P_ManualApprovalTransferManagerFactory.address, - "0x", - new BN(web3.utils.toWei("500", "ether")), - new BN(0), + "0x0", + web3.utils.toWei("500", "ether"), + 0, { from: token_owner } ); - assert.equal(tx.logs[3].args._types[0].toNumber(), transferManagerKey, "Manual Approval Transfer Manager doesn't get deployed"); + assert.equal(tx.logs[3].args._types[0].toString(), transferManagerKey, "Manual Approval Transfer Manager doesn't get deployed"); assert.equal( web3.utils.toAscii(tx.logs[3].args._name).replace(/\u0000/g, ""), "ManualApprovalTransferManager", @@ -260,8 +254,8 @@ contract("ManualApprovalTransferManager", async (accounts) => { }); it("Should successfully attach the ManualApprovalTransferManager with the security token", async () => { - const tx = await I_SecurityToken.addModule(I_ManualApprovalTransferManagerFactory.address, "0x0", new BN(0), new BN(0), { from: token_owner }); - assert.equal(tx.logs[2].args._types[0].toNumber(), transferManagerKey, "ManualApprovalTransferManager doesn't get deployed"); + const tx = await I_SecurityToken.addModule(I_ManualApprovalTransferManagerFactory.address, "0x0", 0, 0, { from: token_owner }); + assert.equal(tx.logs[2].args._types[0].toString(), transferManagerKey, "ManualApprovalTransferManager doesn't get deployed"); assert.equal( web3.utils.toUtf8(tx.logs[2].args._name), "ManualApprovalTransferManager", @@ -275,7 +269,7 @@ contract("ManualApprovalTransferManager", async (accounts) => { I_ManualApprovalTransferManager.verifyTransfer( account_investor4, account_investor4, - new BN(web3.utils.toWei("2", "ether")), + web3.utils.toWei("2", "ether"), "0x0", true, { from: token_owner } @@ -287,7 +281,7 @@ contract("ManualApprovalTransferManager", async (accounts) => { await I_ManualApprovalTransferManager.verifyTransfer( account_investor4, account_investor4, - new BN(web3.utils.toWei("2", "ether")), + web3.utils.toWei("2", "ether"), "0x0", false, { from: token_owner } @@ -313,31 +307,32 @@ contract("ManualApprovalTransferManager", async (accounts) => { "Failed in adding the investor in whitelist" ); // Pause at the transferManager level - await I_ManualApprovalTransferManager.pause({ from: token_owner }); + await I_ManualApprovalTransferManager.pause({from: token_owner}); // Add the Investor in to the whitelist // Mint some tokens - await I_SecurityToken.mint(account_investor3, new BN(web3.utils.toWei("1", "ether")), { from: token_owner }); + await I_SecurityToken.mint(account_investor3, web3.utils.toWei("10", "ether"), { from: token_owner }); - assert.equal((await I_SecurityToken.balanceOf(account_investor3)).toString(), new BN(web3.utils.toWei("1", "ether")).toString()); + assert.equal((await I_SecurityToken.balanceOf(account_investor3)).toString(), web3.utils.toWei("10", "ether")); // Unpause at the transferManager level - await I_ManualApprovalTransferManager.unpause({ from: token_owner }); + await I_ManualApprovalTransferManager.unpause({from: token_owner}); }); it("Should still be able to transfer between existing token holders", async () => { // Add the Investor in to the whitelist // Mint some tokens - await I_SecurityToken.transfer(account_investor1, new BN(web3.utils.toWei("1", "ether")), { from: account_investor2 }); + await I_SecurityToken.transfer(account_investor1, web3.utils.toWei("1", "ether"), { from: account_investor2 }); - assert.equal((await I_SecurityToken.balanceOf(account_investor1)).toString(), new BN(web3.utils.toWei("5", "ether")).toString()); + assert.equal((await I_SecurityToken.balanceOf(account_investor1)).toString(), web3.utils.toWei("31", "ether")); }); it("Should fail to add a manual approval because invalid _to address", async () => { await catchRevert( I_ManualApprovalTransferManager.addManualApproval( account_investor1, - address_zero, - new BN(web3.utils.toWei("2", "ether")), + "0x0000000000000000000000000000000000000000", + web3.utils.toWei("2", "ether"), currentTime.add(new BN(duration.days(1))), + web3.utils.fromAscii("DESCRIPTION"), { from: token_owner } ) ); @@ -348,159 +343,434 @@ contract("ManualApprovalTransferManager", async (accounts) => { I_ManualApprovalTransferManager.addManualApproval( account_investor1, account_investor4, - new BN(web3.utils.toWei("2", "ether")), + web3.utils.toWei("2", "ether"), 99999, + web3.utils.fromAscii("DESCRIPTION"), { from: token_owner } ) ); }); - it("Add a manual approval for a 4th investor", async () => { + it("Add a manual approval for a 4th investor & return correct length", async () => { + approvalTime = currentTime.add(new BN(duration.days(1))); await I_ManualApprovalTransferManager.addManualApproval( account_investor1, account_investor4, - new BN(web3.utils.toWei("2", "ether")), - currentTime.add(new BN(duration.days(1))), - { from: token_owner } + web3.utils.toWei("3", "ether"), + approvalTime, + web3.utils.fromAscii("DESCRIPTION"), + { + from: token_owner + } ); + assert.equal((await I_ManualApprovalTransferManager.getTotalApprovalsLength.call()).toString(), 1); }); - it("Add a manual approval for a 5th investor from issuance", async () => { - await I_ManualApprovalTransferManager.addManualApproval( - address_zero, - account_investor5, - new BN(web3.utils.toWei("2", "ether")), - currentTime.add(new BN(duration.days(1))), - { from: token_owner } - ); - }); + it("Should return all approvals correctly", async () => { + + console.log("current approval length is " + (await I_ManualApprovalTransferManager.getTotalApprovalsLength.call()).toString()); - it("Should fail to add a manual approval because allowance is laready exists", async () => { + let tx = await I_ManualApprovalTransferManager.getAllApprovals({from: token_owner }); + assert.equal(tx[0][0], account_investor1); + console.log("1"); + assert.equal(tx[1][0], account_investor4); + console.log("2"); + assert.equal(tx[2][0], web3.utils.toWei("3")); + console.log("3"); + assert.equal(tx[3][0].toString(), approvalTime); + console.log("4"); + assert.equal(web3.utils.toUtf8(tx[4][0]), "DESCRIPTION"); + }) + + it("Should try to add the same manual approval for the same `_from` & `_to` address", async() => { await catchRevert( I_ManualApprovalTransferManager.addManualApproval( account_investor1, account_investor4, - new BN(web3.utils.toWei("2", "ether")), - currentTime.add(new BN(duration.days(5))), - { from: token_owner } + web3.utils.toWei("5", "ether"), + currentTime.add(new BN(duration.days(1))), + web3.utils.fromAscii("DESCRIPTION"), + { + from: token_owner + } ) ); + }) + + it("Check verifyTransfer without actually transferring", async () => { + let verified = await I_SecurityToken.verifyTransfer.call( + account_investor1, + account_investor4, + web3.utils.toWei("2", "ether"), + "0x0" + ); + console.log(JSON.stringify(verified)); + assert.equal(verified, true); + + verified = await I_SecurityToken.verifyTransfer.call(account_investor1, account_investor4, web3.utils.toWei("4", "ether"), "0x0"); + assert.equal(verified, false); + + verified = await I_SecurityToken.verifyTransfer.call(account_investor1, account_investor4, web3.utils.toWei("1", "ether"), "0x0"); + assert.equal(verified, true); }); - it("Should fail to revoke manual approval because invalid _to address", async () => { - await catchRevert(I_ManualApprovalTransferManager.revokeManualApproval(account_investor1, address_zero, { from: token_owner })); + it("Should fail to sell the tokens more than the allowance", async() => { + await catchRevert( + I_SecurityToken.transfer(account_investor4, web3.utils.toWei("4"), {from: account_investor1}) + ); + }) + + it("Approval fails with wrong from to address", async () => { + await catchRevert(I_SecurityToken.transfer(account_investor5, web3.utils.toWei("1", "ether"), { from: account_investor1 })); }); - it("Should revoke manual approval", async () => { - let tx = await I_ManualApprovalTransferManager.revokeManualApproval(account_investor1, account_investor4, { - from: token_owner - }); - assert.equal(tx.logs[0].args._from, account_investor1); - assert.equal(tx.logs[0].args._to, account_investor4); - assert.equal(tx.logs[0].args._addedBy, token_owner); + it("Should sell the tokens to investor 4 (GTM will give INVALID as investor 4 not in the whitelist)", async() => { + let oldBal4 = await I_SecurityToken.balanceOf.call(account_investor4); + await I_SecurityToken.transfer(account_investor4, web3.utils.toWei("1"), {from: account_investor1}); + let newBal4 = await I_SecurityToken.balanceOf.call(account_investor4); + assert.equal((newBal4.sub(oldBal4)).div(new BN(10).pow(new BN(18))).toString(), 1); + }); + + it("Should sell more tokens to investor 4 with in the same day(GTM will give INVALID as investor 4 not in the whitelist)", async() => { + let oldBal4 = await I_SecurityToken.balanceOf.call(account_investor4); + await I_SecurityToken.transfer(account_investor4, web3.utils.toWei("1"), {from: account_investor1}); + let newBal4 = await I_SecurityToken.balanceOf.call(account_investor4); + assert.equal((newBal4.sub(oldBal4)).div(new BN(10).pow(new BN(18))).toString(), 1); + + + let tx = await I_ManualApprovalTransferManager.getActiveApprovalsToUser.call(account_investor1); + let tx2 = await I_ManualApprovalTransferManager.getActiveApprovalsToUser.call(account_investor4); + + assert.equal(tx2[0].length, 1); + assert.equal(tx[0].length, 1); + }); + + it("Should fail to transact after the approval get expired", async() => { + await increaseTime(duration.days(1)); + currentTime = new BN(await latestTime()); + await catchRevert( + I_SecurityToken.transfer(account_investor4, web3.utils.toWei("1"), {from: account_investor1}) + ); + }); + + it("Should fail to modify the manual approval when the approval get expired", async() => { + await catchRevert( + I_ManualApprovalTransferManager.modifyManualApproval( + account_investor1, + account_investor4, + currentTime.add(new BN(duration.days(2))), + web3.utils.toWei("5"), + web3.utils.fromAscii("New Description"), + 0, + { + from: token_owner + } + ) + ); + }); + + it("Should attach the manual approval for the investor4 again", async() => { + assert.equal((await I_ManualApprovalTransferManager.getActiveApprovalsToUser.call(account_investor4))[0].length, 0); + currentTime = new BN(await latestTime()); await I_ManualApprovalTransferManager.addManualApproval( account_investor1, account_investor4, - new BN(web3.utils.toWei("2", "ether")), + web3.utils.toWei("2", "ether"), currentTime.add(new BN(duration.days(1))), - { from: token_owner } + web3.utils.fromAscii("DESCRIPTION"), + { + from: token_owner + } ); + assert.equal((await I_ManualApprovalTransferManager.getTotalApprovalsLength.call()).toString(), 1); + let data = await I_ManualApprovalTransferManager.approvals.call(0); + assert.equal(data[0], account_investor1); + assert.equal(data[1], account_investor4); + assert.equal(data[2], web3.utils.toWei("2")); + assert.equal(web3.utils.toUtf8(data[4]), "DESCRIPTION"); + }); + + it("Should modify the manual approval expiry time for 4th investor", async () => { + currentTime = new BN(await latestTime()); + expiryTimeMA = currentTime.add(new BN(duration.days(3))); + let tx = await I_ManualApprovalTransferManager.modifyManualApproval( + account_investor1, + account_investor4, + expiryTimeMA, + web3.utils.toWei("5"), + web3.utils.fromAscii("New Description"), + 45, + { + from: token_owner + } + ); + + let data = await I_ManualApprovalTransferManager.approvals.call(0); + assert.equal(data[0], account_investor1); + assert.equal(data[1], account_investor4); + assert.equal(data[2], web3.utils.toWei("2")); + assert.equal(data[3].toString(), expiryTimeMA); + assert.equal(web3.utils.toUtf8(data[4]), "New Description"); + assert.equal(tx.logs[0].args._from, account_investor1); + assert.equal(tx.logs[0].args._to, account_investor4); + assert.equal((tx.logs[0].args._expiryTime).toString(), expiryTimeMA); + assert.equal((tx.logs[0].args._allowance).toString(), web3.utils.toWei("2")); + assert.equal(web3.utils.toUtf8(tx.logs[0].args._description), "New Description"); }); - it("Use 50% of manual approval for transfer", async () => { - await I_SecurityToken.transfer(account_investor4, new BN(web3.utils.toWei("1", "ether")), { from: account_investor1 }); - - assert.equal((await I_SecurityToken.balanceOf(account_investor4)).toString(), new BN(web3.utils.toWei("1", "ether")).toString()); + it("Should transact after two days", async() => { + await increaseTime(2); + currentTime = new BN(await latestTime()); + let oldBal4 = await I_SecurityToken.balanceOf.call(account_investor4); + await I_SecurityToken.transfer(account_investor4, web3.utils.toWei("1"), {from: account_investor1}); + let newBal4 = await I_SecurityToken.balanceOf.call(account_investor4); + assert.equal((newBal4.sub(oldBal4)).div(new BN(10).pow(new BN(18))).toString(), 1); }); - it("Approval fails with wrong from to address", async () => { - await catchRevert(I_SecurityToken.transfer(account_investor5, new BN(web3.utils.toWei("1", "ether")), { from: account_investor1 })); + it("Should modify the allowance of the manual approval (increase)", async() => { + currentTime = new BN(await latestTime()); + await I_ManualApprovalTransferManager.modifyManualApproval( + account_investor1, + account_investor4, + expiryTimeMA, + web3.utils.toWei("4"), + web3.utils.fromAscii("New Description"), + 1, + { + from: token_owner + } + ); + + let data = await I_ManualApprovalTransferManager.approvals.call(0); + assert.equal(data[0], account_investor1); + assert.equal(data[1], account_investor4); + assert.equal(data[2].toString(), web3.utils.toWei("5")); + assert.equal(data[3].toString(), expiryTimeMA); + assert.equal(web3.utils.toUtf8(data[4]), "New Description"); }); - it("Use 100% of issuance approval", async () => { - await I_SecurityToken.mint(account_investor5, new BN(web3.utils.toWei("2", "ether")), { from: token_owner }); - assert.equal((await I_SecurityToken.balanceOf(account_investor5)).toString(), new BN(web3.utils.toWei("2", "ether")).toString()); + it("Should transact according to new allowance", async() => { + currentTime = new BN(await latestTime()); + let oldBal4 = await I_SecurityToken.balanceOf.call(account_investor4); + await I_SecurityToken.transfer(account_investor4, web3.utils.toWei("3"), {from: account_investor1}); + let newBal4 = await I_SecurityToken.balanceOf.call(account_investor4); + assert.equal((newBal4.sub(oldBal4)).div(new BN(10).pow(new BN(18))).toString(), 3); }); - it("Check verifyTransfer without actually transferring", async () => { - let verified = await I_SecurityToken.verifyTransfer.call( + it("Should decrease the allowance", async() => { + currentTime = new BN(await latestTime()); + await I_ManualApprovalTransferManager.modifyManualApproval( account_investor1, account_investor4, - new BN(web3.utils.toWei("1", "ether")), - "0x0" + expiryTimeMA, + web3.utils.toWei("1"), + web3.utils.fromAscii("New Description"), + 0, + { + from: token_owner + } ); - assert.equal(verified, true); - - verified = await I_SecurityToken.verifyTransfer.call(account_investor1, account_investor4, new BN(web3.utils.toWei("2", "ether")), "0x0"); - assert.equal(verified, false); - verified = await I_SecurityToken.verifyTransfer.call(account_investor1, account_investor4, new BN(web3.utils.toWei("1", "ether")), "0x0"); - assert.equal(verified, true); + let data = await I_ManualApprovalTransferManager.approvals.call(0); + assert.equal(data[0], account_investor1); + assert.equal(data[1], account_investor4); + assert.equal(data[2].toString(), web3.utils.toWei("1")); + assert.equal(data[3].toString(), expiryTimeMA); + assert.equal(web3.utils.toUtf8(data[4]), "New Description"); }); - it("Use remaining 50% of manual approval for transfer", async () => { - await I_SecurityToken.transfer(account_investor4, new BN(web3.utils.toWei("1", "ether")), { from: account_investor1 }); - - assert.equal((await I_SecurityToken.balanceOf(account_investor4)).toString(), new BN(web3.utils.toWei("2", "ether")).toString()); + it("Should fail to transfer the tokens because allowance get changed", async() => { + await catchRevert( + I_SecurityToken.transfer(account_investor4, web3.utils.toWei("2"), {from: account_investor1}) + ); }); - it("Check further transfers fail", async () => { - await catchRevert(I_SecurityToken.transfer(account_investor4, new BN(web3.utils.toWei("1", "ether")), { from: account_investor1 })); - - //Check that other transfers are still valid - await I_SecurityToken.transfer(account_investor2, new BN(web3.utils.toWei("1", "ether")), { from: account_investor1 }); + it("Should successfully transfer the tokens within the allowance limit", async() => { + currentTime = new BN(await latestTime()); + let oldBal4 = await I_SecurityToken.balanceOf.call(account_investor4); + await I_SecurityToken.transfer(account_investor4, web3.utils.toWei("1"), {from: account_investor1}); + let newBal4 = await I_SecurityToken.balanceOf.call(account_investor4); + assert.equal((newBal4.sub(oldBal4)).div(new BN(10).pow(new BN(18))).toString(), 1); }); - it("Should fail to add a manual block because invalid _to address", async () => { + it("Should fail to modify because allowance is zero", async() => { await catchRevert( - I_ManualApprovalTransferManager.addManualBlocking(account_investor1, address_zero, currentTime.add(new BN(duration.days(1))), { - from: token_owner - }) + I_ManualApprovalTransferManager.modifyManualApproval( + account_investor1, + account_investor4, + expiryTimeMA, + web3.utils.toWei("5"), + web3.utils.fromAscii("New Description"), + 0, + { + from: token_owner + } + ) ); }); - it("Should fail to add a manual block because invalid expiry time", async () => { + it("Should fail to revoke the manual Approval -- bad owner", async() => { await catchRevert( - I_ManualApprovalTransferManager.addManualBlocking(account_investor1, account_investor2, 99999, { from: token_owner }) + I_ManualApprovalTransferManager.revokeManualApproval(account_investor1, account_investor4, {from: account_investor5}) ); - }); + }) - it("Add a manual block for a 2nd investor", async () => { - await I_ManualApprovalTransferManager.addManualBlocking(account_investor1, account_investor2, currentTime.add(new BN(duration.days(1))), { - from: token_owner - }); + it("Should revoke the manual Approval b/w investor4 and 1", async() => { + await I_ManualApprovalTransferManager.revokeManualApproval(account_investor1, account_investor4, {from: token_owner}); + assert.equal((await I_ManualApprovalTransferManager.getActiveApprovalsToUser.call(account_investor1))[0].length, 0); + assert.equal((await I_ManualApprovalTransferManager.getActiveApprovalsToUser.call(account_investor4))[0].length, 0); }); - it("Should fail to add a manual block because blocking already exist", async () => { + it("Should fail to revoke the same manual approval again", async() => { await catchRevert( - I_ManualApprovalTransferManager.addManualBlocking(account_investor1, account_investor2, currentTime.add(new BN(duration.days(5))), { - from: token_owner - }) + I_ManualApprovalTransferManager.revokeManualApproval(account_investor1, account_investor4, {from: token_owner}) ); }); - it("Check manual block causes failure", async () => { - await catchRevert(I_SecurityToken.transfer(account_investor2, new BN(web3.utils.toWei("1", "ether")), { from: account_investor1 })); + it("Should fail to add multiple manual approvals -- failed because of bad owner", async () => { + await catchRevert ( + I_ManualApprovalTransferManager.addManualApprovalMulti( + [account_investor2,account_investor3], + [account_investor3,account_investor4], + [web3.utils.toWei("2", "ether"), web3.utils.toWei("2", "ether")], + [currentTime.add(new BN(duration.days(1))),currentTime.add(new BN(duration.days(1)))], + [web3.utils.fromAscii("DESCRIPTION_1"), web3.utils.fromAscii("DESCRIPTION_2")], + { + from: account_investor5 + } + ) + ) + }); + + it("Should fail to add multiple manual approvals -- failed because of length mismatch", async () => { + await catchRevert ( + I_ManualApprovalTransferManager.addManualApprovalMulti( + [account_investor2], + [account_investor3,account_investor4], + [web3.utils.toWei("2", "ether"), web3.utils.toWei("2", "ether")], + [currentTime.add(new BN(duration.days(1))),currentTime.add(new BN(duration.days(1)))], + [web3.utils.fromAscii("DESCRIPTION_1"), web3.utils.fromAscii("DESCRIPTION_2")], + { + from: token_owner + } + ) + ) + }); + + it("Should fail to add multiple manual approvals -- failed because of length mismatch", async () => { + await catchRevert ( + I_ManualApprovalTransferManager.addManualApprovalMulti( + [account_investor2,account_investor3], + [account_investor3,account_investor4], + [web3.utils.toWei("2", "ether"), web3.utils.toWei("2", "ether")], + [currentTime.add(new BN(duration.days(1)))], + [web3.utils.fromAscii("DESCRIPTION_1"), web3.utils.fromAscii("DESCRIPTION_2")], + { + from: token_owner + } + ) + ) + }); + + it("Should fail to add multiple manual approvals -- failed because of length mismatch", async () => { + await catchRevert ( + I_ManualApprovalTransferManager.addManualApprovalMulti( + [account_investor2,account_investor3], + [account_investor3,account_investor4], + [web3.utils.toWei("2", "ether")], + [currentTime.add(new BN(duration.days(1))),currentTime.add(new BN(duration.days(1)))], + [web3.utils.fromAscii("DESCRIPTION_1"), web3.utils.fromAscii("DESCRIPTION_2")], + { + from: token_owner + } + ) + ) + }); + + it("Should fail to add multiple manual approvals -- failed because of length mismatch", async () => { + await catchRevert ( + I_ManualApprovalTransferManager.addManualApprovalMulti( + [account_investor2,account_investor3], + [account_investor3,account_investor4], + [web3.utils.toWei("2", "ether"), web3.utils.toWei("2", "ether")], + [currentTime.add(new BN(duration.days(1))),currentTime.add(new BN(duration.days(1)))], + [web3.utils.fromAscii("DESCRIPTION_1")], + { + from: token_owner + } + ) + ) }); - it("Should fail to revoke manual block because invalid _to address", async () => { - await catchRevert(I_ManualApprovalTransferManager.revokeManualBlocking(account_investor1, address_zero, { from: token_owner })); - }); + it("Add multiple manual approvals", async () => { + let time = currentTime.add(new BN(duration.days(1))); + await I_ManualApprovalTransferManager.addManualApprovalMulti( + [account_investor2,account_investor3], + [account_investor3,account_investor4], + [web3.utils.toWei("2", "ether"), web3.utils.toWei("2", "ether")], + [time,currentTime.add(new BN(duration.days(1)))], + [web3.utils.fromAscii("DESCRIPTION_1"), web3.utils.fromAscii("DESCRIPTION_2")], + { + from: token_owner + } + ); - it("Revoke manual block and check transfer works", async () => { - await I_ManualApprovalTransferManager.revokeManualBlocking(account_investor1, account_investor2, { from: token_owner }); - await I_SecurityToken.transfer(account_investor2, new BN(web3.utils.toWei("1", "ether")), { from: account_investor1 }); - assert.equal((await I_SecurityToken.balanceOf(account_investor2)).toString(), new BN(web3.utils.toWei("2", "ether")).toString()); + assert.equal(await I_ManualApprovalTransferManager.getTotalApprovalsLength.call(), 2); + assert.equal((await I_ManualApprovalTransferManager.getActiveApprovalsToUser.call(account_investor3))[0].length , 2); + assert.equal((await I_ManualApprovalTransferManager.getActiveApprovalsToUser.call(account_investor3))[0][1], account_investor3); + assert.equal((await I_ManualApprovalTransferManager.getActiveApprovalsToUser.call(account_investor3))[1][0], account_investor3); + let approvalDetail = await I_ManualApprovalTransferManager.getApprovalDetails.call(account_investor2, account_investor3); + assert.equal(approvalDetail[0].toString(), time); + assert.equal(approvalDetail[1].toString(), web3.utils.toWei("2", "ether")); + assert.equal(web3.utils.toUtf8(approvalDetail[2]), "DESCRIPTION_1"); }); - it("Check manual block ignored after expiry", async () => { - await I_ManualApprovalTransferManager.addManualBlocking(account_investor1, account_investor2, currentTime.add(new BN(duration.days(1))), { - from: token_owner - }); + it("Should fail to revoke the multiple manual approvals -- because of bad owner", async() => { + await catchRevert( + I_ManualApprovalTransferManager.revokeManualApprovalMulti( + [account_investor2,account_investor3], + [account_investor3,account_investor4], + { + from: account_investor5 + } + ) + ); + }) - await catchRevert(I_SecurityToken.transfer(account_investor2, new BN(web3.utils.toWei("1", "ether")), { from: account_investor1 })); - await increaseTime(1 + 24 * 60 * 60); - await I_SecurityToken.transfer(account_investor2, new BN(web3.utils.toWei("1", "ether")), { from: account_investor1 }); + it("Should fail to revoke the multiple manual approvals -- because of input length mismatch", async() => { + await catchRevert( + I_ManualApprovalTransferManager.revokeManualApprovalMulti( + [account_investor2,account_investor3], + [account_investor3], + { + from: token_owner + } + ) + ); + }) + + it("Revoke multiple manual approvals", async () => { + await I_ManualApprovalTransferManager.revokeManualApprovalMulti( + [account_investor2,account_investor3], + [account_investor3,account_investor4], + { + from: token_owner + } + ); + assert.equal(await I_ManualApprovalTransferManager.getTotalApprovalsLength.call(), 0); + }); + + it("Add a manual approval for a 5th investor from issuance", async () => { + await I_ManualApprovalTransferManager.addManualApproval( + "0x0000000000000000000000000000000000000000", + account_investor5, + web3.utils.toWei("2", "ether"), + currentTime.add(new BN(duration.days(1))), + web3.utils.fromAscii("DESCRIPTION"), + { + from: token_owner + } + ); }); it("Should successfully attach the CountTransferManager with the security token (count of 1)", async () => { @@ -518,8 +788,8 @@ contract("ManualApprovalTransferManager", async (accounts) => { [1] ); - const tx = await I_SecurityToken.addModule(I_CountTransferManagerFactory.address, bytesCountTM, new BN(0), new BN(0), { from: token_owner }); - assert.equal(tx.logs[2].args._types[0].toNumber(), transferManagerKey, "CountTransferManager doesn't get deployed"); + const tx = await I_SecurityToken.addModule(I_CountTransferManagerFactory.address, bytesCountTM, 0, 0, { from: token_owner }); + assert.equal(tx.logs[2].args._types[0].toString(), transferManagerKey, "CountTransferManager doesn't get deployed"); let name = web3.utils.toUtf8(tx.logs[2].args._name); assert.equal(name, "CountTransferManager", "CountTransferManager module was not added"); I_CountTransferManager = await CountTransferManager.at(tx.logs[2].args._module); @@ -543,16 +813,16 @@ contract("ManualApprovalTransferManager", async (accounts) => { let name = web3.utils.toUtf8(await I_ManualApprovalTransferManagerFactory.getName.call()); assert.equal(name, "ManualApprovalTransferManager", "Wrong Module added"); let desc = await I_ManualApprovalTransferManagerFactory.description.call(); - assert.equal(desc, "Manage transfers using single approvals / blocking", "Wrong Module added"); + assert.equal(desc, "Manage transfers using single approvals", "Wrong Module added"); let title = await I_ManualApprovalTransferManagerFactory.title.call(); assert.equal(title, "Manual Approval Transfer Manager", "Wrong Module added"); let inst = await I_ManualApprovalTransferManagerFactory.getInstructions.call(); assert.equal( inst, - "Allows an issuer to set manual approvals or blocks for specific pairs of addresses and amounts. Init function takes no parameters.", + "Allows an issuer to set manual approvals for specific pairs of addresses and amounts. Init function takes no parameters.", "Wrong Module added" ); - assert.equal(await I_ManualApprovalTransferManagerFactory.version.call(), "2.0.1"); + assert.equal(await I_ManualApprovalTransferManagerFactory.version.call(), "2.1.0"); }); it("Should get the tags of the factory", async () => { diff --git a/test/k_module_registry.js b/test/k_module_registry.js index 1504d86b6..efba2e2f2 100644 --- a/test/k_module_registry.js +++ b/test/k_module_registry.js @@ -192,33 +192,33 @@ contract("ModuleRegistry", async (accounts) => { it("Should successfully update the registry contract addresses", async () => { await I_MRProxied.updateFromRegistry({ from: account_polymath }); assert.equal( - await I_MRProxied.getAddressValues.call(web3.utils.soliditySha3("securityTokenRegistry")), + await I_MRProxied.getAddressValue.call(web3.utils.soliditySha3("securityTokenRegistry")), I_SecurityTokenRegistryProxy.address ); assert.equal( - await I_MRProxied.getAddressValues.call(web3.utils.soliditySha3("featureRegistry")), + await I_MRProxied.getAddressValue.call(web3.utils.soliditySha3("featureRegistry")), I_FeatureRegistry.address ); - assert.equal(await I_MRProxied.getAddressValues.call(web3.utils.soliditySha3("polyToken")), I_PolyToken.address); + assert.equal(await I_MRProxied.getAddressValue.call(web3.utils.soliditySha3("polyToken")), I_PolyToken.address); }); }); describe("Test the state variables", async () => { it("Should be the right owner", async () => { - let _owner = await I_MRProxied.getAddressValues.call(web3.utils.soliditySha3("owner")); + let _owner = await I_MRProxied.getAddressValue.call(web3.utils.soliditySha3("owner")); assert.equal(_owner, account_polymath, "Owner should be the correct"); }); it("Should be the expected value of the paused and intialised variable", async () => { - let _paused = await I_MRProxied.getBoolValues.call(web3.utils.soliditySha3("paused")); + let _paused = await I_MRProxied.getBoolValue.call(web3.utils.soliditySha3("paused")); assert.isFalse(_paused, "Should be the false"); - let _intialised = await I_MRProxied.getBoolValues.call(web3.utils.soliditySha3("initialised")); + let _intialised = await I_MRProxied.getBoolValue.call(web3.utils.soliditySha3("initialised")); assert.isTrue(_intialised, "Values should be the true"); }); it("Should be the expected value of the polymath registry", async () => { - let _polymathRegistry = await I_MRProxied.getAddressValues.call(web3.utils.soliditySha3("polymathRegistry")); + let _polymathRegistry = await I_MRProxied.getAddressValue.call(web3.utils.soliditySha3("polymathRegistry")); assert.equal( _polymathRegistry, I_PolymathRegistry.address, @@ -481,10 +481,10 @@ contract("ModuleRegistry", async (accounts) => { // re-ordering assert.equal(sto3_end, sto3); // delete related data - assert.equal(await I_MRProxied.getUintValues.call(web3.utils.soliditySha3("registry", sto4)), 0); + assert.equal(await I_MRProxied.getUintValue.call(web3.utils.soliditySha3("registry", sto4)), 0); assert.equal(await I_MRProxied.getReputationByFactory.call(sto4), 0); assert.equal((await I_MRProxied.getModulesByType.call(3)).length, 3); - assert.equal(await I_MRProxied.getBoolValues.call(web3.utils.soliditySha3("verified", sto4)), false); + assert.equal(await I_MRProxied.getBoolValue.call(web3.utils.soliditySha3("verified", sto4)), false); await revertToSnapshot(snap); }); @@ -507,10 +507,10 @@ contract("ModuleRegistry", async (accounts) => { // re-ordering assert.equal(sto1_end, sto1); // delete related data - assert.equal(await I_MRProxied.getUintValues.call(web3.utils.soliditySha3("registry", sto2)), 0); + assert.equal(await I_MRProxied.getUintValue.call(web3.utils.soliditySha3("registry", sto2)), 0); assert.equal(await I_MRProxied.getReputationByFactory.call(sto2), 0); assert.equal((await I_MRProxied.getModulesByType.call(3)).length, 3); - assert.equal(await I_MRProxied.getBoolValues.call(web3.utils.soliditySha3("verified", sto2)), false); + assert.equal(await I_MRProxied.getBoolValue.call(web3.utils.soliditySha3("verified", sto2)), false); }); it("Should fail if module already removed", async () => { @@ -548,7 +548,7 @@ contract("ModuleRegistry", async (accounts) => { it("Should successfully pause the contract", async () => { await I_MRProxied.pause({ from: account_polymath }); - let status = await I_MRProxied.getBoolValues.call(web3.utils.soliditySha3("paused")); + let status = await I_MRProxied.getBoolValue.call(web3.utils.soliditySha3("paused")); assert.isOk(status); }); @@ -558,7 +558,7 @@ contract("ModuleRegistry", async (accounts) => { it("Should successfully unpause the contract", async () => { await I_MRProxied.unpause({ from: account_polymath }); - let status = await I_MRProxied.getBoolValues.call(web3.utils.soliditySha3("paused")); + let status = await I_MRProxied.getBoolValue.call(web3.utils.soliditySha3("paused")); assert.isNotOk(status); }); }); diff --git a/test/l_percentage_transfer_manager.js b/test/l_percentage_transfer_manager.js index f98e2c41f..b27f34427 100644 --- a/test/l_percentage_transfer_manager.js +++ b/test/l_percentage_transfer_manager.js @@ -132,7 +132,6 @@ contract("PercentageTransferManager", async (accounts) => { I_MRProxied, new BN(web3.utils.toWei("500", "ether")) ); - // Printing all the contract addresses console.log(` --------------------- Polymath Network Smart Contracts: --------------------- @@ -162,7 +161,7 @@ contract("PercentageTransferManager", async (accounts) => { it("Should generate the new security token with the same symbol as registered above", async () => { await I_PolyToken.approve(I_STRProxied.address, initRegFee, { from: token_owner }); - + let tx = await I_STRProxied.generateSecurityToken(name, symbol, tokenDetails, false, { from: token_owner }); // Verify the successful generation of the security token diff --git a/test/n_security_token_registry.js b/test/n_security_token_registry.js index 05955cb71..bc7a2731d 100644 --- a/test/n_security_token_registry.js +++ b/test/n_security_token_registry.js @@ -127,7 +127,7 @@ contract("SecurityTokenRegistry", async (accounts) => { console.log(I_SecurityTokenRegistry.address); I_SecurityTokenRegistry = await SecurityTokenRegistry.new({ from: account_polymath }); console.log(I_SecurityTokenRegistry.address); - + assert.notEqual( I_SecurityTokenRegistry.address.valueOf(), address_zero, @@ -227,7 +227,7 @@ contract("SecurityTokenRegistry", async (accounts) => { "tx-> revert because tickerRegFee is 0" ); }); - + it("Should successfully update the implementation address -- fail because owner address is 0x", async () => { let bytesProxy = encodeProxyCall(STRProxyParameters, [ I_PolymathRegistry.address, @@ -274,28 +274,28 @@ contract("SecurityTokenRegistry", async (accounts) => { describe(" Test cases of the registerTicker", async () => { it("verify the intial parameters", async () => { - let intialised = await I_STRProxied.getBoolValues.call(web3.utils.soliditySha3("initialised")); + let intialised = await I_STRProxied.getBoolValue.call(web3.utils.soliditySha3("initialised")); assert.isTrue(intialised, "Should be true"); - let expiry = await I_STRProxied.getUintValues.call(web3.utils.soliditySha3("expiryLimit")); + let expiry = await I_STRProxied.getUintValue.call(web3.utils.soliditySha3("expiryLimit")); assert.equal(expiry.toNumber(), 5184000, "Expiry limit should be equal to 60 days"); - let polytoken = await I_STRProxied.getAddressValues.call(web3.utils.soliditySha3("polyToken")); + let polytoken = await I_STRProxied.getAddressValue.call(web3.utils.soliditySha3("polyToken")); assert.equal(polytoken, I_PolyToken.address, "Should be the polytoken address"); - let stlaunchFee = await I_STRProxied.getUintValues.call(web3.utils.soliditySha3("stLaunchFee")); + let stlaunchFee = await I_STRProxied.getUintValue.call(web3.utils.soliditySha3("stLaunchFee")); assert.equal(stlaunchFee.toString(), initRegFee.toString(), "Should be provided reg fee"); - let tickerRegFee = await I_STRProxied.getUintValues.call(web3.utils.soliditySha3("tickerRegFee")); + let tickerRegFee = await I_STRProxied.getUintValue.call(web3.utils.soliditySha3("tickerRegFee")); assert.equal(tickerRegFee.toString(), tickerRegFee.toString(), "Should be provided reg fee"); - let polymathRegistry = await I_STRProxied.getAddressValues.call(web3.utils.soliditySha3("polymathRegistry")); + let polymathRegistry = await I_STRProxied.getAddressValue.call(web3.utils.soliditySha3("polymathRegistry")); assert.equal(polymathRegistry, I_PolymathRegistry.address, "Should be the address of the polymath registry"); - let getterContract = await I_STRProxied.getAddressValues.call(web3.utils.soliditySha3("STRGetter")); + let getterContract = await I_STRProxied.getAddressValue.call(web3.utils.soliditySha3("STRGetter")); assert.equal(getterContract, I_STRGetter.address, "Should be the address of the getter contract"); - let owner = await I_STRProxied.getAddressValues.call(web3.utils.soliditySha3("owner")); + let owner = await I_STRProxied.getAddressValue.call(web3.utils.soliditySha3("owner")); assert.equal(owner, account_polymath, "Should be the address of the registry owner"); }); @@ -417,7 +417,7 @@ contract("SecurityTokenRegistry", async (accounts) => { it("Should successfully set the expiry limit", async () => { await I_STRProxied.changeExpiryLimit(duration.days(10), { from: account_polymath }); assert.equal( - (await I_STRProxied.getUintValues.call(web3.utils.soliditySha3("expiryLimit"))).toNumber(), + (await I_STRProxied.getUintValue.call(web3.utils.soliditySha3("expiryLimit"))).toNumber(), duration.days(10), "Failed to change the expiry limit" ); @@ -501,7 +501,7 @@ contract("SecurityTokenRegistry", async (accounts) => { }); it("Should generate the new security token with the same symbol as registered above", async () => { - + let tx = await I_STRProxied.generateSecurityToken(name, symbol, tokenDetails, false, { from: token_owner }); // Verify the successful generation of the security token @@ -596,7 +596,7 @@ contract("SecurityTokenRegistry", async (accounts) => { it("Should generate the new security token with version 2", async () => { await I_PolyToken.approve(I_STRProxied.address, initRegFee, { from: token_owner }); - + let tx = await I_STRProxied.generateSecurityToken(name2, symbol2, tokenDetails, false, { from: token_owner }); // Verify the successful generation of the security token @@ -628,7 +628,7 @@ contract("SecurityTokenRegistry", async (accounts) => { it("Should upgrade the logic contract into the STRProxy", async () => { await I_SecurityTokenRegistryProxy.upgradeTo("1.1.0", I_SecurityTokenRegistryV2.address, { from: account_polymath }); I_STRProxied = await SecurityTokenRegistry.at(I_SecurityTokenRegistryProxy.address); - assert.isTrue(await I_STRProxied.getBoolValues.call(web3.utils.soliditySha3("paused")), "Paused value should be false"); + assert.isTrue(await I_STRProxied.getBoolValue.call(web3.utils.soliditySha3("paused")), "Paused value should be false"); }); it("Should check the old data persist or not", async () => { @@ -640,7 +640,7 @@ contract("SecurityTokenRegistry", async (accounts) => { it("Should unpause the logic contract", async () => { await I_STRProxied.unpause({ from: account_polymath }); - assert.isFalse(await I_STRProxied.getBoolValues.call(web3.utils.soliditySha3("paused")), "Paused value should be false"); + assert.isFalse(await I_STRProxied.getBoolValue.call(web3.utils.soliditySha3("paused")), "Paused value should be false"); }); }); @@ -895,7 +895,7 @@ contract("SecurityTokenRegistry", async (accounts) => { it("Should able to change the STLaunchFee", async () => { let tx = await I_STRProxied.changeSecurityLaunchFee(new BN(web3.utils.toWei("500")), { from: account_polymath }); assert.equal(tx.logs[0].args._newFee.toString(), new BN(web3.utils.toWei("500")).toString()); - let stLaunchFee = await I_STRProxied.getUintValues(web3.utils.soliditySha3("stLaunchFee")); + let stLaunchFee = await I_STRProxied.getUintValue(web3.utils.soliditySha3("stLaunchFee")); assert.equal(stLaunchFee.toString(), new BN(web3.utils.toWei("500")).toString()); }); }); @@ -918,7 +918,7 @@ contract("SecurityTokenRegistry", async (accounts) => { it("Should able to change the ExpiryLimit", async () => { let tx = await I_STRProxied.changeExpiryLimit(duration.days(20), { from: account_polymath }); assert.equal(tx.logs[0].args._newExpiry, duration.days(20)); - let expiry = await I_STRProxied.getUintValues(web3.utils.soliditySha3("expiryLimit")); + let expiry = await I_STRProxied.getUintValue(web3.utils.soliditySha3("expiryLimit")); assert.equal(expiry, duration.days(20)); }); }); @@ -941,7 +941,7 @@ contract("SecurityTokenRegistry", async (accounts) => { it("Should able to change the TickerRegFee", async () => { let tx = await I_STRProxied.changeTickerRegistrationFee(new BN(web3.utils.toWei("400")), { from: account_polymath }); assert.equal(tx.logs[0].args._newFee.toString(), new BN(web3.utils.toWei("400")).toString()); - let tickerRegFee = await I_STRProxied.getUintValues(web3.utils.soliditySha3("tickerRegFee")); + let tickerRegFee = await I_STRProxied.getUintValue(web3.utils.soliditySha3("tickerRegFee")); assert.equal(tickerRegFee.toString(), new BN(web3.utils.toWei("400")).toString()); }); @@ -978,6 +978,29 @@ contract("SecurityTokenRegistry", async (accounts) => { }); }); + describe("Test case for the update poly token", async () => { + it("Should change the polytoken address -- failed because of bad owner", async () => { + catchRevert( + I_STRProxied.updatePolyTokenAddress(dummy_token, { from: account_temp }), + "tx revert -> failed because of bad owner" + ); + }); + + it("Should change the polytoken address -- failed because of 0x address", async () => { + catchRevert( + I_STRProxied.updatePolyTokenAddress(address_zero, { from: account_polymath }), + "tx revert -> failed because 0x address" + ); + }); + + it("Should successfully change the polytoken address", async () => { + let _id = await takeSnapshot(); + await I_STRProxied.updatePolyTokenAddress(dummy_token, { from: account_polymath }); + assert.equal(await I_STRProxied.getAddressValue.call(web3.utils.soliditySha3("polyToken")), dummy_token); + await revertToSnapshot(_id); + }); + }); + describe("Test cases for getters", async () => { it("Should get the security token address", async () => { let address = await I_Getter.getSecurityTokenAddress.call(symbol); @@ -1117,7 +1140,7 @@ contract("SecurityTokenRegistry", async (accounts) => { it("Should successfully pause the contract", async () => { await I_STRProxied.pause({ from: account_polymath }); - let status = await I_STRProxied.getBoolValues.call(web3.utils.soliditySha3("paused")); + let status = await I_STRProxied.getBoolValue.call(web3.utils.soliditySha3("paused")); assert.isOk(status); }); @@ -1127,7 +1150,7 @@ contract("SecurityTokenRegistry", async (accounts) => { it("Should successfully unpause the contract", async () => { await I_STRProxied.unpause({ from: account_polymath }); - let status = await I_STRProxied.getBoolValues.call(web3.utils.soliditySha3("paused")); + let status = await I_STRProxied.getBoolValue.call(web3.utils.soliditySha3("paused")); assert.isNotOk(status); }); }); diff --git a/test/p_usd_tiered_sto.js b/test/p_usd_tiered_sto.js index 1ee447b7f..4661fb043 100644 --- a/test/p_usd_tiered_sto.js +++ b/test/p_usd_tiered_sto.js @@ -161,8 +161,8 @@ contract("USDTieredSTO", async (accounts) => { name: "_reserveWallet" }, { - type: "address", - name: "_usdToken" + type: "address[]", + name: "_usdTokens" } ] }; @@ -280,7 +280,7 @@ contract("USDTieredSTO", async (accounts) => { it("Should generate the new security token with the same symbol as registered above", async () => { await I_PolyToken.getTokens(REGFEE, ISSUER); await I_PolyToken.approve(I_STRProxied.address, REGFEE, { from: ISSUER }); - + let tx = await I_STRProxied.generateSecurityToken(NAME, SYMBOL, TOKENDETAILS, true, { from: ISSUER }); assert.equal(tx.logs[2].args._ticker, SYMBOL, "SecurityToken doesn't get deployed"); @@ -304,7 +304,7 @@ contract("USDTieredSTO", async (accounts) => { let stoId = 0; // No discount _startTime.push(new BN(currentTime).add(new BN(duration.days(2)))); - _endTime.push(new BN(_startTime[stoId]).add(new BN(currentTime).add(new BN(duration.days(100))))); + _endTime.push(_startTime[stoId].add(new BN(duration.days(100)))); _ratePerTier.push([new BN(10).mul(e16), new BN(15).mul(e16)]); // [ 0.10 USD/Token, 0.15 USD/Token ] _ratePerTierDiscountPoly.push([new BN(10).mul(e16), new BN(15).mul(e16)]); // [ 0.10 USD/Token, 0.15 USD/Token ] _tokensPerTierTotal.push([new BN(100000000).mul(new BN(e18)), new BN(200000000).mul(new BN(e18))]); // [ 100m Token, 200m Token ] @@ -314,7 +314,7 @@ contract("USDTieredSTO", async (accounts) => { _fundRaiseTypes.push([0, 1, 2]); _wallet.push(WALLET); _reserveWallet.push(RESERVEWALLET); - _usdToken.push(I_DaiToken.address); + _usdToken.push([I_DaiToken.address]); let config = [ _startTime[stoId], @@ -378,7 +378,7 @@ contract("USDTieredSTO", async (accounts) => { _reserveWallet[stoId], "Incorrect _reserveWallet in config" ); - assert.equal(await I_USDTieredSTO_Array[stoId].usdToken.call(), _usdToken[stoId], "Incorrect _usdToken in config"); + assert.equal((await I_USDTieredSTO_Array[stoId].getUsdTokens())[0], _usdToken[stoId][0], "Incorrect _usdToken in config"); assert.equal( await I_USDTieredSTO_Array[stoId].getNumberOfTiers(), _tokensPerTierTotal[stoId].length, @@ -503,7 +503,7 @@ contract("USDTieredSTO", async (accounts) => { _fundRaiseTypes.push([0, 1, 2]); _wallet.push(WALLET); _reserveWallet.push(RESERVEWALLET); - _usdToken.push(I_DaiToken.address); + _usdToken.push([I_DaiToken.address]); let config = [ _startTime[stoId], @@ -567,7 +567,7 @@ contract("USDTieredSTO", async (accounts) => { _reserveWallet[stoId], "Incorrect _reserveWallet in config" ); - assert.equal(await I_USDTieredSTO_Array[stoId].usdToken.call(), _usdToken[stoId], "Incorrect _usdToken in config"); + assert.equal((await I_USDTieredSTO_Array[stoId].getUsdTokens())[0], _usdToken[stoId][0], "Incorrect _usdToken in config"); assert.equal( await I_USDTieredSTO_Array[stoId].getNumberOfTiers(), _tokensPerTierTotal[stoId].length, @@ -589,7 +589,7 @@ contract("USDTieredSTO", async (accounts) => { _fundRaiseTypes.push([0, 1, 2]); _wallet.push(WALLET); _reserveWallet.push(RESERVEWALLET); - _usdToken.push(I_DaiToken.address); + _usdToken.push([I_DaiToken.address]); let config = [ _startTime[stoId], _endTime[stoId], @@ -626,7 +626,7 @@ contract("USDTieredSTO", async (accounts) => { _fundRaiseTypes.push([0, 1, 2]); _wallet.push(WALLET); _reserveWallet.push(RESERVEWALLET); - _usdToken.push(I_DaiToken.address); + _usdToken.push([I_DaiToken.address]); let config = [ _startTime[stoId], @@ -665,7 +665,7 @@ contract("USDTieredSTO", async (accounts) => { _fundRaiseTypes.push([0, 1, 2]); _wallet.push(WALLET); _reserveWallet.push(RESERVEWALLET); - _usdToken.push(I_DaiToken.address); + _usdToken.push([I_DaiToken.address]); let config = [ _startTime[stoId], @@ -683,11 +683,14 @@ contract("USDTieredSTO", async (accounts) => { ]; let bytesSTO = web3.eth.abi.encodeFunctionCall(functionSignature, config); - let tx = await I_SecurityToken.addModule(I_USDTieredSTOFactory.address, bytesSTO, new BN(0), new BN(0), { from: ISSUER, gasPrice: GAS_PRICE }); + let tx = await I_SecurityToken.addModule(I_USDTieredSTOFactory.address, bytesSTO, 0, 0, { from: ISSUER, gasPrice: GAS_PRICE }); console.log(" Gas addModule: ".grey + tx.receipt.gasUsed.toString().grey); assert.equal(tx.logs[2].args._types[0], STOKEY, "USDTieredSTO doesn't get deployed"); assert.equal(web3.utils.hexToString(tx.logs[2].args._name), "USDTieredSTO", "USDTieredSTOFactory module was not added"); I_USDTieredSTO_Array.push(await USDTieredSTO.at(tx.logs[2].args._module)); + // console.log(I_USDTieredSTO_Array[I_USDTieredSTO_Array.length - 1]); + let tokens = await I_USDTieredSTO_Array[I_USDTieredSTO_Array.length - 1].getUsdTokens.call(); + assert.equal(tokens[0], I_DaiToken.address, "USD Tokens should match"); }); it("Should fail because rates and tier array of different length", async () => { @@ -823,7 +826,8 @@ contract("USDTieredSTO", async (accounts) => { _minimumInvestmentUSD[stoId], _fundRaiseTypes[stoId], _wallet[stoId], - reserveWallet + reserveWallet, + _usdToken[stoId] ]; let bytesSTO = web3.eth.abi.encodeFunctionCall(functionSignature, config); @@ -944,11 +948,11 @@ contract("USDTieredSTO", async (accounts) => { await I_USDTieredSTO_Array[stoId].modifyTimes(new BN(tempTime1), new BN(tempTime2), { from: ISSUER }); assert.equal((await I_USDTieredSTO_Array[stoId].startTime.call()).toString(), tempTime1.toString(), "STO Configuration doesn't set as expected"); assert.equal((await I_USDTieredSTO_Array[stoId].endTime.call()).toString(), tempTime2.toString(), "STO Configuration doesn't set as expected"); - + console.log("HERE"); await I_USDTieredSTO_Array[stoId].modifyAddresses( "0x0000000000000000000000000400000000000000", "0x0000000000000000000003000000000000000000", - address_zero, + [accounts[3]], { from: ISSUER } ); assert.equal( @@ -961,7 +965,7 @@ contract("USDTieredSTO", async (accounts) => { "0x0000000000000000000003000000000000000000", "STO Configuration doesn't set as expected" ); - assert.equal(await I_USDTieredSTO_Array[stoId].usdToken.call(), address_zero, "STO Configuration doesn't set as expected"); + assert.equal((await I_USDTieredSTO_Array[stoId].getUsdTokens())[0], accounts[3], "STO Configuration doesn't set as expected"); }); it("Should fail to change config after endTime", async () => { @@ -984,20 +988,11 @@ contract("USDTieredSTO", async (accounts) => { ) ); - let tempTime1 = await latestTime(); + let tempTime1 = await latestTime() + duration.days(1); let tempTime2 = await latestTime() + duration.days(3); await catchRevert(I_USDTieredSTO_Array[stoId].modifyTimes(tempTime1, tempTime2, { from: ISSUER })); - await catchRevert( - I_USDTieredSTO_Array[stoId].modifyAddresses( - "0x0000000000000000000000000400000000000000", - "0x0000000000000000000003000000000000000000", - I_DaiToken.address, - { from: ISSUER } - ) - ); - await revertToSnapshot(snapId); }); }); @@ -1041,13 +1036,13 @@ contract("USDTieredSTO", async (accounts) => { // NONACCREDITED POLY await catchRevert(I_USDTieredSTO_Array[stoId].buyWithPOLY(NONACCREDITED1, investment_POLY, { from: NONACCREDITED1 })); // NONACCREDITED DAI - await catchRevert(I_USDTieredSTO_Array[stoId].buyWithUSD(NONACCREDITED1, investment_DAI, { from: NONACCREDITED1 })); + await catchRevert(I_USDTieredSTO_Array[stoId].buyWithUSD(NONACCREDITED1, investment_DAI, I_DaiToken.address, { from: NONACCREDITED1 })); // ACCREDITED ETH await catchRevert(I_USDTieredSTO_Array[stoId].buyWithETH(ACCREDITED1, { from: ACCREDITED1, value: investment_ETH })); // ACCREDITED POLY await catchRevert(I_USDTieredSTO_Array[stoId].buyWithPOLY(ACCREDITED1, investment_POLY, { from: ACCREDITED1 })); // ACCREDITED DAI - await catchRevert(I_USDTieredSTO_Array[stoId].buyWithUSD(ACCREDITED1, investment_DAI, { from: ACCREDITED1 })); + await catchRevert(I_USDTieredSTO_Array[stoId].buyWithUSD(ACCREDITED1, investment_DAI, I_DaiToken.address, { from: ACCREDITED1 })); await revertToSnapshot(snapId); }); @@ -1090,7 +1085,7 @@ contract("USDTieredSTO", async (accounts) => { await catchRevert(I_USDTieredSTO_Array[stoId].buyWithPOLY(NONACCREDITED1, investment_POLY, { from: NONACCREDITED1 })); // NONACCREDITED DAI - await catchRevert(I_USDTieredSTO_Array[stoId].buyWithUSD(NONACCREDITED1, investment_DAI, { from: NONACCREDITED1 })); + await catchRevert(I_USDTieredSTO_Array[stoId].buyWithUSD(NONACCREDITED1, investment_DAI, I_DaiToken.address, { from: NONACCREDITED1 })); // ACCREDITED ETH await catchRevert(I_USDTieredSTO_Array[stoId].buyWithETH(ACCREDITED1, { from: ACCREDITED1, value: investment_ETH })); @@ -1099,7 +1094,7 @@ contract("USDTieredSTO", async (accounts) => { await catchRevert(I_USDTieredSTO_Array[stoId].buyWithPOLY(ACCREDITED1, investment_POLY, { from: ACCREDITED1 })); // ACCREDITED DAI - await catchRevert(I_USDTieredSTO_Array[stoId].buyWithUSD(ACCREDITED1, investment_DAI, { from: ACCREDITED1 })); + await catchRevert(I_USDTieredSTO_Array[stoId].buyWithUSD(ACCREDITED1, investment_DAI, I_DaiToken.address, { from: ACCREDITED1 })); await revertToSnapshot(snapId); }); @@ -1146,7 +1141,7 @@ contract("USDTieredSTO", async (accounts) => { await catchRevert(I_USDTieredSTO_Array[stoId].buyWithPOLY(NONACCREDITED1, investment_POLY, { from: NONACCREDITED1 })); // NONACCREDITED DAI - await catchRevert(I_USDTieredSTO_Array[stoId].buyWithUSD(NONACCREDITED1, investment_DAI, { from: NONACCREDITED1 })); + await catchRevert(I_USDTieredSTO_Array[stoId].buyWithUSD(NONACCREDITED1, investment_DAI, I_DaiToken.address, { from: NONACCREDITED1 })); // ACCREDITED ETH await catchRevert(I_USDTieredSTO_Array[stoId].buyWithETH(ACCREDITED1, { from: ACCREDITED1, value: investment_ETH })); @@ -1155,7 +1150,7 @@ contract("USDTieredSTO", async (accounts) => { await catchRevert(I_USDTieredSTO_Array[stoId].buyWithPOLY(ACCREDITED1, investment_POLY, { from: ACCREDITED1 })); // ACCREDITED DAI - await catchRevert(I_USDTieredSTO_Array[stoId].buyWithUSD(ACCREDITED1, investment_DAI, { from: ACCREDITED1 })); + await catchRevert(I_USDTieredSTO_Array[stoId].buyWithUSD(ACCREDITED1, investment_DAI, I_DaiToken.address, { from: ACCREDITED1 })); await revertToSnapshot(snapId); }); @@ -1204,7 +1199,7 @@ contract("USDTieredSTO", async (accounts) => { await catchRevert(I_USDTieredSTO_Array[stoId].buyWithPOLY(NONACCREDITED1, investment_POLY, { from: NONACCREDITED1 })); // NONACCREDITED DAI - await catchRevert(I_USDTieredSTO_Array[stoId].buyWithUSD(NONACCREDITED1, investment_DAI, { from: NONACCREDITED1 })); + await catchRevert(I_USDTieredSTO_Array[stoId].buyWithUSD(NONACCREDITED1, investment_DAI, I_DaiToken.address, { from: NONACCREDITED1 })); // ACCREDITED ETH await catchRevert(I_USDTieredSTO_Array[stoId].buyWithETH(ACCREDITED1, { from: ACCREDITED1, value: investment_ETH })); @@ -1213,7 +1208,7 @@ contract("USDTieredSTO", async (accounts) => { await catchRevert(I_USDTieredSTO_Array[stoId].buyWithPOLY(ACCREDITED1, investment_POLY, { from: ACCREDITED1 })); // ACCREDITED DAI - await catchRevert(I_USDTieredSTO_Array[stoId].buyWithUSD(ACCREDITED1, investment_DAI, { from: ACCREDITED1 })); + await catchRevert(I_USDTieredSTO_Array[stoId].buyWithUSD(ACCREDITED1, investment_DAI, I_DaiToken.address, { from: ACCREDITED1 })); // Unpause the STO await I_USDTieredSTO_Array[stoId].unpause({ from: ISSUER }); @@ -1221,11 +1216,58 @@ contract("USDTieredSTO", async (accounts) => { await I_USDTieredSTO_Array[stoId].buyWithETH(NONACCREDITED1, { from: NONACCREDITED1, value: investment_ETH }); await I_USDTieredSTO_Array[stoId].buyWithPOLY(NONACCREDITED1, investment_POLY, { from: NONACCREDITED1 }); - await I_USDTieredSTO_Array[stoId].buyWithUSD(NONACCREDITED1, investment_DAI, { from: NONACCREDITED1 }); + await I_USDTieredSTO_Array[stoId].buyWithUSD(NONACCREDITED1, investment_DAI, I_DaiToken.address, { from: NONACCREDITED1 }); await I_USDTieredSTO_Array[stoId].buyWithETH(ACCREDITED1, { from: ACCREDITED1, value: investment_ETH }); await I_USDTieredSTO_Array[stoId].buyWithPOLY(ACCREDITED1, investment_POLY, { from: ACCREDITED1 }); - await I_USDTieredSTO_Array[stoId].buyWithUSD(ACCREDITED1, investment_DAI, { from: ACCREDITED1 }); + await I_USDTieredSTO_Array[stoId].buyWithUSD(ACCREDITED1, investment_DAI, I_DaiToken.address, { from: ACCREDITED1 }); + + await revertToSnapshot(snapId); + }); + + it("should allow changing stable coin address in middle of STO", async () => { + let stoId = 0; + let snapId = await takeSnapshot(); + + // Whitelist + let fromTime = new BN(await latestTime()); + let toTime = fromTime.add(new BN(duration.days(15))); + let expiryTime = toTime.add(new BN(duration.days(100))); + let whitelisted = true; + + await I_GeneralTransferManager.modifyWhitelist(ACCREDITED1, fromTime, toTime, expiryTime, whitelisted, { from: ISSUER }); + await I_GeneralTransferManager.modifyWhitelist(NONACCREDITED1, fromTime, toTime, expiryTime, whitelisted, { from: ISSUER }); + + // Advance time to after STO start + await increaseTime(duration.days(3)); + + // Set as accredited + await I_USDTieredSTO_Array[stoId].changeAccredited([ACCREDITED1], [true], { from: ISSUER }); + + // Prep for investments + let investment_DAI = web3.utils.toWei("500", "ether"); // Invest 10000 POLY + await I_DaiToken.getTokens(investment_DAI, NONACCREDITED1); + await I_DaiToken.approve(I_USDTieredSTO_Array[stoId].address, investment_DAI, { from: NONACCREDITED1 }); + await I_DaiToken.getTokens(investment_DAI, ACCREDITED1); + await I_DaiToken.approve(I_USDTieredSTO_Array[stoId].address, investment_DAI, { from: ACCREDITED1 }); + + // Make sure buying works before changing + await I_USDTieredSTO_Array[stoId].buyWithUSD(NONACCREDITED1, investment_DAI, I_DaiToken.address, { from: NONACCREDITED1 }); + + // Change Stable coin address + await I_USDTieredSTO_Array[stoId].modifyAddresses(WALLET, RESERVEWALLET, [I_PolyToken.address], { from: ISSUER }); + + // NONACCREDITED DAI + await catchRevert(I_USDTieredSTO_Array[stoId].buyWithUSD(NONACCREDITED1, investment_DAI, I_DaiToken.address, { from: NONACCREDITED1 })); + + // ACCREDITED DAI + await catchRevert(I_USDTieredSTO_Array[stoId].buyWithUSD(ACCREDITED1, investment_DAI, I_DaiToken.address, { from: ACCREDITED1 })); + + // Revert stable coin address + await I_USDTieredSTO_Array[stoId].modifyAddresses(WALLET, RESERVEWALLET, [I_DaiToken.address], { from: ISSUER }); + + // Make sure buying works again + await I_USDTieredSTO_Array[stoId].buyWithUSD(ACCREDITED1, investment_DAI, I_DaiToken.address, { from: ACCREDITED1 }); await revertToSnapshot(snapId); }); @@ -1271,7 +1313,7 @@ contract("USDTieredSTO", async (accounts) => { await catchRevert(I_USDTieredSTO_Array[stoId].buyWithPOLY(NONACCREDITED1, investment_POLY, { from: NONACCREDITED1 })); // NONACCREDITED DAI - await catchRevert(I_USDTieredSTO_Array[stoId].buyWithUSD(NONACCREDITED1, investment_DAI, { from: NONACCREDITED1 })); + await catchRevert(I_USDTieredSTO_Array[stoId].buyWithUSD(NONACCREDITED1, investment_DAI, I_DaiToken.address, { from: NONACCREDITED1 })); // ACCREDITED ETH await catchRevert(I_USDTieredSTO_Array[stoId].buyWithETH(ACCREDITED1, { from: ACCREDITED1, value: investment_ETH })); @@ -1280,7 +1322,7 @@ contract("USDTieredSTO", async (accounts) => { await catchRevert(I_USDTieredSTO_Array[stoId].buyWithPOLY(ACCREDITED1, investment_POLY, { from: ACCREDITED1 })); // ACCREDITED DAI - await catchRevert(I_USDTieredSTO_Array[stoId].buyWithUSD(ACCREDITED1, investment_DAI, { from: ACCREDITED1 })); + await catchRevert(I_USDTieredSTO_Array[stoId].buyWithUSD(ACCREDITED1, investment_DAI, I_DaiToken.address, { from: ACCREDITED1 })); await revertToSnapshot(snapId); }); @@ -1333,7 +1375,7 @@ contract("USDTieredSTO", async (accounts) => { await catchRevert(I_USDTieredSTO_Array[stoId].buyWithPOLY(NONACCREDITED1, investment_POLY, { from: NONACCREDITED1 })); // NONACCREDITED DAI - await catchRevert(I_USDTieredSTO_Array[stoId].buyWithUSD(NONACCREDITED1, investment_DAI, { from: NONACCREDITED1 })); + await catchRevert(I_USDTieredSTO_Array[stoId].buyWithUSD(NONACCREDITED1, investment_DAI, I_DaiToken.address, { from: NONACCREDITED1 })); // ACCREDITED ETH await catchRevert(I_USDTieredSTO_Array[stoId].buyWithETH(ACCREDITED1, { from: ACCREDITED1, value: investment_ETH })); @@ -1342,7 +1384,7 @@ contract("USDTieredSTO", async (accounts) => { await catchRevert(I_USDTieredSTO_Array[stoId].buyWithPOLY(ACCREDITED1, investment_POLY, { from: ACCREDITED1 })); // ACCREDITED DAI - await catchRevert(I_USDTieredSTO_Array[stoId].buyWithUSD(ACCREDITED1, investment_DAI, { from: ACCREDITED1 })); + await catchRevert(I_USDTieredSTO_Array[stoId].buyWithUSD(ACCREDITED1, investment_DAI, I_DaiToken.address, { from: ACCREDITED1 })); await revertToSnapshot(snapId); }); @@ -1375,20 +1417,31 @@ contract("USDTieredSTO", async (accounts) => { it("should successfully modify accredited addresses for first STO", async () => { let stoId = 0; - - let status1 = await I_USDTieredSTO_Array[stoId].accredited.call(NONACCREDITED1); - assert.equal(status1, false, "Initial accreditation is set to true"); + let investorStatus = await I_USDTieredSTO_Array[stoId].investors.call(NONACCREDITED1); + let status1 = investorStatus[0].toNumber(); + assert.equal(status1, 0, "Initial accreditation is set to true"); await I_USDTieredSTO_Array[stoId].changeAccredited([NONACCREDITED1], [true], { from: ISSUER }); - let status2 = await I_USDTieredSTO_Array[stoId].accredited.call(NONACCREDITED1); - assert.equal(status2, true, "Failed to set single address"); + investorStatus = await I_USDTieredSTO_Array[stoId].investors.call(NONACCREDITED1); + let status2 = investorStatus[0].toNumber(); + assert.equal(status2, 1, "Failed to set single address"); await I_USDTieredSTO_Array[stoId].changeAccredited([NONACCREDITED1, ACCREDITED1], [false, true], { from: ISSUER }); - let status3 = await I_USDTieredSTO_Array[stoId].accredited.call(NONACCREDITED1); - assert.equal(status3, false, "Failed to set multiple addresses"); - let status4 = await I_USDTieredSTO_Array[stoId].accredited.call(ACCREDITED1); - assert.equal(status4, true, "Failed to set multiple addresses"); - + investorStatus = await I_USDTieredSTO_Array[stoId].investors.call(NONACCREDITED1); + let status3 = investorStatus[0].toNumber(); + assert.equal(status3, 0, "Failed to set multiple addresses"); + investorStatus = await I_USDTieredSTO_Array[stoId].investors.call(ACCREDITED1); + let status4 = investorStatus[0].toNumber(); + assert.equal(status4, 1, "Failed to set multiple addresses"); + + let totalStatus = await I_USDTieredSTO_Array[stoId].getAccreditedData.call(); + console.log(totalStatus); + assert.equal(totalStatus[0][0], NONACCREDITED1, "Account match"); + assert.equal(totalStatus[0][1], ACCREDITED1, "Account match"); + assert.equal(totalStatus[1][0], false, "Account match"); + assert.equal(totalStatus[1][1], true, "Account match"); + assert.equal(totalStatus[2][0].toNumber(), 0, "override match"); + assert.equal(totalStatus[2][1].toNumber(), 0, "override match"); await catchRevert(I_USDTieredSTO_Array[stoId].changeAccredited([NONACCREDITED1, ACCREDITED1], [true], { from: ISSUER })); }); @@ -1396,10 +1449,12 @@ contract("USDTieredSTO", async (accounts) => { let stoId = 1; await I_USDTieredSTO_Array[stoId].changeAccredited([NONACCREDITED1, ACCREDITED1], [false, true], { from: ISSUER }); - let status1 = await I_USDTieredSTO_Array[stoId].accredited.call(NONACCREDITED1); - let status2 = await I_USDTieredSTO_Array[stoId].accredited.call(ACCREDITED1); - assert.equal(status1, false, "Failed to set multiple address"); - assert.equal(status2, true, "Failed to set multiple address"); + let investorStatus = await I_USDTieredSTO_Array[stoId].investors.call(NONACCREDITED1); + let status1 = investorStatus[0].toNumber(); + investorStatus = await I_USDTieredSTO_Array[stoId].investors.call(ACCREDITED1); + let status2 = investorStatus[0].toNumber(); + assert.equal(status1, 0, "Failed to set multiple address"); + assert.equal(status2, 1, "Failed to set multiple address"); }); }); @@ -1732,7 +1787,7 @@ contract("USDTieredSTO", async (accounts) => { let init_WalletDAIBal = await I_DaiToken.balanceOf(WALLET); // Buy With DAI - let tx2 = await I_USDTieredSTO_Array[stoId].buyWithUSD(NONACCREDITED1, investment_DAI, { + let tx2 = await I_USDTieredSTO_Array[stoId].buyWithUSD(NONACCREDITED1, investment_DAI, I_DaiToken.address, { from: NONACCREDITED1, gasPrice: GAS_PRICE }); @@ -1796,6 +1851,11 @@ contract("USDTieredSTO", async (accounts) => { init_WalletDAIBal.add(investment_DAI).toString(), "Wallet DAI Balance not changed as expected" ); + assert.equal( + (await I_USDTieredSTO_Array[stoId].stableCoinsRaised.call(I_DaiToken.address)).toString(), + investment_DAI.toString(), + "DAI Raised not changed as expected" + ); }); it("should successfully buy using fallback at tier 0 for ACCREDITED1", async () => { @@ -2103,14 +2163,24 @@ contract("USDTieredSTO", async (accounts) => { await I_USDTieredSTO_Array[stoId].changeNonAccreditedLimit([NONACCREDITED1], [_nonAccreditedLimitUSD[stoId].div(new BN(2))], { from: ISSUER }); - console.log("Current limit: " + (await I_USDTieredSTO_Array[stoId].nonAccreditedLimitUSDOverride(NONACCREDITED1)).toString()); + let investorStatus = await I_USDTieredSTO_Array[stoId].investors.call(NONACCREDITED1); + console.log("Current limit: " + investorStatus[2].toString()); + let totalStatus = await I_USDTieredSTO_Array[stoId].getAccreditedData.call(); + + assert.equal(totalStatus[0][0], NONACCREDITED1, "Account match"); + assert.equal(totalStatus[0][1], ACCREDITED1, "Account match"); + assert.equal(totalStatus[1][0], false, "Account match"); + assert.equal(totalStatus[1][1], true, "Account match"); + assert.equal(totalStatus[2][0].toString(), _nonAccreditedLimitUSD[stoId].div(new BN(2)), "override match"); + assert.equal(totalStatus[2][1].toString(), 0, "override match"); }); it("should successfully buy a partial amount and refund balance when reaching NONACCREDITED cap", async () => { let stoId = 0; let tierId = 0; - let investment_USD = await I_USDTieredSTO_Array[stoId].nonAccreditedLimitUSDOverride(NONACCREDITED1); //_nonAccreditedLimitUSD[stoId]; + let investorStatus = await I_USDTieredSTO_Array[stoId].investors.call(NONACCREDITED1); + let investment_USD = investorStatus[2];//await I_USDTieredSTO_Array[stoId].nonAccreditedLimitUSDOverride(NONACCREDITED1); //_nonAccreditedLimitUSD[stoId]; let investment_Token = await convert(stoId, tierId, false, "USD", "TOKEN", investment_USD); let investment_ETH = await convert(stoId, tierId, false, "USD", "ETH", investment_USD); let investment_POLY = await convert(stoId, tierId, false, "USD", "POLY", investment_USD); @@ -2723,7 +2793,7 @@ contract("USDTieredSTO", async (accounts) => { let init_WalletPOLYBal = await I_PolyToken.balanceOf(WALLET); let init_WalletDAIBal = await I_DaiToken.balanceOf(WALLET); - let tx2 = await I_USDTieredSTO_Array[stoId].buyWithUSD(ACCREDITED1, investment_DAI, { from: ACCREDITED1, gasPrice: GAS_PRICE }); + let tx2 = await I_USDTieredSTO_Array[stoId].buyWithUSD(ACCREDITED1, investment_DAI, I_DaiToken.address, { from: ACCREDITED1, gasPrice: GAS_PRICE }); let gasCost2 = new BN(GAS_PRICE).mul(new BN(tx2.receipt.gasUsed)); console.log(" Gas buyWithUSD: ".grey + new BN(tx2.receipt.gasUsed).toString().grey); @@ -2962,7 +3032,7 @@ contract("USDTieredSTO", async (accounts) => { // Buy with DAI NONACCREDITED await catchRevert( - I_USDTieredSTO_Array[stoId].buyWithUSD(NONACCREDITED1, investment_DAI, { from: NONACCREDITED1, gasPrice: GAS_PRICE }) + I_USDTieredSTO_Array[stoId].buyWithUSD(NONACCREDITED1, investment_DAI, I_DaiToken.address, { from: NONACCREDITED1, gasPrice: GAS_PRICE }) ); // Buy with ETH ACCREDITED @@ -2980,7 +3050,7 @@ contract("USDTieredSTO", async (accounts) => { // Buy with DAI ACCREDITED await catchRevert( - I_USDTieredSTO_Array[stoId].buyWithUSD(ACCREDITED1, investment_DAI, { from: ACCREDITED1, gasPrice: GAS_PRICE }) + I_USDTieredSTO_Array[stoId].buyWithUSD(ACCREDITED1, investment_DAI, I_DaiToken.address, { from: ACCREDITED1, gasPrice: GAS_PRICE }) ); }); @@ -3798,7 +3868,7 @@ contract("USDTieredSTO", async (accounts) => { let init_WalletETHBal = new BN(await web3.eth.getBalance(WALLET)); let init_WalletPOLYBal = await I_PolyToken.balanceOf(WALLET); - let tokensToMint = (await I_USDTieredSTO_Array[stoId].buyTokensView(ACCREDITED1, investment_POLY, POLY))[2]; + let tokensToMint = (await I_USDTieredSTO_Array[stoId].buyWithPOLY.call(ACCREDITED1, investment_POLY, {from: ACCREDITED1}))[2]; // Buy With POLY let tx2 = await I_USDTieredSTO_Array[stoId].buyWithPOLY(ACCREDITED1, investment_POLY, { diff --git a/test/q_usd_tiered_sto_sim.js b/test/q_usd_tiered_sto_sim.js index 2e5fd4e5f..e4f9db8e1 100644 --- a/test/q_usd_tiered_sto_sim.js +++ b/test/q_usd_tiered_sto_sim.js @@ -90,7 +90,7 @@ contract("USDTieredSTO Sim", async (accounts) => { let _wallet = []; let _reserveWallet = []; let _usdToken = []; - + const address_zero = "0x0000000000000000000000000000000000000000"; const one_address = "0x0000000000000000000000000000000000000001"; @@ -157,8 +157,8 @@ contract("USDTieredSTO Sim", async (accounts) => { name: "_reserveWallet" }, { - type: "address", - name: "_usdToken" + type: "address[]", + name: "_usdTokens" } ] }; @@ -256,7 +256,7 @@ contract("USDTieredSTO Sim", async (accounts) => { it("Should generate the new security token with the same symbol as registered above", async () => { await I_PolyToken.getTokens(REGFEE, ISSUER); await I_PolyToken.approve(I_STRProxied.address, REGFEE, { from: ISSUER }); - + let tx = await I_STRProxied.generateSecurityToken(NAME, SYMBOL, TOKENDETAILS, true, { from: ISSUER }); assert.equal(tx.logs[2].args._ticker, SYMBOL, "SecurityToken doesn't get deployed"); @@ -302,7 +302,7 @@ contract("USDTieredSTO Sim", async (accounts) => { _fundRaiseTypes[stoId], _wallet[stoId], _reserveWallet[stoId], - _usdToken[stoId] + [_usdToken[stoId]] ]; let bytesSTO = web3.eth.abi.encodeFunctionCall(functionSignature, config); @@ -608,7 +608,7 @@ contract("USDTieredSTO Sim", async (accounts) => { await I_DaiToken.getTokens(investment_DAI, _investor); await I_DaiToken.approve(I_USDTieredSTO_Array[stoId].address, investment_DAI, { from: _investor }); await catchRevert( - I_USDTieredSTO_Array[stoId].buyWithUSD(_investor, investment_DAI, { from: _investor, gasPrice: GAS_PRICE }) + I_USDTieredSTO_Array[stoId].buyWithUSD(_investor, investment_DAI, I_DaiToken.address, { from: _investor, gasPrice: GAS_PRICE }) ); } else await catchRevert( @@ -691,7 +691,7 @@ contract("USDTieredSTO Sim", async (accounts) => { .yellow ); } else if (isDai && investment_DAI.gt(new BN(10))) { - tx = await I_USDTieredSTO_Array[stoId].buyWithUSD(_investor, investment_DAI, { from: _investor, gasPrice: GAS_PRICE }); + tx = await I_USDTieredSTO_Array[stoId].buyWithUSD(_investor, investment_DAI, I_DaiToken.address, { from: _investor, gasPrice: GAS_PRICE }); gasCost = new BN(GAS_PRICE).mul(new BN(tx.receipt.gasUsed)); console.log( `buyWithUSD: ${investment_Token.div(e18)} tokens for ${investment_DAI.div(e18)} DAI by ${_investor}` @@ -731,43 +731,43 @@ contract("USDTieredSTO Sim", async (accounts) => { // console.log('final_TokenSupply: '+final_TokenSupply.div(10**18).toNumber()); if (isPoly) { - assertIsNear(final_TokenSupply, init_TokenSupply.add(investment_Token), "Token Supply not changed as expected" ); - assertIsNear(final_InvestorTokenBal, init_InvestorTokenBal.add(investment_Token), "Investor Token Balance not changed as expected" ); - assertIsNear(final_InvestorETHBal, init_InvestorETHBal.sub(gasCost), "Investor ETH Balance not changed as expected" ); - assertIsNear(final_InvestorPOLYBal, init_InvestorPOLYBal.sub(investment_POLY), "Investor POLY Balance not changed as expected" ); - assertIsNear(final_STOTokenSold, init_STOTokenSold.add(investment_Token), "STO Token Sold not changed as expected" ); - assertIsNear(final_STOETHBal, init_STOETHBal, "STO ETH Balance not changed as expected" ); - assertIsNear(final_STOPOLYBal, init_STOPOLYBal, "STO POLY Balance not changed as expected" ); - assertIsNear(final_RaisedUSD, init_RaisedUSD.add(investment_USD), "Raised USD not changed as expected" ); - assertIsNear(final_RaisedETH, init_RaisedETH, "Raised ETH not changed as expected"); - assertIsNear(final_RaisedPOLY, init_RaisedPOLY.add(investment_POLY), "Raised POLY not changed as expected" ); - assertIsNear(final_WalletETHBal, init_WalletETHBal, "Wallet ETH Balance not changed as expected" ); - assertIsNear(final_WalletPOLYBal, init_WalletPOLYBal.add(investment_POLY), "Wallet POLY Balance not changed as expected" ); - } else if (isDai) { - assertIsNear(final_TokenSupply, init_TokenSupply.add(investment_Token), "Token Supply not changed as expected" ); - assertIsNear(final_InvestorTokenBal, init_InvestorTokenBal.add(investment_Token), "Investor Token Balance not changed as expected" ); - assertIsNear(final_InvestorETHBal, init_InvestorETHBal.sub(gasCost), "Investor ETH Balance not changed as expected" ); - assertIsNear(final_InvestorDAIBal, init_InvestorDAIBal.sub(investment_DAI), "Investor DAI Balance not changed as expected" ); - assertIsNear(final_STOTokenSold, init_STOTokenSold.add(investment_Token), "STO Token Sold not changed as expected" ); - assertIsNear(final_STOETHBal, init_STOETHBal, "STO ETH Balance not changed as expected" ); - assertIsNear(final_STODAIBal, init_STODAIBal, "STO DAI Balance not changed as expected" ); - assertIsNear(final_RaisedUSD, init_RaisedUSD.add(investment_USD), "Raised USD not changed as expected" ); - assertIsNear(final_RaisedETH, init_RaisedETH, "Raised ETH not changed as expected"); - assertIsNear(final_RaisedDAI, init_RaisedDAI.add(investment_DAI), "Raised DAI not changed as expected" ); - assertIsNear(final_WalletETHBal, init_WalletETHBal, "Wallet ETH Balance not changed as expected" ); - assertIsNear(final_WalletDAIBal, init_WalletDAIBal.add(investment_DAI), "Wallet DAI Balance not changed as expected" ); - } else { - assertIsNear(final_TokenSupply, init_TokenSupply.add(investment_Token), "Token Supply not changed as expected" ); - assertIsNear(final_InvestorTokenBal, init_InvestorTokenBal.add(investment_Token), "Investor Token Balance not changed as expected" ); - assertIsNear(final_InvestorETHBal, init_InvestorETHBal .sub(gasCost) .sub(investment_ETH) , "Investor ETH Balance not changed as expected" ); - assertIsNear(final_InvestorPOLYBal, init_InvestorPOLYBal, "Investor POLY Balance not changed as expected" ); - assertIsNear(final_STOTokenSold, init_STOTokenSold.add(investment_Token), "STO Token Sold not changed as expected" ); - assertIsNear(final_STOETHBal, init_STOETHBal, "STO ETH Balance not changed as expected" ); - assertIsNear(final_STOPOLYBal, init_STOPOLYBal, "STO POLY Balance not changed as expected" ); - assertIsNear(final_RaisedUSD, init_RaisedUSD.add(investment_USD), "Raised USD not changed as expected" ); - assertIsNear(final_RaisedETH, init_RaisedETH.add(investment_ETH), "Raised ETH not changed as expected" ); - assertIsNear(final_RaisedPOLY, init_RaisedPOLY, "Raised POLY not changed as expected" ); - assertIsNear(final_WalletETHBal, init_WalletETHBal.add(investment_ETH), "Wallet ETH Balance not changed as expected" ); + assertIsNear(final_TokenSupply, init_TokenSupply.add(investment_Token), "Token Supply not changed as expected" ); + assertIsNear(final_InvestorTokenBal, init_InvestorTokenBal.add(investment_Token), "Investor Token Balance not changed as expected" ); + assertIsNear(final_InvestorETHBal, init_InvestorETHBal.sub(gasCost), "Investor ETH Balance not changed as expected" ); + assertIsNear(final_InvestorPOLYBal, init_InvestorPOLYBal.sub(investment_POLY), "Investor POLY Balance not changed as expected" ); + assertIsNear(final_STOTokenSold, init_STOTokenSold.add(investment_Token), "STO Token Sold not changed as expected" ); + assertIsNear(final_STOETHBal, init_STOETHBal, "STO ETH Balance not changed as expected" ); + assertIsNear(final_STOPOLYBal, init_STOPOLYBal, "STO POLY Balance not changed as expected" ); + assertIsNear(final_RaisedUSD, init_RaisedUSD.add(investment_USD), "Raised USD not changed as expected" ); + assertIsNear(final_RaisedETH, init_RaisedETH, "Raised ETH not changed as expected"); + assertIsNear(final_RaisedPOLY, init_RaisedPOLY.add(investment_POLY), "Raised POLY not changed as expected" ); + assertIsNear(final_WalletETHBal, init_WalletETHBal, "Wallet ETH Balance not changed as expected" ); + assertIsNear(final_WalletPOLYBal, init_WalletPOLYBal.add(investment_POLY), "Wallet POLY Balance not changed as expected" ); + } else if (isDai) { + assertIsNear(final_TokenSupply, init_TokenSupply.add(investment_Token), "Token Supply not changed as expected" ); + assertIsNear(final_InvestorTokenBal, init_InvestorTokenBal.add(investment_Token), "Investor Token Balance not changed as expected" ); + assertIsNear(final_InvestorETHBal, init_InvestorETHBal.sub(gasCost), "Investor ETH Balance not changed as expected" ); + assertIsNear(final_InvestorDAIBal, init_InvestorDAIBal.sub(investment_DAI), "Investor DAI Balance not changed as expected" ); + assertIsNear(final_STOTokenSold, init_STOTokenSold.add(investment_Token), "STO Token Sold not changed as expected" ); + assertIsNear(final_STOETHBal, init_STOETHBal, "STO ETH Balance not changed as expected" ); + assertIsNear(final_STODAIBal, init_STODAIBal, "STO DAI Balance not changed as expected" ); + assertIsNear(final_RaisedUSD, init_RaisedUSD.add(investment_USD), "Raised USD not changed as expected" ); + assertIsNear(final_RaisedETH, init_RaisedETH, "Raised ETH not changed as expected"); + assertIsNear(final_RaisedDAI, init_RaisedDAI.add(investment_DAI), "Raised DAI not changed as expected" ); + assertIsNear(final_WalletETHBal, init_WalletETHBal, "Wallet ETH Balance not changed as expected" ); + assertIsNear(final_WalletDAIBal, init_WalletDAIBal.add(investment_DAI), "Wallet DAI Balance not changed as expected" ); + } else { + assertIsNear(final_TokenSupply, init_TokenSupply.add(investment_Token), "Token Supply not changed as expected" ); + assertIsNear(final_InvestorTokenBal, init_InvestorTokenBal.add(investment_Token), "Investor Token Balance not changed as expected" ); + assertIsNear(final_InvestorETHBal, init_InvestorETHBal .sub(gasCost) .sub(investment_ETH) , "Investor ETH Balance not changed as expected" ); + assertIsNear(final_InvestorPOLYBal, init_InvestorPOLYBal, "Investor POLY Balance not changed as expected" ); + assertIsNear(final_STOTokenSold, init_STOTokenSold.add(investment_Token), "STO Token Sold not changed as expected" ); + assertIsNear(final_STOETHBal, init_STOETHBal, "STO ETH Balance not changed as expected" ); + assertIsNear(final_STOPOLYBal, init_STOPOLYBal, "STO POLY Balance not changed as expected" ); + assertIsNear(final_RaisedUSD, init_RaisedUSD.add(investment_USD), "Raised USD not changed as expected" ); + assertIsNear(final_RaisedETH, init_RaisedETH.add(investment_ETH), "Raised ETH not changed as expected" ); + assertIsNear(final_RaisedPOLY, init_RaisedPOLY, "Raised POLY not changed as expected" ); + assertIsNear(final_WalletETHBal, init_WalletETHBal.add(investment_ETH), "Wallet ETH Balance not changed as expected" ); assertIsNear(final_WalletPOLYBal, init_WalletPOLYBal, "Wallet POLY Balance not changed as expected" ); } } diff --git a/test/t_security_token_registry_proxy.js b/test/t_security_token_registry_proxy.js index bd4161b2f..0854d94e1 100644 --- a/test/t_security_token_registry_proxy.js +++ b/test/t_security_token_registry_proxy.js @@ -49,7 +49,7 @@ contract("SecurityTokenRegistryProxy", async (accounts) => { const decimals = 18; const transferManagerKey = 2; - + const address_zero = "0x0000000000000000000000000000000000000000"; const one_address = "0x0000000000000000000000000000000000000001"; const STRProxyParameters = ["address", "address", "uint256", "uint256", "address", "address"]; @@ -134,12 +134,12 @@ contract("SecurityTokenRegistryProxy", async (accounts) => { it("Verify the initialize data", async () => { assert.equal( - (await I_STRProxied.getUintValues.call(web3.utils.soliditySha3("expiryLimit"))).toNumber(), + (await I_STRProxied.getUintValue.call(web3.utils.soliditySha3("expiryLimit"))).toNumber(), 60 * 24 * 60 * 60, "Should equal to 60 days" ); assert.equal( - await I_STRProxied.getUintValues.call(web3.utils.soliditySha3("tickerRegFee")), + await I_STRProxied.getUintValue.call(web3.utils.soliditySha3("tickerRegFee")), web3.utils.toWei("250") ); }); @@ -156,7 +156,7 @@ contract("SecurityTokenRegistryProxy", async (accounts) => { it("Should generate the new security token with the same symbol as registered above", async () => { await I_PolyToken.approve(I_STRProxied.address, initRegFee, { from: token_owner }); - + let tx = await I_STRProxied.generateSecurityToken(name, symbol, tokenDetails, false, { from: token_owner }); // Verify the successful generation of the security token diff --git a/test/u_module_registry_proxy.js b/test/u_module_registry_proxy.js index 19ddab75b..3c52dc693 100644 --- a/test/u_module_registry_proxy.js +++ b/test/u_module_registry_proxy.js @@ -129,12 +129,12 @@ contract("ModuleRegistryProxy", async (accounts) => { // STEP 4: Deploy the GeneralTransferManagerFactory let I_GeneralTransferManagerLogic = await GeneralTransferManagerLogic.new( - "0x0000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000", + address_zero, + address_zero, { from: account_polymath } ); - I_GeneralTransferManagerFactory = await GeneralTransferManagerFactory.new(0, new BN(0), new BN(0), I_GeneralTransferManagerLogic.address, { + I_GeneralTransferManagerFactory = await GeneralTransferManagerFactory.new(new BN(0), new BN(0), new BN(0), I_GeneralTransferManagerLogic.address, { from: account_polymath }); @@ -158,11 +158,11 @@ contract("ModuleRegistryProxy", async (accounts) => { it("Verify the initialize data", async () => { assert.equal( - await I_MRProxied.getAddressValues.call(web3.utils.soliditySha3("owner")), + await I_MRProxied.getAddressValue.call(web3.utils.soliditySha3("owner")), account_polymath, "Should equal to right address" ); - assert.equal(await I_MRProxied.getAddressValues.call(web3.utils.soliditySha3("polymathRegistry")), I_PolymathRegistry.address); + assert.equal(await I_MRProxied.getAddressValue.call(web3.utils.soliditySha3("polymathRegistry")), I_PolymathRegistry.address); }); }); diff --git a/test/w_lockup_transfer_manager.js b/test/w_lockup_transfer_manager.js new file mode 100644 index 000000000..0be2bfe04 --- /dev/null +++ b/test/w_lockup_transfer_manager.js @@ -0,0 +1,966 @@ +import latestTime from './helpers/latestTime'; +import { duration, promisifyLogWatch, latestBlock } from './helpers/utils'; +import takeSnapshot, { increaseTime, revertToSnapshot } from './helpers/time'; +import { encodeProxyCall } from './helpers/encodeCall'; +import { setUpPolymathNetwork, deployLockupVolumeRTMAndVerified } from "./helpers/createInstances"; +import { catchRevert } from "./helpers/exceptions"; + +const SecurityToken = artifacts.require('./SecurityToken.sol'); +const GeneralTransferManager = artifacts.require('./GeneralTransferManager'); +const LockUpTransferManager = artifacts.require('./LockUpTransferManager'); +const GeneralPermissionManager = artifacts.require('./GeneralPermissionManager'); + +const Web3 = require('web3'); +let BN = Web3.utils.BN; +const web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545")) // Hardcoded development port + +contract('LockUpTransferManager', accounts => { + + // Accounts Variable declaration + let account_polymath; + let account_issuer; + let token_owner; + let account_investor1; + let account_investor2; + let account_investor3; + let account_investor4; + + let message = "Transaction Should Fail!"; + + // Contract Instance Declaration + let P_LockUpTransferManagerFactory; + let I_SecurityTokenRegistryProxy; + let P_LockUpTransferManager; + let I_GeneralTransferManagerFactory; + let I_LockUpTransferManagerFactory; + let I_GeneralPermissionManager; + let I_LockUpTransferManager; + let I_GeneralTransferManager; + let I_ModuleRegistryProxy; + let I_ModuleRegistry; + let I_FeatureRegistry; + let I_SecurityTokenRegistry; + let I_STRProxied; + let I_MRProxied; + let I_STFactory; + let I_SecurityToken; + let I_PolyToken; + let I_PolymathRegistry; + let I_SecurityToken_div; + let I_GeneralTransferManager_div; + let I_LockUpVolumeRestrictionTM_div; + + // SecurityToken Details + const name = "Team"; + const symbol = "sap"; + const tokenDetails = "This is equity type of issuance"; + const decimals = 18; + const contact = "team@polymath.network"; + + const name2 = "Core"; + const symbol2 = "Core"; + + // Module key + const delegateManagerKey = 1; + const transferManagerKey = 2; + const stoKey = 3; + + let temp; + + // Initial fee for ticker registry and security token registry + const initRegFee = new BN(web3.utils.toWei("250")); + let currentTime; + + before(async() => { + currentTime = new BN(await latestTime()); + // Accounts setup + account_polymath = accounts[0]; + account_issuer = accounts[1]; + + token_owner = account_issuer; + + account_investor1 = accounts[7]; + account_investor2 = accounts[8]; + account_investor3 = accounts[9]; + + let instances = await setUpPolymathNetwork(account_polymath, token_owner); + + [ + I_PolymathRegistry, + I_PolyToken, + I_FeatureRegistry, + I_ModuleRegistry, + I_ModuleRegistryProxy, + I_MRProxied, + I_GeneralTransferManagerFactory, + I_STFactory, + I_SecurityTokenRegistry, + I_SecurityTokenRegistryProxy, + I_STRProxied + ] = instances; + + // STEP 4(c): Deploy the LockUpVolumeRestrictionTMFactory + [I_LockUpTransferManagerFactory] = await deployLockupVolumeRTMAndVerified(account_polymath, I_MRProxied, 0); + // STEP 4(d): Deploy the LockUpVolumeRestrictionTMFactory + [P_LockUpTransferManagerFactory] = await deployLockupVolumeRTMAndVerified(account_polymath, I_MRProxied, new BN(web3.utils.toWei("500"))); + + // Printing all the contract addresses + console.log(` + --------------------- Polymath Network Smart Contracts: --------------------- + PolymathRegistry: ${I_PolymathRegistry.address} + SecurityTokenRegistryProxy: ${I_SecurityTokenRegistryProxy.address} + SecurityTokenRegistry: ${I_SecurityTokenRegistry.address} + ModuleRegistry: ${I_ModuleRegistry.address} + ModuleRegistryProxy: ${I_ModuleRegistryProxy.address} + FeatureRegistry: ${I_FeatureRegistry.address} + + STFactory: ${I_STFactory.address} + GeneralTransferManagerFactory: ${I_GeneralTransferManagerFactory.address} + + LockupVolumeRestrictionTransferManagerFactory: + ${I_LockUpTransferManagerFactory.address} + ----------------------------------------------------------------------------- + `); + }); + + describe("Generate the SecurityToken", async() => { + + it("Should register the ticker before the generation of the security token", async () => { + await I_PolyToken.approve(I_STRProxied.address, initRegFee, { from: token_owner }); + let tx = await I_STRProxied.registerTicker(token_owner, symbol, contact, { from : token_owner }); + assert.equal(tx.logs[0].args._owner, token_owner); + assert.equal(tx.logs[0].args._ticker, symbol.toUpperCase()); + }); + + it("Should generate the new security token with the same symbol as registered above", async () => { + await I_PolyToken.approve(I_STRProxied.address, initRegFee, { from: token_owner }); + let _blockNo = latestBlock(); + let tx = await I_STRProxied.generateSecurityToken(name, symbol, tokenDetails, false, { from: token_owner }); + + // Verify the successful generation of the security token + assert.equal(tx.logs[2].args._ticker, symbol.toUpperCase(), "SecurityToken doesn't get deployed"); + + I_SecurityToken = await SecurityToken.at(tx.logs[2].args._securityTokenAddress); + + const log = (await I_SecurityToken.getPastEvents('ModuleAdded', {filter: {transactionHash: tx.transactionHash}}))[0]; + + // Verify that GeneralTransferManager module get added successfully or not + assert.equal(log.args._types[0].toString(), 2); + assert.equal( + web3.utils.toAscii(log.args._name) + .replace(/\u0000/g, ''), + "GeneralTransferManager" + ); + }); + + it("Should intialize the auto attached modules", async () => { + let moduleData = (await I_SecurityToken.getModulesByType(2))[0]; + I_GeneralTransferManager = await GeneralTransferManager.at(moduleData); + }); + it("Should register another ticker before the generation of new security token", async () => { + await I_PolyToken.approve(I_STRProxied.address, initRegFee, { from: token_owner }); + let tx = await I_STRProxied.registerTicker(token_owner, symbol2, contact, { from : token_owner }); + assert.equal(tx.logs[0].args._owner, token_owner); + assert.equal(tx.logs[0].args._ticker, symbol2.toUpperCase()); + }); + + it("Should generate the new security token with the same symbol as registered above", async () => { + await I_PolyToken.approve(I_STRProxied.address, initRegFee, { from: token_owner }); + let _blockNo = latestBlock(); + let tx = await I_STRProxied.generateSecurityToken(name2, symbol2, tokenDetails, true, { from: token_owner }); + + // Verify the successful generation of the security token + assert.equal(tx.logs[2].args._ticker, symbol2.toUpperCase(), "SecurityToken doesn't get deployed"); + + I_SecurityToken_div = await SecurityToken.at(tx.logs[2].args._securityTokenAddress); + const log = (await I_SecurityToken_div.getPastEvents('ModuleAdded', {filter: {transactionHash: tx.transactionHash}}))[0]; + + // Verify that GeneralTransferManager module get added successfully or not + assert.equal(log.args._types[0].toString(), 2); + assert.equal( + web3.utils.toAscii(log.args._name) + .replace(/\u0000/g, ''), + "GeneralTransferManager" + ); + }); + + it("Should intialize the auto attached modules", async () => { + let moduleData = (await I_SecurityToken_div.getModulesByType(2))[0]; + I_GeneralTransferManager_div = GeneralTransferManager.at(moduleData); + }); + + + }); + + describe("Buy tokens using on-chain whitelist and test locking them up and attempting to transfer", async() => { + + it("Should Buy the tokens from non-divisible", async() => { + // Add the Investor in to the whitelist + console.log(account_investor1); + let tx = await I_GeneralTransferManager.modifyWhitelist( + account_investor1, + currentTime, + currentTime, + currentTime.add(new BN(duration.days(10))), + true, + { + from: account_issuer + }); + + assert.equal(tx.logs[0].args._investor.toLowerCase(), account_investor1.toLowerCase(), "Failed in adding the investor in whitelist"); + + // Jump time + await increaseTime(5000); + + // Mint some tokens + await I_SecurityToken.mint(account_investor1, new BN(web3.utils.toWei('2', 'ether')), { from: token_owner }); + + assert.equal( + (await I_SecurityToken.balanceOf(account_investor1)).toString(), + web3.utils.toWei('2', 'ether') + ); + }); + + it("Should Buy some more tokens from non-divisible tokens", async() => { + // Add the Investor in to the whitelist + + let tx = await I_GeneralTransferManager.modifyWhitelist( + account_investor2, + currentTime, + currentTime, + currentTime.add(new BN(duration.days(10))), + true, + { + from: account_issuer + }); + + assert.equal(tx.logs[0].args._investor.toLowerCase(), account_investor2.toLowerCase(), "Failed in adding the investor in whitelist"); + + // Mint some tokens + await I_SecurityToken.mint(account_investor2, web3.utils.toWei('10', 'ether'), { from: token_owner }); + + assert.equal( + (await I_SecurityToken.balanceOf(account_investor2)).toString(), + web3.utils.toWei('10', 'ether') + ); + }); + + it("Should unsuccessfully attach the LockUpTransferManager factory with the security token -- failed because Token is not paid", async () => { + await I_PolyToken.getTokens(web3.utils.toWei("500", "ether"), token_owner); + await catchRevert( + I_SecurityToken.addModule(P_LockUpTransferManagerFactory.address, "0x", new BN(web3.utils.toWei("500", "ether")), 0, { from: token_owner }) + ) + }); + + it("Should successfully attach the LockUpTransferManager factory with the security token", async () => { + let snapId = await takeSnapshot(); + await I_PolyToken.transfer(I_SecurityToken.address, new BN(web3.utils.toWei("500", "ether")), {from: token_owner}); + console.log((await P_LockUpTransferManagerFactory.getSetupCost.call()).toString()); + const tx = await I_SecurityToken.addModule(P_LockUpTransferManagerFactory.address, "0x", new BN(web3.utils.toWei("500", "ether")), new BN(0), { from: token_owner }); + assert.equal(tx.logs[3].args._types[0].toString(), transferManagerKey, "LockUpVolumeRestrictionTMFactory doesn't get deployed"); + assert.equal( + web3.utils.toAscii(tx.logs[3].args._name) + .replace(/\u0000/g, ''), + "LockUpTransferManager", + "LockUpTransferManager module was not added" + ); + P_LockUpTransferManager = await LockUpTransferManager.at(tx.logs[3].args._module); + await revertToSnapshot(snapId); + }); + + it("Should successfully attach the LockUpVolumeRestrictionTMFactory with the non-divisible security token", async () => { + const tx = await I_SecurityToken.addModule(I_LockUpTransferManagerFactory.address, "0x", 0, 0, { from: token_owner }); + assert.equal(tx.logs[2].args._types[0].toString(), transferManagerKey, "LockUpVolumeRestrictionTMFactory doesn't get deployed"); + assert.equal( + web3.utils.toAscii(tx.logs[2].args._name) + .replace(/\u0000/g, ''), + "LockUpTransferManager", + "LockUpTransferManager module was not added" + ); + I_LockUpTransferManager = await LockUpTransferManager.at(tx.logs[2].args._module); + }); + + it("Should successfully attach the LockUpVolumeRestrictionTMFactory with the divisible security token", async () => { + const tx = await I_SecurityToken_div.addModule(I_LockUpTransferManagerFactory.address, "0x", 0, 0, { from: token_owner }); + assert.equal(tx.logs[2].args._types[0].toString(), transferManagerKey, "LockUpVolumeRestrictionTMFactory doesn't get deployed"); + assert.equal( + web3.utils.toAscii(tx.logs[2].args._name) + .replace(/\u0000/g, ''), + "LockUpTransferManager", + "LockUpTransferManager module was not added" + ); + I_LockUpVolumeRestrictionTM_div = await LockUpTransferManager.at(tx.logs[2].args._module); + }); + + it("Add a new token holder", async() => { + + let tx = await I_GeneralTransferManager.modifyWhitelist( + account_investor3, + currentTime, + currentTime, + currentTime.add(new BN(duration.days(10))), + true, + { + from: account_issuer + }); + + assert.equal(tx.logs[0].args._investor.toLowerCase(), account_investor3.toLowerCase(), "Failed in adding the investor in whitelist"); + + // Add the Investor in to the whitelist + // Mint some tokens + await I_SecurityToken.mint(account_investor3, web3.utils.toWei('10', 'ether'), { from: token_owner }); + + assert.equal( + (await I_SecurityToken.balanceOf(account_investor3)).toString(), + web3.utils.toWei('10', 'ether') + ); + }); + + it("Should pause the tranfers at transferManager level", async() => { + let tx = await I_LockUpTransferManager.pause({from: token_owner}); + }); + + it("Should still be able to transfer between existing token holders up to limit", async() => { + // Transfer Some tokens between the investor + await I_SecurityToken.transfer(account_investor1, web3.utils.toWei('1', 'ether'), { from: account_investor2 }); + + assert.equal( + (await I_SecurityToken.balanceOf(account_investor1)).toString(), + web3.utils.toWei('3', 'ether') + ); + }); + + it("Should unpause the tranfers at transferManager level", async() => { + await I_LockUpTransferManager.unpause({from: token_owner}); + }); + + it("Should prevent the creation of a lockup with bad parameters where the lockupAmount is zero", async() => { + // create a lockup + // this will generate an exception because the lockupAmount is zero + await catchRevert( + I_LockUpTransferManager.addNewLockUpToUser( + account_investor2, + 0, + currentTime.add(new BN(duration.seconds(1))), + new BN(duration.seconds(400000)), + new BN(duration.seconds(100000)), + web3.utils.fromAscii("a_lockup"), + { + from: token_owner + } + ) + ) + }); + + it("Should prevent the creation of a lockup with bad parameters where the releaseFrequencySeconds is zero", async() => { + // create a lockup + // this will generate an exception because the releaseFrequencySeconds is zero + await catchRevert( + I_LockUpTransferManager.addNewLockUpToUser( + account_investor2, + new BN(web3.utils.toWei('1', 'ether')), + currentTime.add(new BN(duration.seconds(1))), + new BN(duration.seconds(400000)), + 0, + web3.utils.fromAscii("a_lockup"), + { + from: token_owner + } + ) + ); + }); + + it("Should prevent the creation of a lockup with bad parameters where the lockUpPeriodSeconds is zero", async() => { + // create a lockup + // this will generate an exception because the lockUpPeriodSeconds is zero + await catchRevert( + I_LockUpTransferManager.addNewLockUpToUser( + account_investor2, + new BN(web3.utils.toWei('1', 'ether')), + currentTime.add(new BN(duration.seconds(1))), + 0, + new BN(duration.seconds(100000)), + web3.utils.fromAscii("a_lockup"), + { + from: token_owner + } + ) + ); + }); + + it("Should add the lockup type -- fail because of bad owner", async() => { + await catchRevert( + I_LockUpTransferManager.addNewLockUpType( + new BN(web3.utils.toWei('12', 'ether')), + currentTime.add(new BN(duration.days(1))), + 60, + 20, + web3.utils.fromAscii("a_lockup"), + { + from: account_investor1 + } + ) + ); + }) + + it("Should add the new lockup type", async() => { + let tx = await I_LockUpTransferManager.addNewLockUpType( + new BN(web3.utils.toWei('12', 'ether')), + currentTime.add(new BN(duration.days(1))), + 60, + 20, + web3.utils.fromAscii("a_lockup"), + { + from: token_owner + } + ); + assert.equal((tx.logs[0].args._lockupAmount).toString(), web3.utils.toWei('12', 'ether')); + }); + + it("Should fail to add the creation of the lockup where lockupName is already exists", async() => { + await catchRevert( + I_LockUpTransferManager.addNewLockUpToUser( + account_investor1, + web3.utils.toWei('5', 'ether'), + currentTime.add(new BN(duration.seconds(1))), + new BN(duration.seconds(400000)), + new BN(duration.seconds(100000)), + web3.utils.fromAscii("a_lockup"), + { + from: token_owner + } + ) + ); + }) + + it("Should allow the creation of a lockup where the lockup amount is divisible" , async() => { + // create a lockup + currentTime = new BN(await latestTime()); + let tx = await I_LockUpVolumeRestrictionTM_div.addNewLockUpToUser( + account_investor1, + web3.utils.toWei('0.5', 'ether'), + currentTime.add(new BN(duration.seconds(1))), + new BN(duration.seconds(400000)), + new BN(duration.seconds(100000)), + web3.utils.fromAscii("a_lockup"), + { + from: token_owner + } + ); + assert.equal(tx.logs[1].args._userAddress, account_investor1); + assert.equal((tx.logs[0].args._lockupAmount).toString(), web3.utils.toWei('0.5', 'ether')); + }); + + it("Should allow the creation of a lockup where the lockup amount is prime no", async() => { + // create a lockup + let tx = await I_LockUpVolumeRestrictionTM_div.addNewLockUpToUser( + account_investor1, + new BN(web3.utils.toWei('64951', 'ether')), + currentTime.add(new BN(duration.seconds(1))), + new BN(duration.seconds(400000)), + new BN(duration.seconds(100000)), + web3.utils.fromAscii("b_lockup"), + { + from: token_owner + } + ); + assert.equal(tx.logs[1].args._userAddress, account_investor1); + assert.equal((tx.logs[0].args._lockupAmount).toString(), web3.utils.toWei('64951', 'ether')); + }); + + it("Should prevent the transfer of tokens in a lockup", async() => { + + let balance = await I_SecurityToken.balanceOf(account_investor2) + console.log("balance", balance.div(new BN(1).mul(new BN(10).pow(new BN(18)))).toString()); + // create a lockup for their entire balance + // over 12 seconds total, with 3 periods of 20 seconds each. + await I_LockUpTransferManager.addNewLockUpToUser( + account_investor2, + balance, + currentTime.add(new BN(duration.seconds(1))), + 60, + 20, + web3.utils.fromAscii("b_lockup"), + { + from: token_owner + } + ); + await increaseTime(2); + let tx = await I_LockUpTransferManager.getLockUp.call(web3.utils.fromAscii("b_lockup")); + console.log("Amount get unlocked:", (tx[4].toString())); + await catchRevert( + I_SecurityToken.transfer(account_investor1, web3.utils.toWei('1', 'ether'), { from: account_investor2 }) + ); + }); + + it("Should prevent the transfer of tokens if the amount is larger than the amount allowed by lockups", async() => { + // wait 20 seconds + await increaseTime(duration.seconds(20)); + let tx = await I_LockUpTransferManager.getLockUp.call(web3.utils.fromAscii("b_lockup")); + console.log("Amount get unlocked:", (tx[4].toString())); + await catchRevert( + I_SecurityToken.transfer(account_investor1, web3.utils.toWei('4', 'ether'), { from: account_investor2 }) + ); + }); + + it("Should allow the transfer of tokens in a lockup if a period has passed", async() => { + let tx = await I_LockUpTransferManager.getLockUp.call(web3.utils.fromAscii("b_lockup")); + console.log("Amount get unlocked:", (tx[4].toString())); + await I_SecurityToken.transfer(account_investor1, web3.utils.toWei('1', 'ether'), { from: account_investor2 }); + }); + + it("Should again transfer of tokens in a lockup if a period has passed", async() => { + let tx = await I_LockUpTransferManager.getLockUp.call(web3.utils.fromAscii("b_lockup")); + console.log("Amount get unlocked:", (tx[4].toString())); + await I_SecurityToken.transfer(account_investor1, web3.utils.toWei('1', 'ether'), { from: account_investor2 }); + }); + + it("Should allow the transfer of more tokens in a lockup if another period has passed", async() => { + + // wait 20 more seconds + 1 to get rid of same block time + await increaseTime(duration.seconds(21)); + let tx = await I_LockUpTransferManager.getLockUp.call(web3.utils.fromAscii("b_lockup")); + console.log("Amount get unlocked:", (tx[4].toString())); + await I_SecurityToken.transfer(account_investor1, web3.utils.toWei('2', 'ether'), { from: account_investor2 }); + }); + + it("Buy more tokens from secondary market to investor2", async() => { + // Mint some tokens + await I_SecurityToken.mint(account_investor2, web3.utils.toWei('5', 'ether'), { from: token_owner }); + + assert.equal( + (await I_SecurityToken.balanceOf(account_investor2)).toString(), + web3.utils.toWei('10', 'ether') + ); + }) + + it("Should allow transfer for the tokens that comes from secondary market + unlocked tokens", async() => { + + await I_SecurityToken.transfer(account_investor1, web3.utils.toWei('4', 'ether'), { from: account_investor2 }); + assert.equal( + (await I_SecurityToken.balanceOf(account_investor2)).toString(), + web3.utils.toWei('6', 'ether') + ); + }); + + it("Should allow the transfer of all tokens in a lockup if the entire lockup has passed", async() => { + + let balance = await I_SecurityToken.balanceOf(account_investor2) + + // wait 20 more seconds + 1 to get rid of same block time + await increaseTime(duration.seconds(21)); + console.log((await I_LockUpTransferManager.getLockedTokenToUser.call(account_investor2)).toString()); + await I_SecurityToken.transfer(account_investor1, balance, { from: account_investor2 }); + assert.equal( + (await I_SecurityToken.balanceOf(account_investor2)).toString(), + web3.utils.toWei('0', 'ether') + ); + }); + + it("Should fail to add the multiple lockups -- because array length mismatch", async() => { + await catchRevert( + I_LockUpTransferManager.addNewLockUpToUserMulti( + [account_investor3], + [web3.utils.toWei("6", "ether"), web3.utils.toWei("3", "ether")], + [currentTime.add(new BN(duration.seconds(1))), currentTime.add(new BN(duration.seconds(21)))], + [60, 45], + [20, 15], + [web3.utils.fromAscii("c_lockup"), web3.utils.fromAscii("d_lockup")], + { + from: token_owner + } + ) + ); + }) + + it("Should fail to add the multiple lockups -- because array length mismatch", async() => { + await catchRevert( + I_LockUpTransferManager.addNewLockUpToUserMulti( + [account_investor3, account_investor3], + [], + [currentTime.add(new BN(duration.seconds(1))), currentTime.add(new BN(duration.seconds(21)))], + [60, 45], + [20, 15], + [web3.utils.fromAscii("c_lockup"), web3.utils.fromAscii("d_lockup")], + { + from: token_owner + } + ) + ); + }) + + it("Should fail to add the multiple lockups -- because array length mismatch", async() => { + await catchRevert( + I_LockUpTransferManager.addNewLockUpToUserMulti( + [account_investor3, account_investor3], + [web3.utils.toWei("6", "ether"), web3.utils.toWei("3", "ether")], + [currentTime.add(new BN(duration.seconds(1))), currentTime.add(new BN(duration.seconds(21)))], + [60, 45, 50], + [20, 15], + [web3.utils.fromAscii("c_lockup"), web3.utils.fromAscii("d_lockup")], + { + from: token_owner + } + ) + ); + }) + + it("Should fail to add the multiple lockups -- because array length mismatch", async() => { + await catchRevert( + I_LockUpTransferManager.addNewLockUpToUserMulti( + [account_investor3, account_investor3], + [web3.utils.toWei("6", "ether"), web3.utils.toWei("3", "ether")], + [currentTime.add(new BN(duration.seconds(1))), currentTime.add(new BN(duration.seconds(21)))], + [60, 45, 50], + [20, 15, 10], + [web3.utils.fromAscii("c_lockup"), web3.utils.fromAscii("d_lockup")], + { + from: token_owner + } + ) + ); + }) + + it("Should fail to add the multiple lockups -- because array length mismatch", async() => { + await catchRevert( + I_LockUpTransferManager.addNewLockUpToUserMulti( + [account_investor3, account_investor3], + [web3.utils.toWei("6", "ether"), web3.utils.toWei("3", "ether")], + [currentTime.add(new BN(duration.seconds(1))), currentTime.add(new BN(duration.seconds(21)))], + [60, 45], + [20, 15], + [web3.utils.fromAscii("c_lockup")], + { + from: token_owner + } + ) + ); + }); + + it("Should add the multiple lockup to a address", async() => { + currentTime = new BN(await latestTime()); + await I_LockUpTransferManager.addNewLockUpToUserMulti( + [account_investor3, account_investor3], + [web3.utils.toWei("6", "ether"), web3.utils.toWei("3", "ether")], + [currentTime.add(new BN(duration.seconds(1))), currentTime.add(new BN(duration.seconds(21)))], + [60, 45], + [20, 15], + [web3.utils.fromAscii("c_lockup"), web3.utils.fromAscii("d_lockup")], + { + from: token_owner + } + ); + + await increaseTime(1); + let tx = await I_LockUpTransferManager.getLockUp.call(web3.utils.fromAscii("c_lockup")); + let tx2 = await I_LockUpTransferManager.getLockUp.call(web3.utils.fromAscii("d_lockup")); + console.log("Total Amount get unlocked:", (tx[4].toString()) + (tx2[4].toString())); + await catchRevert( + I_SecurityToken.transfer(account_investor2, web3.utils.toWei('2', 'ether'), { from: account_investor3 }) + ); + + }); + + it("Should transfer the tokens after period get passed", async() => { + // increase 20 sec that makes 1 period passed + // 2 from a period and 1 is already unlocked + await increaseTime(21); + await I_SecurityToken.transfer(account_investor1, web3.utils.toWei('3'), { from: account_investor3 }) + }) + + it("Should transfer the tokens after passing another period of the lockup", async() => { + // increase the 15 sec that makes first period of another lockup get passed + // allow 1 token to transfer + await increaseTime(15); + // first fail because 3 tokens are not in unlocked state + await catchRevert( + I_SecurityToken.transfer(account_investor1, web3.utils.toWei('3'), { from: account_investor3 }) + ) + // second txn will pass because 1 token is in unlocked state + await I_SecurityToken.transfer(account_investor1, web3.utils.toWei('1'), { from: account_investor3 }) + }); + + it("Should transfer the tokens from both the lockup simultaneously", async() => { + // Increase the 20 sec (+ 1 to mitigate the block time) that unlocked another 2 tokens from the lockup 1 and simultaneously unlocked 1 + // more token from the lockup 2 + await increaseTime(21); + + await I_SecurityToken.transfer(account_investor1, web3.utils.toWei('3'), { from: account_investor3 }) + assert.equal( + (await I_SecurityToken.balanceOf(account_investor3)).toString(), + web3.utils.toWei('3', 'ether') + ); + }); + + it("Should remove multiple lockup --failed because of bad owner", async() => { + await catchRevert( + I_LockUpTransferManager.removeLockUpFromUserMulti( + [account_investor3, account_investor3], + [web3.utils.fromAscii("c_lockup"), web3.utils.fromAscii("d_lockup")], + { + from: account_polymath + } + ) + ); + }); + + it("Should remove the multiple lockup -- failed because of invalid lockupname", async() => { + await catchRevert( + I_LockUpTransferManager.removeLockUpFromUserMulti( + [account_investor3, account_investor3], + [web3.utils.fromAscii("c_lockup"), web3.utils.fromAscii("e_lockup")], + { + from: account_polymath + } + ) + ); + }) + + it("Should remove the multiple lockup", async() => { + await I_LockUpTransferManager.removeLockUpFromUserMulti( + [account_investor3, account_investor3], + [web3.utils.fromAscii("d_lockup"), web3.utils.fromAscii("c_lockup")], + { + from: token_owner + } + ) + // do the free transaction now + await I_SecurityToken.transfer(account_investor1, web3.utils.toWei('3'), { from: account_investor3 }) + }); + + it("Should fail to modify the lockup -- because of bad owner", async() => { + await I_SecurityToken.mint(account_investor3, web3.utils.toWei("9"), {from: token_owner}); + + let tx = await I_LockUpTransferManager.addNewLockUpToUser( + account_investor3, + web3.utils.toWei("9"), + currentTime.add(new BN(duration.minutes(5))), + 60, + 20, + web3.utils.fromAscii("z_lockup"), + { + from: token_owner + } + ); + + await catchRevert( + // edit the lockup + I_LockUpTransferManager.modifyLockUpType( + web3.utils.toWei("9"), + currentTime.add(new BN(duration.seconds(1))), + 60, + 20, + web3.utils.fromAscii("z_lockup"), + { + from: account_polymath + } + ) + ) + }) + + it("Modify the lockup when startTime is in past -- failed because startTime is in the past", async() => { + await catchRevert( + // edit the lockup + I_LockUpTransferManager.modifyLockUpType( + web3.utils.toWei("9"), + currentTime.add(new BN(duration.seconds(50))), + 60, + 20, + web3.utils.fromAscii("z_lockup"), + { + from: token_owner + } + ) + ) + }) + + it("Modify the lockup when startTime is in past -- failed because of invalid index", async() => { + await catchRevert( + // edit the lockup + I_LockUpTransferManager.modifyLockUpType( + web3.utils.toWei("9"), + currentTime.add(new BN(duration.seconds(50))), + 60, + 20, + web3.utils.fromAscii("m_lockup"), + { + from: token_owner + } + ) + ) + }) + + it("should successfully modify the lockup", async() => { + // edit the lockup + currentTime = new BN(await latestTime()); + await I_LockUpTransferManager.modifyLockUpType( + web3.utils.toWei("9"), + currentTime.add(new BN(duration.seconds(50))), + 60, + 20, + web3.utils.fromAscii("z_lockup"), + { + from: token_owner + } + ); + }) + + it("Should prevent the transfer of tokens in an edited lockup", async() => { + + // balance here should be 12000000000000000000 (12e18 or 12 eth) + let balance = await I_SecurityToken.balanceOf(account_investor1) + + console.log("balance", balance.div(new BN(1).mul(new BN(10).pow(new BN(18)))).toString()); + + // create a lockup for their entire balance + // over 16 seconds total, with 4 periods of 4 seconds each. + await I_LockUpTransferManager.addNewLockUpToUser( + account_investor1, + balance, + currentTime.add(new BN(duration.minutes(5))), + 60, + 20, + web3.utils.fromAscii("f_lockup"), + { + from: token_owner + } + ); + + await catchRevert( + I_SecurityToken.transfer(account_investor2, web3.utils.toWei('1', 'ether'), { from: account_investor1 }) + ); + + let lockUp = await I_LockUpTransferManager.getLockUp(web3.utils.fromAscii("f_lockup")); + console.log(lockUp); + // elements in lockup array are uint lockUpPeriodSeconds, uint releaseFrequencySeconds, uint startTime, uint totalAmount + assert.equal( + lockUp[0].div(new BN(1).mul(new BN(10).pow(new BN(18)))).toString(), + balance.div(new BN(1).mul(new BN(10).pow(new BN(18)))).toString() + ); + assert.equal(lockUp[2].toString(), 60); + assert.equal(lockUp[3].toString(), 20); + assert.equal(lockUp[4].toString(), 0); + + // edit the lockup + temp = currentTime.add(new BN(duration.seconds(1))); + await I_LockUpTransferManager.modifyLockUpType( + balance, + temp, + 60, + 20, + web3.utils.fromAscii("f_lockup"), + { + from: token_owner + } + ); + + // attempt a transfer + await catchRevert( + I_SecurityToken.transfer(account_investor2, web3.utils.toWei('6', 'ether'), { from: account_investor1 }) + ); + + // wait 20 seconds + await increaseTime(21); + + // transfer should succeed + await I_SecurityToken.transfer(account_investor2, web3.utils.toWei('6', 'ether'), { from: account_investor1 }); + + }); + + it("Modify the lockup during the lockup periods", async() => { + let balance = await I_SecurityToken.balanceOf(account_investor1) + let lockUp = await I_LockUpTransferManager.getLockUp(web3.utils.fromAscii("f_lockup")); + console.log(lockUp[4].div(new BN(1).mul(new BN(10).pow(new BN(18)))).toString()); + // edit the lockup + await I_LockUpTransferManager.modifyLockUpType( + balance, + currentTime.add(new BN(duration.days(10))), + 90, + 30, + web3.utils.fromAscii("f_lockup"), + { + from: token_owner + } + ); + }); + + it("Should remove the lockup multi", async() => { + await I_LockUpTransferManager.addNewLockUpTypeMulti( + [web3.utils.toWei("10"), web3.utils.toWei("16")], + [currentTime.add(new BN(duration.days(1))), currentTime.add(new BN(duration.days(1)))], + [50000, 50000], + [10000, 10000], + [web3.utils.fromAscii("k_lockup"), web3.utils.fromAscii("l_lockup")], + { + from: token_owner + } + ); + + // removing the lockup type + let tx = await I_LockUpTransferManager.removeLockupType(web3.utils.fromAscii("k_lockup"), {from: token_owner}); + assert.equal(web3.utils.toUtf8(tx.logs[0].args._lockupName), "k_lockup"); + + // attaching the lockup to a user + + await I_LockUpTransferManager.addLockUpByName(account_investor2, web3.utils.fromAscii("l_lockup"), {from: token_owner}); + + // try to delete the lockup but fail + + await catchRevert( + I_LockUpTransferManager.removeLockupType(web3.utils.fromAscii("l_lockup"), {from: token_owner}) + ); + }) + + it("Should succesfully get the non existed lockup value, it will give everything 0", async() => { + let data = await I_LockUpTransferManager.getLockUp(web3.utils.fromAscii("foo")); + assert.equal(data[0], 0); + }) + + it("Should get configuration function signature", async() => { + let sig = await I_LockUpTransferManager.getInitFunction.call(); + assert.equal(web3.utils.hexToNumber(sig), 0); + }); + + it("Should get the all lockups added by the issuer till now", async() => { + let tx = await I_LockUpTransferManager.getAllLockups.call(); + for (let i = 0; i < tx.length; i++) { + console.log(web3.utils.toUtf8(tx[i])); + } + }) + + it("Should get the permission", async() => { + let perm = await I_LockUpTransferManager.getPermissions.call(); + assert.equal(perm.length, 1); + assert.equal(web3.utils.toAscii(perm[0]).replace(/\u0000/g, ''), "ADMIN") + }); + + }); + + describe("LockUpTransferManager Transfer Manager Factory test cases", async() => { + + it("Should get the exact details of the factory", async() => { + assert.equal(await I_LockUpTransferManagerFactory.getSetupCost.call(),0); + assert.equal((await I_LockUpTransferManagerFactory.getTypes.call())[0],2); + assert.equal(web3.utils.toAscii(await I_LockUpTransferManagerFactory.getName.call()) + .replace(/\u0000/g, ''), + "LockUpTransferManager", + "Wrong Module added"); + assert.equal(await I_LockUpTransferManagerFactory.description.call(), + "Manage transfers using lock ups over time", + "Wrong Module added"); + assert.equal(await I_LockUpTransferManagerFactory.title.call(), + "LockUp Transfer Manager", + "Wrong Module added"); + assert.equal(await I_LockUpTransferManagerFactory.getInstructions.call(), + "Allows an issuer to set lockup periods for user addresses, with funds distributed over time. Init function takes no parameters.", + "Wrong Module added"); + assert.equal(await I_LockUpTransferManagerFactory.version.call(), "1.0.0"); + }); + + it("Should get the tags of the factory", async() => { + let tags = await I_LockUpTransferManagerFactory.getTags.call(); + assert.equal(web3.utils.toAscii(tags[0]).replace(/\u0000/g, ''), "LockUp"); + }); + }); + +}); diff --git a/test/w_lockup_volume_restriction_transfer_manager.js b/test/w_lockup_volume_restriction_transfer_manager.js deleted file mode 100644 index 5b2a4f8e8..000000000 --- a/test/w_lockup_volume_restriction_transfer_manager.js +++ /dev/null @@ -1,793 +0,0 @@ -import latestTime from "./helpers/latestTime"; -import { duration, promisifyLogWatch, latestBlock } from "./helpers/utils"; -import takeSnapshot, { increaseTime, revertToSnapshot } from "./helpers/time"; -import { encodeProxyCall } from "./helpers/encodeCall"; -import { setUpPolymathNetwork, deployLockupVolumeRTMAndVerified } from "./helpers/createInstances"; -import { catchRevert } from "./helpers/exceptions"; - -const SecurityToken = artifacts.require("./SecurityToken.sol"); -const GeneralTransferManager = artifacts.require("./GeneralTransferManager"); -const VolumeRestrictionTransferManager = artifacts.require("./LockupVolumeRestrictionTM"); -const GeneralPermissionManager = artifacts.require("./GeneralPermissionManager"); - -const Web3 = require("web3"); -let BN = Web3.utils.BN; -const web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545")); // Hardcoded development port - -contract("LockupVolumeRestrictionTransferManager", async (accounts) => { - // Accounts Variable declaration - let account_polymath; - let account_issuer; - let token_owner; - let account_investor1; - let account_investor2; - let account_investor3; - let account_investor4; - - let message = "Transaction Should Fail!"; - - // Contract Instance Declaration - let P_VolumeRestrictionTransferManagerFactory; - let I_SecurityTokenRegistryProxy; - let P_VolumeRestrictionTransferManager; - let I_GeneralTransferManagerFactory; - let I_VolumeRestrictionTransferManagerFactory; - let I_GeneralPermissionManager; - let I_VolumeRestrictionTransferManager; - let I_GeneralTransferManager; - let I_ModuleRegistryProxy; - let I_ModuleRegistry; - let I_FeatureRegistry; - let I_SecurityTokenRegistry; - let I_STRProxied; - let I_MRProxied; - let I_STFactory; - let I_SecurityToken; - let I_PolyToken; - let I_PolymathRegistry; - let I_STRGetter - - // SecurityToken Details - const name = "Team"; - const symbol = "sap"; - const tokenDetails = "This is equity type of issuance"; - const decimals = 18; - const contact = "team@polymath.network"; - - // Module key - const delegateManagerKey = 1; - const transferManagerKey = 2; - const stoKey = 3; - - // Initial fee for ticker registry and security token registry - const initRegFee = new BN(web3.utils.toWei("250")); - - let currentTime; - const address_zero = "0x0000000000000000000000000000000000000000"; - const one_address = "0x0000000000000000000000000000000000000001"; - - before(async () => { - currentTime = new BN(await latestTime()); - account_polymath = accounts[0]; - account_issuer = accounts[1]; - - token_owner = account_issuer; - - account_investor1 = accounts[7]; - account_investor2 = accounts[8]; - account_investor3 = accounts[9]; - - let instances = await setUpPolymathNetwork(account_polymath, token_owner); - - [ - I_PolymathRegistry, - I_PolyToken, - I_FeatureRegistry, - I_ModuleRegistry, - I_ModuleRegistryProxy, - I_MRProxied, - I_GeneralTransferManagerFactory, - I_STFactory, - I_SecurityTokenRegistry, - I_SecurityTokenRegistryProxy, - I_STRProxied, - I_STRGetter - ] = instances; - - // STEP 4(c): Deploy the VolumeRestrictionTransferManager - [I_VolumeRestrictionTransferManagerFactory] = await deployLockupVolumeRTMAndVerified(account_polymath, I_MRProxied, 0); - // STEP 4(d): Deploy the VolumeRestrictionTransferManager - [P_VolumeRestrictionTransferManagerFactory] = await deployLockupVolumeRTMAndVerified( - account_polymath, - I_MRProxied, - new BN(web3.utils.toWei("500")) - ); - - // Printing all the contract addresses - console.log(` - --------------------- Polymath Network Smart Contracts: --------------------- - PolymathRegistry: ${I_PolymathRegistry.address} - SecurityTokenRegistryProxy: ${I_SecurityTokenRegistryProxy.address} - SecurityTokenRegistry: ${I_SecurityTokenRegistry.address} - ModuleRegistry: ${I_ModuleRegistry.address} - ModuleRegistryProxy: ${I_ModuleRegistryProxy.address} - FeatureRegistry: ${I_FeatureRegistry.address} - - STFactory: ${I_STFactory.address} - GeneralTransferManagerFactory: ${I_GeneralTransferManagerFactory.address} - - LockupVolumeRestrictionTransferManagerFactory: - ${I_VolumeRestrictionTransferManagerFactory.address} - ----------------------------------------------------------------------------- - `); - }); - - describe("Generate the SecurityToken", async () => { - it("Should register the ticker before the generation of the security token", async () => { - await I_PolyToken.approve(I_STRProxied.address, initRegFee, { from: token_owner }); - let tx = await I_STRProxied.registerTicker(token_owner, symbol, contact, { from: token_owner }); - assert.equal(tx.logs[0].args._owner, token_owner); - assert.equal(tx.logs[0].args._ticker, symbol.toUpperCase()); - }); - - it("Should generate the new security token with the same symbol as registered above", async () => { - await I_PolyToken.approve(I_STRProxied.address, initRegFee, { from: token_owner }); - - let tx = await I_STRProxied.generateSecurityToken(name, symbol, tokenDetails, false, { from: token_owner }); - - // Verify the successful generation of the security token - assert.equal(tx.logs[2].args._ticker, symbol.toUpperCase(), "SecurityToken doesn't get deployed"); - - I_SecurityToken = await SecurityToken.at(tx.logs[2].args._securityTokenAddress); - - const log = (await I_SecurityToken.getPastEvents('ModuleAdded', {filter: {transactionHash: tx.transactionHash}}))[0]; - - // Verify that GeneralTransferManager module get added successfully or not - assert.equal(log.args._types[0].toNumber(), 2); - assert.equal(web3.utils.toAscii(log.args._name).replace(/\u0000/g, ""), "GeneralTransferManager"); - }); - - it("Should intialize the auto attached modules", async () => { - let moduleData = (await I_SecurityToken.getModulesByType(2))[0]; - I_GeneralTransferManager = await GeneralTransferManager.at(moduleData); - }); - }); - - describe("Buy tokens using on-chain whitelist and test locking them up and attempting to transfer", async () => { - it("Should Buy the tokens", async () => { - // Add the Investor in to the whitelist - - let tx = await I_GeneralTransferManager.modifyWhitelist( - account_investor1, - currentTime, - currentTime, - currentTime.add(new BN(duration.days(10))), - true, - { - from: account_issuer - } - ); - - assert.equal( - tx.logs[0].args._investor.toLowerCase(), - account_investor1.toLowerCase(), - "Failed in adding the investor in whitelist" - ); - - // Jump time - await increaseTime(5000); - - // Mint some tokens - await I_SecurityToken.mint(account_investor1, new BN(web3.utils.toWei("2", "ether")), { from: token_owner }); - - assert.equal((await I_SecurityToken.balanceOf(account_investor1)).toString(), new BN(web3.utils.toWei("2", "ether")).toString()); - }); - - it("Should Buy some more tokens", async () => { - // Add the Investor in to the whitelist - - let tx = await I_GeneralTransferManager.modifyWhitelist( - account_investor2, - currentTime, - currentTime, - currentTime.add(new BN(duration.days(10))), - true, - { - from: account_issuer - } - ); - - assert.equal( - tx.logs[0].args._investor.toLowerCase(), - account_investor2.toLowerCase(), - "Failed in adding the investor in whitelist" - ); - - // Mint some tokens - await I_SecurityToken.mint(account_investor2, new BN(web3.utils.toWei("10", "ether")), { from: token_owner }); - - assert.equal((await I_SecurityToken.balanceOf(account_investor2)).toString(), new BN(web3.utils.toWei("10", "ether")).toString()); - }); - - it("Should unsuccessfully attach the VolumeRestrictionTransferManager factory with the security token -- failed because Token is not paid", async () => { - await I_PolyToken.getTokens(new BN(web3.utils.toWei("500", "ether")), token_owner); - await catchRevert( - I_SecurityToken.addModule(P_VolumeRestrictionTransferManagerFactory.address, new BN(0), new BN(web3.utils.toWei("500", "ether")), new BN(0), { - from: token_owner - }) - ); - }); - - it("Should successfully attach the VolumeRestrictionTransferManager factory with the security token", async () => { - let snapId = await takeSnapshot(); - await I_PolyToken.transfer(I_SecurityToken.address, new BN(web3.utils.toWei("500", "ether")), { from: token_owner }); - const tx = await I_SecurityToken.addModule( - P_VolumeRestrictionTransferManagerFactory.address, - new BN(0), - new BN(web3.utils.toWei("500", "ether")), - new BN(0), - { from: token_owner } - ); - assert.equal( - tx.logs[3].args._types[0].toNumber(), - transferManagerKey, - "VolumeRestrictionTransferManagerFactory doesn't get deployed" - ); - assert.equal( - web3.utils.toAscii(tx.logs[3].args._name).replace(/\u0000/g, ""), - "LockupVolumeRestrictionTM", - "VolumeRestrictionTransferManagerFactory module was not added" - ); - P_VolumeRestrictionTransferManager = await VolumeRestrictionTransferManager.at(tx.logs[3].args._module); - await revertToSnapshot(snapId); - }); - - it("Should successfully attach the VolumeRestrictionTransferManager with the security token", async () => { - const tx = await I_SecurityToken.addModule(I_VolumeRestrictionTransferManagerFactory.address, new BN(0), new BN(0), new BN(0), { from: token_owner }); - assert.equal(tx.logs[2].args._types[0].toNumber(), transferManagerKey, "VolumeRestrictionTransferManager doesn't get deployed"); - assert.equal( - web3.utils.toAscii(tx.logs[2].args._name).replace(/\u0000/g, ""), - "LockupVolumeRestrictionTM", - "VolumeRestrictionTransferManager module was not added" - ); - I_VolumeRestrictionTransferManager = await VolumeRestrictionTransferManager.at(tx.logs[2].args._module); - }); - - it("Add a new token holder", async () => { - let tx = await I_GeneralTransferManager.modifyWhitelist( - account_investor3, - currentTime, - currentTime, - currentTime.add(new BN(duration.days(10))), - true, - { - from: account_issuer - } - ); - - assert.equal( - tx.logs[0].args._investor.toLowerCase(), - account_investor3.toLowerCase(), - "Failed in adding the investor in whitelist" - ); - - // Add the Investor in to the whitelist - // Mint some tokens - await I_SecurityToken.mint(account_investor3, new BN(web3.utils.toWei("10", "ether")), { from: token_owner }); - - assert.equal((await I_SecurityToken.balanceOf(account_investor3)).toString(), new BN(web3.utils.toWei("10", "ether")).toString()); - }); - - it("Should pause the tranfers at transferManager level", async () => { - let tx = await I_VolumeRestrictionTransferManager.pause({ from: token_owner }); - }); - - it("Should still be able to transfer between existing token holders up to limit", async () => { - // Add the Investor in to the whitelist - // Mint some tokens - await I_SecurityToken.transfer(account_investor1, new BN(web3.utils.toWei("1", "ether")), { from: account_investor2 }); - - assert.equal((await I_SecurityToken.balanceOf(account_investor1)).toString(), new BN(web3.utils.toWei("3", "ether")).toString()); - }); - - it("Should unpause the tranfers at transferManager level", async () => { - await I_VolumeRestrictionTransferManager.unpause({ from: token_owner }); - }); - - it("Should prevent the creation of a lockup with bad parameters where the totalAmount is zero", async () => { - // create a lockup - // this will generate an exception because the totalAmount is zero - await catchRevert(I_VolumeRestrictionTransferManager.addLockUp(account_investor2, 16, 4, new BN(0), new BN(0), { from: token_owner })); - }); - - it("Should prevent the creation of a lockup with bad parameters where the releaseFrequencySeconds is zero", async () => { - // create a lockup - // this will generate an exception because the releaseFrequencySeconds is zero - await catchRevert( - I_VolumeRestrictionTransferManager.addLockUp(account_investor2, 16, new BN(0), new BN(0), new BN(web3.utils.toWei("1", "ether")), { - from: token_owner - }) - ); - }); - - it("Should prevent the creation of a lockup with bad parameters where the lockUpPeriodSeconds is zero", async () => { - // create a lockup - // this will generate an exception because the lockUpPeriodSeconds is zero - await catchRevert( - I_VolumeRestrictionTransferManager.addLockUp(account_investor2, new BN(0), 4, new BN(0), new BN(web3.utils.toWei("1", "ether")), { - from: token_owner - }) - ); - }); - - it("Should prevent the creation of a lockup with bad parameters where the total amount to be released is more granular than allowed by the token", async () => { - // create a lockup - // this will generate an exception because we're locking up 5e17 tokens but the granularity is 5e18 tokens - await catchRevert( - I_VolumeRestrictionTransferManager.addLockUp(account_investor2, 16, 4, new BN(0), new BN(web3.utils.toWei("0.5", "ether")), { - from: token_owner - }) - ); - }); - - it("Should prevent the creation of a lockup with bad parameters where the lockUpPeriodSeconds is not evenly divisible by releaseFrequencySeconds", async () => { - // balance should be 9000000000000000000 here (9 eth) - let balance = await I_SecurityToken.balanceOf(account_investor2); - - // create a lockup - // over 17 seconds total, with 4 periods. - // this will generate an exception because 17 is not evenly divisble by 4. - await catchRevert(I_VolumeRestrictionTransferManager.addLockUp(account_investor2, 17, 4, new BN(0), balance, { from: token_owner })); - }); - - it("Should prevent the creation of a lockup with bad parameters where the total amount being locked up isn't evenly divisible by the number of total periods", async () => { - // create a lockup for a balance of 1 eth - // over 16e18 seconds total, with 4e18 periods of 4 seconds each. - // this will generate an exception because 16e18 / 4e18 = 4e18 but the token granularity is 1e18 and 1e18 % 4e18 != 0 - await catchRevert( - I_VolumeRestrictionTransferManager.addLockUp( - account_investor2, - new BN(web3.utils.toWei("16", "ether")), - 4, - new BN(0), - new BN(web3.utils.toWei("1", "ether")), - { from: token_owner } - ) - ); - }); - - it("Should prevent the creation of a lockup with bad parameters where the amount to be released per period is too granular for the token", async () => { - // balance should be 9000000000000000000 here (9 eth) - let balance = await I_SecurityToken.balanceOf(account_investor2); - - // create a lockup for their entire balance - // over 16 seconds total, with 4 periods of 4 seconds each. - // this will generate an exception because 9000000000000000000 / 4 = 2250000000000000000 but the token granularity is 1000000000000000000 - await catchRevert(I_VolumeRestrictionTransferManager.addLockUp(account_investor2, 16, 4, new BN(0), balance, { from: token_owner })); - }); - - it("Should prevent the transfer of tokens in a lockup", async () => { - let balance = await I_SecurityToken.balanceOf(account_investor2); - console.log("balance", balance.div(new BN(1).mul(new BN(10).pow(new BN(18)))).toNumber()); - // create a lockup for their entire balance - // over 12 seconds total, with 3 periods of 4 seconds each. - await I_VolumeRestrictionTransferManager.addLockUp(account_investor2, 12, 4, new BN(0), balance, { from: token_owner }); - - // read only - check if transfer will pass. it should return INVALID - let result = await I_VolumeRestrictionTransferManager.verifyTransfer.call( - account_investor2, - account_investor1, - new BN(web3.utils.toWei("1", "ether")), - new BN(0), - false - ); - // enum Result {INVALID, NA, VALID, FORCE_VALID} and we want VALID so it should be 2 - assert.equal(result.toString(), "0"); - - await catchRevert(I_SecurityToken.transfer(account_investor1, new BN(web3.utils.toWei("1", "ether")), { from: account_investor2 })); - }); - - it("Should allow the transfer of tokens in a lockup if a period has passed", async () => { - // wait 4 seconds - await increaseTime(duration.seconds(4)); - - await I_SecurityToken.transfer(account_investor1, new BN(web3.utils.toWei("3", "ether")), { from: account_investor2 }); - }); - - it("Should prevent the transfer of tokens if the amount is larger than the amount allowed by lockups", async () => { - await catchRevert(I_SecurityToken.transfer(account_investor1, new BN(web3.utils.toWei("4", "ether")), { from: account_investor2 })); - }); - - it("Should allow the transfer of more tokens in a lockup if another period has passed", async () => { - // wait 4 more seconds - await increaseTime(4000); - - await I_SecurityToken.transfer(account_investor1, new BN(web3.utils.toWei("3", "ether")), { from: account_investor2 }); - }); - - it("Should allow the transfer of all tokens in a lockup if the entire lockup has passed", async () => { - let balance = await I_SecurityToken.balanceOf(account_investor2); - - // wait 4 more seconds - await increaseTime(4000); - - await I_SecurityToken.transfer(account_investor1, balance, { from: account_investor2 }); - }); - - it("Should prevent the transfer of tokens in an edited lockup", async () => { - // balance here should be 12000000000000000000 (12e18 or 12 eth) - let balance = await I_SecurityToken.balanceOf(account_investor1); - - // create a lockup for their entire balance - // over 16 seconds total, with 4 periods of 4 seconds each. - await I_VolumeRestrictionTransferManager.addLockUp(account_investor1, 16, 4, new BN(0), balance, { from: token_owner }); - - // let blockNumber = await web3.eth.getBlockNumber(); - // console.log('blockNumber',blockNumber) - let now = (await web3.eth.getBlock("latest")).timestamp; - - await catchRevert(I_SecurityToken.transfer(account_investor2, new BN(web3.utils.toWei("1", "ether")), { from: account_investor1 })); - - // check and get the lockup - let lockUpCount = await I_VolumeRestrictionTransferManager.getLockUpsLength(account_investor1); - assert.equal(lockUpCount, 1); - - let lockUp = await I_VolumeRestrictionTransferManager.getLockUp(account_investor1, 0); - // console.log(lockUp); - // elements in lockup array are uint lockUpPeriodSeconds, uint releaseFrequencySeconds, uint startTime, uint totalAmount - assert.equal(lockUp[0].toString(), "16"); - assert.equal(lockUp[1].toString(), "4"); - assert.equal(lockUp[2].toNumber(), now); - assert.equal(lockUp[3].toString(), balance.toString()); - - // edit the lockup - await I_VolumeRestrictionTransferManager.modifyLockUp(account_investor1, new BN(0), 8, 4, new BN(0), balance, { from: token_owner }); - - // attempt a transfer - await catchRevert(I_SecurityToken.transfer(account_investor2, new BN(web3.utils.toWei("6", "ether")), { from: account_investor1 })); - - // wait 4 seconds - await new Promise(resolve => setTimeout(resolve, 4000)); - - // transfer should succeed - await I_SecurityToken.transfer(account_investor2, new BN(web3.utils.toWei("6", "ether")), { from: account_investor1 }); - }); - - it("Should succesfully modify the lockup - fail because array index out of bound", async () => { - // balance here should be 12000000000000000000 (12e18 or 12 eth) - let balance = await I_SecurityToken.balanceOf(account_investor1); - await catchRevert( - I_VolumeRestrictionTransferManager.modifyLockUp(account_investor1, 8, 8, 4, new BN(0), balance, { from: token_owner }) - ); - }); - - it("Should succesfully get the lockup - fail because array index out of bound", async () => { - await catchRevert(I_VolumeRestrictionTransferManager.getLockUp(account_investor1, 9)); - }); - - it("Should be possible to remove a lockup -- couldn't transfer because of lock up", async () => { - let acct1Balance = await I_SecurityToken.balanceOf(account_investor1); - - await catchRevert(I_SecurityToken.transfer(account_investor2, acct1Balance, { from: account_investor1 })); - - // check and get the lockup - let lockUpCount = await I_VolumeRestrictionTransferManager.getLockUpsLength(account_investor1); - assert.equal(lockUpCount, 1); - - // remove the lockup - await I_VolumeRestrictionTransferManager.removeLockUp(account_investor1, new BN(0), { from: token_owner }); - - lockUpCount = await I_VolumeRestrictionTransferManager.getLockUpsLength(account_investor1); - assert.equal(lockUpCount, 0); - - let acct2BalanceBefore = await I_SecurityToken.balanceOf(account_investor2); - await I_SecurityToken.transfer(account_investor2, acct1Balance, { from: account_investor1 }); - let acct2BalanceAfter = await I_SecurityToken.balanceOf(account_investor2); - - assert.equal(acct2BalanceAfter.sub(acct2BalanceBefore).toString(), acct1Balance.toString()); - }); - - it("Should try to remove the lockup --failed because of index is out of bounds", async () => { - await catchRevert(I_VolumeRestrictionTransferManager.removeLockUp(account_investor2, 7, { from: token_owner })); - }); - - it("Should be possible to create multiple lockups at once", async () => { - let balancesBefore = {}; - - // should be 12000000000000000000 - balancesBefore[account_investor2] = await I_SecurityToken.balanceOf(account_investor2); - - // should be 10000000000000000000 - balancesBefore[account_investor3] = await I_SecurityToken.balanceOf(account_investor3); - - let lockUpCountsBefore = {}; - - // get lockups for acct 2 - lockUpCountsBefore[account_investor2] = await I_VolumeRestrictionTransferManager.getLockUpsLength(account_investor2); - assert.equal(lockUpCountsBefore[account_investor2], 1); // there's one old, expired lockup on acct already - - // get lockups for acct 3 - lockUpCountsBefore[account_investor3] = await I_VolumeRestrictionTransferManager.getLockUpsLength(account_investor3); - assert.equal(lockUpCountsBefore[account_investor3], 0); - - // create lockups for their entire balances - await I_VolumeRestrictionTransferManager.addLockUpMulti( - [account_investor2, account_investor3], - [24, 8], - [4, 4], - [0, 0], - [balancesBefore[account_investor2], balancesBefore[account_investor3]], - { from: token_owner } - ); - - await catchRevert(I_SecurityToken.transfer(account_investor1, new BN(web3.utils.toWei("2", "ether")), { from: account_investor2 })); - - await catchRevert(I_SecurityToken.transfer(account_investor1, new BN(web3.utils.toWei("5", "ether")), { from: account_investor3 })); - - let balancesAfter = {}; - balancesAfter[account_investor2] = await I_SecurityToken.balanceOf(account_investor2); - assert.equal(balancesBefore[account_investor2].toString(), balancesAfter[account_investor2].toString()); - - balancesAfter[account_investor3] = await I_SecurityToken.balanceOf(account_investor3); - assert.equal(balancesBefore[account_investor3].toString(), balancesAfter[account_investor3].toString()); - - let lockUpCountsAfter = {}; - - // get lockups for acct 2 - lockUpCountsAfter[account_investor2] = await I_VolumeRestrictionTransferManager.getLockUpsLength(account_investor2); - assert.equal(lockUpCountsAfter[account_investor2], 2); - - // get lockups for acct 3 - lockUpCountsAfter[account_investor3] = await I_VolumeRestrictionTransferManager.getLockUpsLength(account_investor3); - assert.equal(lockUpCountsAfter[account_investor3], 1); - - // wait 4 seconds - await increaseTime(4000); - - // try transfers again - await I_SecurityToken.transfer(account_investor1, new BN(web3.utils.toWei("2", "ether")), { from: account_investor2 }); - await I_SecurityToken.transfer(account_investor1, new BN(web3.utils.toWei("5", "ether")), { from: account_investor3 }); - - balancesAfter[account_investor2] = await I_SecurityToken.balanceOf(account_investor2); - assert.equal( - balancesBefore[account_investor2].sub(new BN(web3.utils.toWei("2", "ether"))).toString(), - balancesAfter[account_investor2].toString() - ); - - balancesAfter[account_investor3] = await I_SecurityToken.balanceOf(account_investor3); - assert.equal( - balancesBefore[account_investor3].sub(new BN(web3.utils.toWei("5", "ether"))).toString(), - balancesAfter[account_investor3].toString() - ); - }); - - it("Should revert if the parameters are bad when creating multiple lockups", async () => { - await catchRevert( - // pass in the wrong number of params. txn should revert - I_VolumeRestrictionTransferManager.addLockUpMulti( - [account_investor2, account_investor3], - [16, 8], - [2], // this array should have 2 elements but it has 1, which should cause a revert - [0, 0], - [new BN(web3.utils.toWei("1", "ether")), new BN(web3.utils.toWei("1", "ether"))], - { from: token_owner } - ) - ); - }); - - it("Should be possible to create a lockup with a specific start time in the future", async () => { - // remove all lockups for account 2 - let lockUpsLength = await I_VolumeRestrictionTransferManager.getLockUpsLength(account_investor2); - assert.equal(lockUpsLength, 2); - await I_VolumeRestrictionTransferManager.removeLockUp(account_investor2, new BN(0), { from: token_owner }); - await I_VolumeRestrictionTransferManager.removeLockUp(account_investor2, new BN(0), { from: token_owner }); - lockUpsLength = await I_VolumeRestrictionTransferManager.getLockUpsLength(account_investor2); - assert.equal(lockUpsLength, 0); - - let now = await latestTime(); - - // balance here should be 10000000000000000000 - let balance = await I_SecurityToken.balanceOf(account_investor2); - - await I_VolumeRestrictionTransferManager.addLockUp(account_investor2, 100, 10, now + duration.seconds(4), balance, { - from: token_owner - }); - - // wait 4 seconds for the lockup to begin - await increaseTime(duration.seconds(4)); - - // try another transfer. it should also fail because the lockup has just begun - await catchRevert(I_SecurityToken.transfer(account_investor1, new BN(web3.utils.toWei("1", "ether")), { from: account_investor2 })); - }); - - it("Should be possible to edit a lockup with a specific start time in the future", async () => { - // edit the lockup - let now = await latestTime(); - - // should be 10000000000000000000 - let balance = await I_SecurityToken.balanceOf(account_investor2); - - // check and get the lockup - let lockUpCount = await I_VolumeRestrictionTransferManager.getLockUpsLength(account_investor2); - assert.equal(lockUpCount, 1); - - let lockUp = await I_VolumeRestrictionTransferManager.getLockUp(account_investor2, 0); - - // elements in lockup array are uint lockUpPeriodSeconds, uint releaseFrequencySeconds, uint startTime, uint totalAmount - assert.equal(lockUp[0].toString(), "100"); - assert.equal(lockUp[1].toString(), "10"); - assert.isAtMost(lockUp[2].toNumber(), now); - assert.equal(lockUp[3].toString(), balance.toString()); - - // edit the lockup - await I_VolumeRestrictionTransferManager.modifyLockUp(account_investor2, new BN(0), 8, 4, now + duration.seconds(4), balance, { - from: token_owner - }); - - // check and get the lockup again - lockUpCount = await I_VolumeRestrictionTransferManager.getLockUpsLength(account_investor2); - assert.equal(lockUpCount, 1); - - lockUp = await I_VolumeRestrictionTransferManager.getLockUp(account_investor2, 0); - - // elements in lockup array are uint lockUpPeriodSeconds, uint releaseFrequencySeconds, uint startTime, uint totalAmount - assert.equal(lockUp[0].toString(), "8"); - assert.equal(lockUp[1].toString(), "4"); - assert.isAtMost(lockUp[2].toNumber(), now + 4); - assert.equal(lockUp[3].toString(), balance.toString()); - - // try a transfer. it should fail because again, the lockup hasn't started yet. - await catchRevert(I_SecurityToken.transfer(account_investor1, new BN(web3.utils.toWei("1", "ether")), { from: account_investor2 })); - - // wait 4 seconds for the lockup to begin - await increaseTime(duration.seconds(4)); - - // try another transfer. it should fail because the lockup has just begun - await catchRevert(I_SecurityToken.transfer(account_investor1, new BN(web3.utils.toWei("1", "ether")), { from: account_investor2 })); - - // wait 4 seconds for the lockup's first period to elapse - await increaseTime(duration.seconds(4)); - - // try another transfer. it should pass - await I_SecurityToken.transfer(account_investor1, new BN(web3.utils.toWei("5", "ether")), { from: account_investor2 }); - - // try another transfer without waiting for another period to pass. it should fail - await catchRevert(I_SecurityToken.transfer(account_investor1, new BN(web3.utils.toWei("5", "ether")), { from: account_investor2 })); - - // wait 4 seconds for the lockup's first period to elapse - await increaseTime(duration.seconds(4)); - - let lockUpBeforeVerify = await I_VolumeRestrictionTransferManager.getLockUp(account_investor2, 0); - // check if transfer will pass in read-only operation - let result = await I_VolumeRestrictionTransferManager.verifyTransfer.call( - account_investor2, - account_investor1, - new BN(web3.utils.toWei("5", "ether")), - new BN(0), - false - ); - // enum Result {INVALID, NA, VALID, FORCE_VALID} and we want VALID so it should be 2 - assert.equal(result.toString(), "2"); - let lockUpAfterVerify = await I_VolumeRestrictionTransferManager.getLockUp(account_investor2, 0); - - assert.equal(lockUpBeforeVerify[4].toString(), lockUpAfterVerify[4].toString()); - - // try another transfer. it should pass - await I_SecurityToken.transfer(account_investor1, new BN(web3.utils.toWei("5", "ether")), { from: account_investor2 }); - - // wait 4 seconds for the lockup's first period to elapse. but, we are all out of periods. - await increaseTime(duration.seconds(4)); - - // try one final transfer. this should fail because the user has already withdrawn their entire balance - await catchRevert(I_SecurityToken.transfer(account_investor1, new BN(web3.utils.toWei("1", "ether")), { from: account_investor2 })); - }); - - it("Should be possible to stack lockups", async () => { - // should be 17000000000000000000 - let balance = await I_SecurityToken.balanceOf(account_investor1); - - // check and make sure that acct1 has no lockups so far - let lockUpCount = await I_VolumeRestrictionTransferManager.getLockUpsLength(account_investor1); - assert.equal(lockUpCount.toString(), 0); - - await I_VolumeRestrictionTransferManager.addLockUp(account_investor1, 12, 4, new BN(0), new BN(web3.utils.toWei("6", "ether")), { - from: token_owner - }); - - // try to transfer 11 tokens that aren't locked up yet be locked up. should succeed - await I_SecurityToken.transfer(account_investor2, new BN(web3.utils.toWei("11", "ether")), { from: account_investor1 }); - - // try a transfer. it should fail because it's locked up from the first lockups - await catchRevert(I_SecurityToken.transfer(account_investor2, new BN(web3.utils.toWei("1", "ether")), { from: account_investor1 })); - // wait 4 seconds for the lockup's first period to elapse. - await increaseTime(duration.seconds(4)); - - // should succeed - await I_SecurityToken.transfer(account_investor2, new BN(web3.utils.toWei("2", "ether")), { from: account_investor1 }); - - // send 8 back to investor1 so that we can lock them up - await I_SecurityToken.transfer(account_investor1, new BN(web3.utils.toWei("8", "ether")), { from: account_investor2 }); - - // let's add another lockup to stack them - await I_VolumeRestrictionTransferManager.addLockUp(account_investor1, 16, 4, new BN(0), new BN(web3.utils.toWei("8", "ether")), { - from: token_owner - }); - - // try a transfer. it should fail because it's locked up from both lockups - await catchRevert(I_SecurityToken.transfer(account_investor2, new BN(web3.utils.toWei("1", "ether")), { from: account_investor1 })); - - // wait 4 seconds for the 1st lockup's second period to elapse, and the 2nd lockup's first period to elapse - await increaseTime(duration.seconds(4)); - - // should now be able to transfer 4, because of 2 allowed from the 1st lockup and 2 from the 2nd - await I_SecurityToken.transfer(account_investor2, new BN(web3.utils.toWei("4", "ether")), { from: account_investor1 }); - - // try aother transfer. it should fail because it's locked up from both lockups again - await catchRevert(I_SecurityToken.transfer(account_investor2, new BN(web3.utils.toWei("1", "ether")), { from: account_investor1 })); - - // wait 4 seconds for the 1st lockup's final period to elapse, and the 2nd lockup's second period to elapse - await increaseTime(duration.seconds(4)); - // should now be able to transfer 4, because of 2 allowed from the 1st lockup and 2 from the 2nd - await I_SecurityToken.transfer(account_investor2, new BN(web3.utils.toWei("4", "ether")), { from: account_investor1 }); - - // wait 8 seconds for 2nd lockup's third and fourth periods to elapse - await increaseTime(duration.seconds(8)); - // should now be able to transfer 4, because there are 2 allowed per period in the 2nd lockup, and 2 periods have elapsed - await I_SecurityToken.transfer(account_investor2, new BN(web3.utils.toWei("4", "ether")), { from: account_investor1 }); - - // send the 3 back from acct2 that we sent over in the beginning of this test - await I_SecurityToken.transfer(account_investor1, new BN(web3.utils.toWei("3", "ether")), { from: account_investor2 }); - - // try another transfer. it should pass because both lockups have been entirely used - await I_SecurityToken.transfer(account_investor2, new BN(web3.utils.toWei("1", "ether")), { from: account_investor1 }); - - balance = await I_SecurityToken.balanceOf(account_investor1); - assert.equal(balance.toString(), new BN(web3.utils.toWei("2", "ether"))); - }); - - it("Should get configuration function signature", async () => { - let sig = await I_VolumeRestrictionTransferManager.getInitFunction.call(); - assert.equal(web3.utils.hexToNumber(sig), 0); - }); - - it("Should get the permission", async () => { - let perm = await I_VolumeRestrictionTransferManager.getPermissions.call(); - assert.equal(perm.length, 1); - // console.log(web3.utils.toAscii(perm[0]).replace(/\u0000/g, '')) - assert.equal(web3.utils.toAscii(perm[0]).replace(/\u0000/g, ""), "ADMIN"); - }); - }); - - describe("VolumeRestriction Transfer Manager Factory test cases", async () => { - it("Should get the exact details of the factory", async () => { - assert.equal(await I_VolumeRestrictionTransferManagerFactory.getSetupCost.call(), 0); - assert.equal((await I_VolumeRestrictionTransferManagerFactory.getTypes.call())[0], 2); - assert.equal( - web3.utils.toAscii(await I_VolumeRestrictionTransferManagerFactory.getName.call()).replace(/\u0000/g, ""), - "LockupVolumeRestrictionTM", - "Wrong Module added" - ); - assert.equal( - await I_VolumeRestrictionTransferManagerFactory.description.call(), - "Manage transfers using lock ups over time", - "Wrong Module added" - ); - assert.equal( - await I_VolumeRestrictionTransferManagerFactory.title.call(), - "Lockup Volume Restriction Transfer Manager", - "Wrong Module added" - ); - assert.equal( - await I_VolumeRestrictionTransferManagerFactory.getInstructions.call(), - "Allows an issuer to set lockup periods for user addresses, with funds distributed over time. Init function takes no parameters.", - "Wrong Module added" - ); - assert.equal(await I_VolumeRestrictionTransferManagerFactory.version.call(), "1.0.0"); - }); - - it("Should get the tags of the factory", async () => { - let tags = await I_VolumeRestrictionTransferManagerFactory.getTags.call(); - assert.equal(web3.utils.toAscii(tags[0]).replace(/\u0000/g, ""), "Volume"); - }); - }); -}); diff --git a/test/y_scheduled_checkpoints.js b/test/x_scheduled_checkpoints.js similarity index 100% rename from test/y_scheduled_checkpoints.js rename to test/x_scheduled_checkpoints.js diff --git a/test/y_volume_restriction_tm.js b/test/y_volume_restriction_tm.js new file mode 100644 index 000000000..0b5456a0d --- /dev/null +++ b/test/y_volume_restriction_tm.js @@ -0,0 +1,1693 @@ +import latestTime from './helpers/latestTime'; +import {signData} from './helpers/signData'; +import { pk } from './helpers/testprivateKey'; +import { duration, promisifyLogWatch, latestBlock } from './helpers/utils'; +import { takeSnapshot, increaseTime, revertToSnapshot } from './helpers/time'; +import { catchRevert } from "./helpers/exceptions"; +import { setUpPolymathNetwork, deployVRTMAndVerifyed } from "./helpers/createInstances"; + +const SecurityToken = artifacts.require('./SecurityToken.sol'); +const GeneralTransferManager = artifacts.require('./GeneralTransferManager.sol'); +const VolumeRestrictionTM = artifacts.require('./VolumeRestrictionTM.sol'); + +const Web3 = require('web3'); +const BN = Web3.utils.BN; +const web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545")) // Hardcoded development port + +contract('VolumeRestrictionTransferManager', accounts => { + + // Accounts Variable declaration + let account_polymath; + let account_issuer; + let token_owner; + let token_owner_pk; + let account_investor1; + let account_investor2; + let account_investor3; + let account_investor4; + let account_delegate; + let account_delegate2; + let account_delegate3; + // investor Details + let fromTime; + let toTime; + let expiryTime; + + let message = "Transaction Should Fail!"; + + // Contract Instance Declaration + let I_VolumeRestrictionTMFactory; + let P_VolumeRestrictionTMFactory; + let I_SecurityTokenRegistryProxy; + let P_VolumeRestrictionTM; + let I_GeneralTransferManagerFactory; + let I_VolumeRestrictionTM; + let I_GeneralTransferManager; + let I_ModuleRegistryProxy; + let I_ModuleRegistry; + let I_FeatureRegistry; + let I_SecurityTokenRegistry; + let I_DummySTOFactory; + let I_STFactory; + let I_SecurityToken; + let I_MRProxied; + let I_STRProxied; + let I_PolyToken; + let I_PolymathRegistry; + + // SecurityToken Details + const name = "Team"; + const symbol = "sap"; + const tokenDetails = "This is equity type of issuance"; + const decimals = 18; + const contact = "team@polymath.network"; + const delegateDetails = web3.utils.toHex("Hello I am legit delegate"); + + // Module key + const delegateManagerKey = 1; + const transferManagerKey = 2; + const stoKey = 3; + + let tempAmount = new BN(0); + let tempArray = new Array(); + let tempArray3 = new Array(); + let tempArrayGlobal = new Array(); + + // Initial fee for ticker registry and security token registry + const initRegFee = new BN(web3.utils.toWei("250")); + + const address_zero = "0x0000000000000000000000000000000000000000"; + + async function print(data, account) { + console.log(` + Latest timestamp: ${data[0].toString()} + SumOfLastPeriod: ${web3.utils.fromWei(data[1]).toString()} + Days Covered: ${data[2].toString()} + Latest timestamp daily: ${data[3].toString()} + Individual Total Trade on latestTimestamp : ${web3.utils.fromWei(await I_VolumeRestrictionTM.getTotalTradedByUser.call(account, data[0])) + .toString()} + Individual Total Trade on daily latestTimestamp : ${web3.utils.fromWei(await I_VolumeRestrictionTM.getTotalTradedByUser.call(account, data[3])) + .toString()} + `) + } + + async function getLatestTime() { + return new BN(await latestTime()); + } + + async function printRestrictedData(data) { + let investors = data[0]; + for (let i = 0 ; i < investors.length; i++) { + console.log(` + Token holder: ${data[0][i]} + Start Time: ${data[2][i].toString()} + Rolling Period In Days: ${data[3][i].toString()} + End Time : ${data[4][i].toString()} + Allowed Tokens: ${web3.utils.fromWei(data[1][i].toString())} + Type of Restriction: ${data[5][i].toString()} + `) + } + } + + async function calculateSum(rollingPeriod, tempArray) { + let sum = 0; + let start = 0; + if (tempArray.length >= rollingPeriod) + start = tempArray.length - rollingPeriod; + for (let i = start; i < tempArray.length; i++) { + sum += tempArray[i]; + } + return sum; + } + + before(async() => { + let newLatestTime = await getLatestTime(); + fromTime = newLatestTime; + toTime = newLatestTime; + expiryTime = toTime.add(new BN(duration.days(15))); + // Accounts setup + account_polymath = accounts[0]; + account_issuer = accounts[1]; + + token_owner = account_issuer; + token_owner_pk = pk.account_1; + + account_investor1 = accounts[8]; + account_investor2 = accounts[9]; + account_investor3 = accounts[4]; + account_investor4 = accounts[3]; + account_delegate = accounts[7]; + account_delegate2 = accounts[6]; + account_delegate3 = accounts[5]; + + // Step 1: Deploy the genral PM ecosystem + let instances = await setUpPolymathNetwork(account_polymath, token_owner); + + [ + I_PolymathRegistry, + I_PolyToken, + I_FeatureRegistry, + I_ModuleRegistry, + I_ModuleRegistryProxy, + I_MRProxied, + I_GeneralTransferManagerFactory, + I_STFactory, + I_SecurityTokenRegistry, + I_SecurityTokenRegistryProxy, + I_STRProxied + ] = instances; + + // STEP 5: Deploy the VolumeRestrictionTMFactory + [I_VolumeRestrictionTMFactory] = await deployVRTMAndVerifyed(account_polymath, I_MRProxied, 0); + // STEP 6: Deploy the VolumeRestrictionTMFactory + [P_VolumeRestrictionTMFactory] = await deployVRTMAndVerifyed(account_polymath, I_MRProxied, new BN(web3.utils.toWei("500"))); + + // Printing all the contract addresses + console.log(` + --------------------- Polymath Network Smart Contracts: --------------------- + PolymathRegistry: ${I_PolymathRegistry.address} + SecurityTokenRegistryProxy: ${I_SecurityTokenRegistryProxy.address} + SecurityTokenRegistry: ${I_SecurityTokenRegistry.address} + ModuleRegistryProxy ${I_ModuleRegistryProxy.address} + ModuleRegistry: ${I_ModuleRegistry.address} + FeatureRegistry: ${I_FeatureRegistry.address} + + STFactory: ${I_STFactory.address} + GeneralTransferManagerFactory: ${I_GeneralTransferManagerFactory.address} + VolumeRestrictionTMFactory: ${I_VolumeRestrictionTMFactory.address} + ----------------------------------------------------------------------------- + `); + }); + + describe("Generate the SecurityToken", async () => { + it("Should register the ticker before the generation of the security token", async () => { + await I_PolyToken.approve(I_STRProxied.address, initRegFee, { from: token_owner }); + let tx = await I_STRProxied.registerTicker(token_owner, symbol, contact, { from: token_owner }); + assert.equal(tx.logs[0].args._owner, token_owner); + assert.equal(tx.logs[0].args._ticker, symbol.toUpperCase()); + }); + + it("Should generate the new security token with the same symbol as registered above", async () => { + await I_PolyToken.approve(I_STRProxied.address, initRegFee, { from: token_owner }); + + let tx = await I_STRProxied.generateSecurityToken(name, symbol, tokenDetails, true, { from: token_owner }); + + // Verify the successful generation of the security token + assert.equal(tx.logs[2].args._ticker, symbol.toUpperCase(), "SecurityToken doesn't get deployed"); + + I_SecurityToken = await SecurityToken.at(tx.logs[2].args._securityTokenAddress); + + const log = (await I_SecurityToken.getPastEvents('ModuleAdded', {filter: {transactionHash: tx.transactionHash}}))[0]; + + // Verify that GeneralTransferManager module get added successfully or not + assert.equal(log.args._types[0].toString(), 2); + assert.equal(web3.utils.toAscii(log.args._name).replace(/\u0000/g, ""), "GeneralTransferManager"); + }); + + it("Should intialize the auto attached modules", async () => { + let moduleData = (await I_SecurityToken.getModulesByType(2))[0]; + I_GeneralTransferManager = await GeneralTransferManager.at(moduleData); + }); + }); + + describe("Attach the VRTM", async() => { + it("Deploy the VRTM and attach with the ST", async()=> { + let tx = await I_SecurityToken.addModule(I_VolumeRestrictionTMFactory.address, "0x0", new BN(0), new BN(0), {from: token_owner }); + assert.equal(tx.logs[2].args._moduleFactory, I_VolumeRestrictionTMFactory.address); + assert.equal( + web3.utils.toUtf8(tx.logs[2].args._name), + "VolumeRestrictionTM", + "VolumeRestrictionTMFactory doesn not added"); + I_VolumeRestrictionTM = await VolumeRestrictionTM.at(tx.logs[2].args._module); + }); + + it("Transfer some tokens to different account", async() => { + // Add tokens in to the whitelist + let newLatestTime = await getLatestTime(); + await I_GeneralTransferManager.modifyWhitelistMulti( + [account_investor1, account_investor2, account_investor3], + [newLatestTime, newLatestTime, newLatestTime], + [newLatestTime, newLatestTime, newLatestTime], + [newLatestTime.add(new BN(duration.days(60))), newLatestTime.add(new BN(duration.days(60))), newLatestTime.add(new BN(duration.days(60)))], + [true, true, true], + { + from: token_owner + } + ); + + // Mint some tokens and transferred to whitelisted addresses + await I_SecurityToken.mint(account_investor1, new BN(web3.utils.toWei("40", "ether")), {from: token_owner}); + await I_SecurityToken.mint(account_investor2, new BN(web3.utils.toWei("30", "ether")), {from: token_owner}); + await I_SecurityToken.mint(account_investor3, new BN(web3.utils.toWei("30", "ether")), {from: token_owner}); + + // Check the balance of the investors + let bal1 = await I_SecurityToken.balanceOf.call(account_investor1); + let bal2 = await I_SecurityToken.balanceOf.call(account_investor2); + // Verifying the balances + assert.equal(web3.utils.fromWei((bal1.toString()).toString()), 40); + assert.equal(web3.utils.fromWei((bal2.toString()).toString()), 30); + + }); + + it("Should transfer the tokens freely without any restriction", async() => { + await I_SecurityToken.transfer(account_investor3, new BN(web3.utils.toWei('5', 'ether')), { from: account_investor1 }); + let bal1 = await I_SecurityToken.balanceOf.call(account_investor3); + // Verifying the balances + assert.equal(web3.utils.fromWei((bal1.toString()).toString()), 35); + }); + }) + + describe("Test for the addIndividualRestriction", async() => { + it("Should add the restriction -- failed because of bad owner", async() => { + let newLatestTime = await getLatestTime(); + await catchRevert( + I_VolumeRestrictionTM.addIndividualRestriction( + account_investor1, + new BN(web3.utils.toWei("12")), + newLatestTime.add(new BN(duration.seconds(2))), + 3, + newLatestTime.add(new BN(duration.days(10))), + 0, + { + from: account_polymath + } + ) + ); + }) + + it("Should add the restriction -- failed because of bad parameters i.e invalid restriction type", async() => { + let newLatestTime = await getLatestTime(); + + await catchRevert( + I_VolumeRestrictionTM.addIndividualRestriction( + account_investor1, + new BN(web3.utils.toWei("12")), + newLatestTime.add(new BN(duration.seconds(2))), + 3, + newLatestTime.add(new BN(duration.days(10))), + 3, + { + from: token_owner + } + ) + ); + }) + + it("Should add the restriction -- failed because of bad parameters i.e Invalid value of allowed tokens", async() => { + let newLatestTime = await getLatestTime(); + await catchRevert( + I_VolumeRestrictionTM.addIndividualRestriction( + account_investor1, + 0, + newLatestTime.add(new BN(duration.seconds(2))), + 3, + newLatestTime.add(new BN(duration.days(10))), + 0, + { + from: token_owner + } + ) + ); + }) + + it("Should add the restriction -- failed because of bad parameters i.e Percentage of tokens not within (0,100]", async() => { + let newLatestTime = await getLatestTime(); + + await catchRevert( + I_VolumeRestrictionTM.addIndividualRestriction( + account_investor1, + 0, + newLatestTime.add(new BN(duration.seconds(2))), + 3, + newLatestTime.add(new BN(duration.days(10))), + 1, + { + from: token_owner + } + ) + ); + }) + + it("Should add the restriction -- failed because of bad parameters i.e Percentage of tokens not within (0,100]", async() => { + let newLatestTime = await getLatestTime(); + await catchRevert( + I_VolumeRestrictionTM.addIndividualRestriction( + account_investor1, + new BN(web3.utils.toWei("10")), + newLatestTime.add(new BN(duration.seconds(2))), + 3, + newLatestTime.add(new BN(duration.days(10))), + 1, + { + from: token_owner + } + ) + ); + }) + + it("Should add the restriction -- failed because of bad parameters i.e invalid dates", async() => { + let newLatestTime = await getLatestTime(); + await catchRevert( + I_VolumeRestrictionTM.addIndividualRestriction( + account_investor1, + new BN(web3.utils.toWei("10")), + newLatestTime.sub(new BN(duration.seconds(5))), + 3, + newLatestTime.add(new BN(duration.days(10))), + 0, + { + from: token_owner + } + ) + ); + }) + + it("Should add the restriction -- failed because of bad parameters i.e invalid dates", async() => { + let newLatestTime = await getLatestTime(); + await catchRevert( + I_VolumeRestrictionTM.addIndividualRestriction( + account_investor1, + new BN(web3.utils.toWei("10")), + newLatestTime.add(new BN(duration.days(2))), + 3, + newLatestTime.add(new BN(duration.days(1))), + 0, + { + from: token_owner + } + ) + ); + }); + + it("Should add the restriction -- failed because of bad parameters i.e invalid rolling period", async() => { + let newLatestTime = await getLatestTime(); + await catchRevert( + I_VolumeRestrictionTM.addIndividualRestriction( + account_investor1, + new BN(web3.utils.toWei("10")), + newLatestTime.add(new BN(duration.days(2))), + 0, + newLatestTime.add(new BN(duration.days(10))), + 0, + { + from: token_owner + } + ) + ); + }); + + it("Should add the restriction -- failed because of bad parameters i.e invalid rolling period", async() => { + let newLatestTime = await getLatestTime(); + await catchRevert( + I_VolumeRestrictionTM.addIndividualRestriction( + account_investor1, + new BN(web3.utils.toWei("10")), + newLatestTime.add(new BN(duration.days(2))), + 366, + newLatestTime.add(new BN(duration.days(10))), + 0, + { + from: token_owner + } + ) + ); + }); + + it("Should add the restriction -- failed because of bad parameters i.e invalid rolling period", async() => { + let newLatestTime = await getLatestTime(); + await catchRevert( + I_VolumeRestrictionTM.addIndividualRestriction( + account_investor1, + new BN(web3.utils.toWei("10")), + newLatestTime.add(new BN(duration.days(2))), + 3, + newLatestTime.add(new BN(duration.days(3))), + 0, + { + from: token_owner + } + ) + ); + }); + + it("Should add the restriction succesfully", async() => { + let newLatestTime = await getLatestTime(); + let tx = await I_VolumeRestrictionTM.addIndividualRestriction( + account_investor1, + new BN(web3.utils.toWei("12")), + newLatestTime.add(new BN(duration.seconds(2))), + 3, + newLatestTime.add(new BN(duration.days(5))), + 0, + { + from: token_owner + } + ); + assert.equal(tx.logs[0].args._holder, account_investor1); + assert.equal(tx.logs[0].args._typeOfRestriction, 0); + let data = await I_VolumeRestrictionTM.getRestrictionData.call(); + await printRestrictedData(data); + assert.equal(data[0][0], account_investor1); + }); + + it("Should add the restriction for multiple investor -- failed because of bad owner", async() => { + let newLatestTime = await getLatestTime(); + await catchRevert( + I_VolumeRestrictionTM.addIndividualRestrictionMulti( + [account_investor2, account_delegate3, account_investor4], + [new BN(web3.utils.toWei("12")), new BN(web3.utils.toWei("10")), new BN(web3.utils.toWei("15"))], + [newLatestTime.add(new BN(duration.seconds(2))), newLatestTime.add(new BN(duration.seconds(2))), newLatestTime.add(new BN(duration.seconds(2)))], + [3,4,5], + [newLatestTime.add(new BN(duration.days(5))), newLatestTime.add(new BN(duration.days(6))), newLatestTime.add(new BN(duration.days(7)))], + [0,0,0], + { + from: account_polymath + } + ) + ) + }); + + it("Should add the restriction for multiple investor -- failed because of bad parameters i.e length mismatch", async() => { + let newLatestTime = await getLatestTime(); + await catchRevert( + I_VolumeRestrictionTM.addIndividualRestrictionMulti( + [account_investor2, account_delegate3], + [new BN(web3.utils.toWei("12")), new BN(web3.utils.toWei("10")), new BN(web3.utils.toWei("15"))], + [newLatestTime.add(new BN(duration.seconds(2))), newLatestTime.add(new BN(duration.seconds(2))), newLatestTime.add(new BN(duration.seconds(2)))], + [3,4,5], + [newLatestTime.add(new BN(duration.days(5))), newLatestTime.add(new BN(duration.days(6))), newLatestTime.add(new BN(duration.days(7)))], + [0,0,0], + { + from: token_owner + } + ) + ) + }); + + it("Should add the restriction for multiple investor -- failed because of bad parameters i.e length mismatch", async() => { + let newLatestTime = await getLatestTime(); + await catchRevert( + I_VolumeRestrictionTM.addIndividualRestrictionMulti( + [account_investor2, account_delegate3, account_investor4], + [new BN(web3.utils.toWei("12")), new BN(web3.utils.toWei("10"))], + [newLatestTime.add(new BN(duration.seconds(2))), newLatestTime.add(new BN(duration.seconds(2))), newLatestTime.add(new BN(duration.seconds(2)))], + [3,4,5], + [newLatestTime.add(new BN(duration.days(5))), newLatestTime.add(new BN(duration.days(6))), newLatestTime.add(new BN(duration.days(7)))], + [0,0,0], + { + from: account_polymath + } + ) + ) + }); + + it("Should add the restriction for multiple investor -- failed because of bad parameters i.e length mismatch", async() => { + let newLatestTime = await getLatestTime(); + await catchRevert( + I_VolumeRestrictionTM.addIndividualRestrictionMulti( + [account_investor2, account_delegate3, account_investor4], + [new BN(web3.utils.toWei("12")), new BN(web3.utils.toWei("10")), new BN(web3.utils.toWei("15"))], + [newLatestTime.add(new BN(duration.seconds(2))), newLatestTime.add(new BN(duration.seconds(2)))], + [3,4,5], + [newLatestTime.add(new BN(duration.days(5))), newLatestTime.add(new BN(duration.days(6))), newLatestTime.add(new BN(duration.days(7)))], + [0,0,0], + { + from: token_owner + } + ) + ) + }); + + it("Should add the restriction for multiple investor -- failed because of bad parameters i.e length mismatch", async() => { + let newLatestTime = await getLatestTime(); + await catchRevert( + I_VolumeRestrictionTM.addIndividualRestrictionMulti( + [account_investor2, account_delegate3, account_investor4], + [new BN(web3.utils.toWei("12")), new BN(web3.utils.toWei("10")), new BN(web3.utils.toWei("15"))], + [newLatestTime.add(new BN(duration.seconds(2))), newLatestTime.add(new BN(duration.seconds(2))), newLatestTime.add(new BN(duration.seconds(2)))], + [3], + [newLatestTime.add(new BN(duration.days(5))), newLatestTime.add(new BN(duration.days(6))), newLatestTime.add(new BN(duration.days(7)))], + [0,0,0], + { + from: token_owner + } + ) + ) + }); + + it("Should add the restriction for multiple investor -- failed because of bad parameters i.e length mismatch", async() => { + let newLatestTime = await getLatestTime(); + await catchRevert( + I_VolumeRestrictionTM.addIndividualRestrictionMulti( + [account_investor2, account_delegate3, account_investor4], + [new BN(web3.utils.toWei("12")), new BN(web3.utils.toWei("10")), new BN(web3.utils.toWei("15"))], + [newLatestTime.add(new BN(duration.seconds(2))), newLatestTime.add(new BN(duration.seconds(2))), newLatestTime.add(new BN(duration.seconds(2)))], + [3, 4, 5], + [newLatestTime.add(new BN(duration.days(5)))], + [0,0,0], + { + from: token_owner + } + ) + ) + }); + + it("Should add the restriction for multiple investor -- failed because of bad parameters i.e length mismatch", async() => { + let newLatestTime = await getLatestTime(); + await catchRevert( + I_VolumeRestrictionTM.addIndividualRestrictionMulti( + [account_investor2, account_delegate3, account_investor4], + [new BN(web3.utils.toWei("12")), new BN(web3.utils.toWei("10")), new BN(web3.utils.toWei("15"))], + [newLatestTime.add(new BN(duration.seconds(2))), newLatestTime.add(new BN(duration.seconds(2))), newLatestTime.add(new BN(duration.seconds(2)))], + [3, 4, 5], + [newLatestTime.add(new BN(duration.days(5))), newLatestTime.add(new BN(duration.days(6))), newLatestTime.add(new BN(duration.days(7)))], + [], + { + from: token_owner + } + ) + ) + }); + + it("Should add the restriction for multiple investor successfully", async() => { + let newLatestTime = await getLatestTime(); + await I_VolumeRestrictionTM.addIndividualRestrictionMulti( + [account_investor2, account_delegate3, account_investor4], + [new BN(web3.utils.toWei("12")), new BN(web3.utils.toWei("10")), new BN(web3.utils.toWei("15"))], + [0, 0, 0], + [3, 4, 5], + [newLatestTime.add(new BN(duration.days(5))), newLatestTime.add(new BN(duration.days(6))), newLatestTime.add(new BN(duration.days(7)))], + [0,0,0], + { + from: token_owner + } + ); + assert.equal((await I_VolumeRestrictionTM.getIndividualRestriction.call(account_investor2))[2].toString(), 3); + assert.equal((await I_VolumeRestrictionTM.getIndividualRestriction.call(account_delegate3))[2].toString(), 4); + assert.equal((await I_VolumeRestrictionTM.getIndividualRestriction.call(account_investor4))[2].toString(), 5); + + let data = await I_VolumeRestrictionTM.getRestrictionData.call(); + await printRestrictedData(data); + assert.equal(data[0].length, 4); + }); + + it("Should remove the restriction multi -- failed because of address is 0", async() => { + await catchRevert( + I_VolumeRestrictionTM.removeIndividualRestrictionMulti( + [address_zero, account_delegate3, account_investor4], + { + from: token_owner + } + ) + ); + }); + + it("Should successfully remove the restriction", async() => { + await I_VolumeRestrictionTM.removeIndividualRestriction(account_investor2, {from: token_owner}); + assert.equal((await I_VolumeRestrictionTM.getIndividualRestriction.call(account_investor2))[3].toString(), 0); + let data = await I_VolumeRestrictionTM.getRestrictionData.call(); + await printRestrictedData(data); + assert.equal(data[0].length, 3); + for (let i = 0; i < data[0].length; i++) { + assert.notEqual(data[0][i], account_investor2); + } + }); + + it("Should remove the restriction -- failed because restriction not present anymore", async() => { + await catchRevert( + I_VolumeRestrictionTM.removeIndividualRestriction(account_investor2, {from: token_owner}) + ); + }); + + it("Should remove the restriction multi", async() => { + await I_VolumeRestrictionTM.removeIndividualRestrictionMulti( + [account_delegate3, account_investor4], + { + from: token_owner + } + ) + let data = await I_VolumeRestrictionTM.getRestrictionData.call(); + await printRestrictedData(data); + assert.equal(data[0].length, 1); + }); + + it("Should add the restriction succesfully after the expiry of previous one for investor 1", async() => { + await increaseTime(duration.days(5.1)); + let newLatestTime = await getLatestTime(); + console.log( + `Estimated gas for addIndividualRestriction: + ${await I_VolumeRestrictionTM.addIndividualRestriction.estimateGas( + account_investor1, + new BN(web3.utils.toWei("12")), + newLatestTime.add(new BN(duration.seconds(2))), + 3, + newLatestTime.add(new BN(duration.days(6))), + 0, + { + from: token_owner + } + )} + `); + newLatestTime = await getLatestTime(); + let tx = await I_VolumeRestrictionTM.addIndividualRestriction( + account_investor1, + new BN(web3.utils.toWei("12")), + newLatestTime.add(new BN(duration.seconds(2))), + 3, + newLatestTime.add(new BN(duration.days(6))), + 0, + { + from: token_owner + } + ); + + assert.equal(tx.logs[1].args._holder, account_investor1); + assert.equal(tx.logs[1].args._typeOfRestriction, 0); + let data = await I_VolumeRestrictionTM.getRestrictionData.call(); + await printRestrictedData(data); + assert.equal(data[0].length, 1); + assert.equal(data[0][0], account_investor1); + }); + + it("Should not successfully transact the tokens -- failed because volume is above the limit", async() => { + await increaseTime(duration.seconds(10)); + await catchRevert( + I_SecurityToken.transfer(account_investor3, new BN(web3.utils.toWei("13")), { from: account_investor1}) + ); + }); + + it("Should succesfully transact the tokens by investor 1 just after the startTime", async() => { + // Check the transfer will be valid or not by calling the verifyTransfer() directly by using _isTransfer = false + let result = await I_VolumeRestrictionTM.verifyTransfer.call(account_investor1, account_investor3, new BN(web3.utils.toWei('.3', "ether")), "0x0", false); + assert.equal(result.toString(), 1); + // Perform the transaction + console.log(` + Gas estimation (Individual): ${await I_SecurityToken.transfer.estimateGas(account_investor3, new BN(web3.utils.toWei('.3', "ether")), {from: account_investor1})}` + ); + await I_SecurityToken.transfer(account_investor3, new BN(web3.utils.toWei('.3')), {from: account_investor1}); + // Check the balance of the investors + let bal1 = await I_SecurityToken.balanceOf.call(account_investor1); + // Verifying the balances + assert.equal(web3.utils.fromWei((bal1.toString()).toString()), 34.7); + + let data = await I_VolumeRestrictionTM.getIndividualBucketDetailsToUser.call(account_investor1); + await print(data, account_investor1); + assert.equal( + web3.utils.fromWei(await I_VolumeRestrictionTM.getTotalTradedByUser.call(account_investor1, data[0])), + 0.3 + ); + assert.equal( + data[0].toString(), + (await I_VolumeRestrictionTM.getIndividualRestriction.call(account_investor1))[1].toString() + ); + assert.equal(web3.utils.fromWei(data[1].toString()), 0.3); + tempArray.push(0.3); + }); + + it("Should fail to add the individual daily restriction -- Bad msg.sender", async() => { + let newLatestTime = await getLatestTime(); + await catchRevert( + I_VolumeRestrictionTM.addIndividualDailyRestriction( + account_investor3, + new BN(web3.utils.toWei("6")), + newLatestTime.add(new BN(duration.seconds(1))), + newLatestTime.add(new BN(duration.days(4))), + 0, + { + from: account_investor1 + } + ) + ); + }) + + it("Should fail to add the individual daily restriction -- Bad params value", async() => { + let newLatestTime = await getLatestTime(); + await catchRevert( + I_VolumeRestrictionTM.addIndividualDailyRestriction( + account_investor3, + new BN(web3.utils.toWei("6")), + newLatestTime.add(new BN(duration.seconds(1))), + newLatestTime.add(new BN(duration.days(4))), + 1, + { + from: token_owner + } + ) + ); + }) + + it("Should fail to add the individual daily restriction -- Bad params value", async() => { + let newLatestTime = await getLatestTime(); + await catchRevert( + I_VolumeRestrictionTM.addIndividualDailyRestriction( + account_investor3, + 0, + newLatestTime.add(new BN(duration.seconds(1))), + newLatestTime.add(new BN(duration.days(4))), + 0, + { + from: token_owner + } + ) + ); + }) + + it("Should fail to add the individual daily restriction -- Bad params value", async() => { + let newLatestTime = await getLatestTime(); + await catchRevert( + I_VolumeRestrictionTM.addIndividualDailyRestriction( + account_investor3, + new BN(web3.utils.toWei("6")), + newLatestTime.add(new BN(duration.days(5))), + newLatestTime.add(new BN(duration.days(4))), + 0, + { + from: token_owner + } + ) + ); + }) + + it("Should add the individual daily restriction for investor 3", async() => { + let newLatestTime = await getLatestTime(); + let tx = await I_VolumeRestrictionTM.addIndividualDailyRestriction( + account_investor3, + new BN(web3.utils.toWei("6")), + newLatestTime.add(new BN(duration.seconds(1))), + newLatestTime.add(new BN(duration.days(4))), + 0, + { + from: token_owner + } + ); + + assert.equal(tx.logs[0].args._holder, account_investor3); + assert.equal(tx.logs[0].args._typeOfRestriction, 0); + assert.equal((tx.logs[0].args._allowedTokens).toString(), new BN(web3.utils.toWei("6"))); + let data = await I_VolumeRestrictionTM.getRestrictionData.call(); + await printRestrictedData(data); + assert.equal(data[0].length, 2); + assert.equal(data[0][1], account_investor3); + let dataRestriction = await I_VolumeRestrictionTM.getIndividualDailyRestriction.call(account_investor3); + console.log(` + *** Individual Daily restriction data *** + Allowed Tokens: ${dataRestriction[0].div(new BN(10).pow(new BN(18))).toString()} + StartTime : ${dataRestriction[1].toString()} + Rolling Period in days : ${dataRestriction[2].toString()} + EndTime : ${dataRestriction[3].toString()} + Type of Restriction: ${dataRestriction[4].toString()} + `); + }); + + it("Should transfer the tokens within the individual daily restriction limits", async() => { + // transfer 2 tokens as per the limit + await increaseTime(5); // increase 5 seconds to layoff the time gap + let startTime = (await I_VolumeRestrictionTM.getIndividualDailyRestriction.call(account_investor3))[1].toString(); + console.log(` + Gas Estimation for the Individual daily tx - ${await I_SecurityToken.transfer.estimateGas(account_investor2, new BN(web3.utils.toWei("2")), {from: account_investor3})} + `) + await I_SecurityToken.transfer(account_investor2, new BN(web3.utils.toWei("2")), {from: account_investor3}); + let data = await I_VolumeRestrictionTM.getIndividualBucketDetailsToUser.call(account_investor3); + await print(data, account_investor3); + + await increaseTime(duration.minutes(15)); + + console.log(` + Gas Estimation for the Individual daily tx - ${await I_SecurityToken.transfer.estimateGas(account_investor2, new BN(web3.utils.toWei("4")), {from: account_investor3})} + `) + // transfer the 4 tokens which is under the limit + await I_SecurityToken.transfer(account_investor2, new BN(web3.utils.toWei("4")), {from: account_investor3}); + let newData = await I_VolumeRestrictionTM.getIndividualBucketDetailsToUser.call(account_investor3); + await print(newData, account_investor3); + + assert.equal(newData[3].toString(), data[3].toString()); + assert.equal(data[3].toString(), startTime); + assert.equal(web3.utils.fromWei(await I_VolumeRestrictionTM.getTotalTradedByUser.call(account_investor3, data[3])) + , 6); + }); + + it("Should fail to transfer more tokens --because of the above limit", async() => { + await catchRevert( + I_SecurityToken.transfer(account_investor2, new BN(web3.utils.toWei(".1")), {from: account_investor3}) + ); + }); + + it("Should try to send after the one day completion", async() => { + // increase the EVM time by one day + await increaseTime(duration.days(1)); + + let startTime = (await I_VolumeRestrictionTM.getIndividualDailyRestriction.call(account_investor3))[1].toString(); + console.log(` + Gas Estimation for the Individual daily tx - ${await I_SecurityToken.transfer.estimateGas(account_investor2, new BN(web3.utils.toWei("2")), {from: account_investor3})} + `) + await I_SecurityToken.transfer(account_investor2, new BN(web3.utils.toWei("2")), {from: account_investor3}); + let data = await I_VolumeRestrictionTM.getIndividualBucketDetailsToUser.call(account_investor3); + await print(data, account_investor3); + + assert.equal(data[3].toString(), new BN(startTime).add(new BN(duration.days(1)))); + assert.equal(web3.utils.fromWei(await I_VolumeRestrictionTM.getTotalTradedByUser.call(account_investor3, data[3])) + , 2); + }); + + it("Should add the daily restriction on the investor 1", async() => { + let newLatestTime = await getLatestTime(); + let tx = await I_VolumeRestrictionTM.addIndividualDailyRestriction( + account_investor1, + new BN(5).mul(new BN(10).pow(new BN(16))), + 0, + newLatestTime.add(new BN(duration.days(4))), + 1, + { + from: token_owner + } + ); + + assert.equal(tx.logs[0].args._holder, account_investor1); + assert.equal((tx.logs[0].args._typeOfRestriction).toString(), 1); + assert.equal(web3.utils.fromWei(new BN(tx.logs[0].args._allowedTokens)), 0.05); + let data = await I_VolumeRestrictionTM.getRestrictionData.call(); + await printRestrictedData(data); + assert.equal(data[0].length, 3); + assert.equal(data[0][2], account_investor3); + assert.equal(data[0][0], account_investor1); + let dataRestriction = await I_VolumeRestrictionTM.getIndividualDailyRestriction.call(account_investor1); + console.log(` + *** Individual Daily restriction data *** + Allowed Tokens: ${dataRestriction[0].div(new BN(10).pow(new BN(16))).toString()} % of TotalSupply + StartTime : ${dataRestriction[1].toString()} + Rolling Period in days : ${dataRestriction[2].toString()} + EndTime : ${dataRestriction[3].toString()} + Type of Restriction: ${dataRestriction[4].toString()} + `); + }); + + it("Should transfer tokens on the 2nd day by investor1 (Individual + Individual daily)", async() => { + let startTime = (await I_VolumeRestrictionTM.getIndividualRestriction.call(account_investor1))[1].toString(); + let rollingPeriod = (await I_VolumeRestrictionTM.getIndividualRestriction.call(account_investor1))[2].toString(); + + console.log(` + Gas estimation (Individual + Individual daily): ${await I_SecurityToken.transfer.estimateGas(account_investor2, new BN(web3.utils.toWei("2")), {from: account_investor1})}` + ); + + await I_SecurityToken.transfer(account_investor2, new BN(web3.utils.toWei("2")), {from: account_investor1}); + // Check the balance of the investors + let bal1 = await I_SecurityToken.balanceOf.call(account_investor1); + // Verifying the balances + assert.equal(web3.utils.fromWei(bal1.toString()), 32.7); + tempArray.push(2); + + let data = await I_VolumeRestrictionTM.getIndividualBucketDetailsToUser.call(account_investor1); + await print(data, account_investor1); + + // get the trade amount using the timestamp + let amt = web3.utils.fromWei(await I_VolumeRestrictionTM.getTotalTradedByUser.call(account_investor1, data[0])); + // Verify the storage changes + assert.equal(data[0].toString(), new BN(startTime).add(new BN(duration.days(data[2].toString())))); + assert.equal(web3.utils.fromWei(data[1]), await calculateSum(rollingPeriod, tempArray)); + assert.equal(data[2].toString(), 1); + assert.equal(data[3].toString(), + (await I_VolumeRestrictionTM.getIndividualDailyRestriction.call(account_investor1))[1].toString()); + assert.equal(amt, 2); + }); + + it("Should fail to transfer by investor 1 -- because voilating the individual daily", async() => { + // transfer 4 tokens -- voilate the daily restriction + await catchRevert( + I_SecurityToken.transfer(account_investor2, new BN(web3.utils.toWei("4")), {from: account_investor1}) + ); + }); + + it("Should add the individual restriction to investor 3", async() => { + let newLatestTime = await getLatestTime(); + let tx = await I_VolumeRestrictionTM.addIndividualRestriction( + account_investor3, + new BN(1536).mul(new BN(10).pow(new BN(14))), // 15.36 tokens as totalsupply is 1000 + newLatestTime.add(new BN(duration.seconds(2))), + 6, + newLatestTime.add(new BN(duration.days(15))), + 1, + { + from: token_owner + } + ); + + assert.equal(tx.logs[0].args._holder, account_investor3); + assert.equal(tx.logs[0].args._typeOfRestriction, 1); + + let data = await I_VolumeRestrictionTM.getRestrictionData.call(); + await printRestrictedData(data); + assert.equal(data[0].length, 4); + assert.equal(data[0][2], account_investor3); + assert.equal(data[0][0], account_investor1); + }); + + it("Should transfer the token by the investor 3 with in the (Individual + Individual daily limit)", async() => { + await increaseTime(4); + // Allowed 4 tokens to transfer + let startTime = (await I_VolumeRestrictionTM.getIndividualRestriction.call(account_investor3))[1].toString(); + let rollingPeriod = (await I_VolumeRestrictionTM.getIndividualRestriction.call(account_investor3))[2].toString(); + let startTimeDaily = (await I_VolumeRestrictionTM.getIndividualDailyRestriction.call(account_investor3))[1].toString(); + console.log(` + Gas estimation (Individual + Individual daily): ${await I_SecurityToken.transfer.estimateGas(account_investor2, new BN(web3.utils.toWei("4")), {from: account_investor3})}` + ); + // Check the balance of the investors + let bal1 = await I_SecurityToken.balanceOf.call(account_investor3); + await I_SecurityToken.transfer(account_investor2, new BN(web3.utils.toWei("4")), {from: account_investor3}); + tempArray3.push(4); + // Check the balance of the investors + let bal2 = await I_SecurityToken.balanceOf.call(account_investor3); + // Verifying the balances + assert.equal(web3.utils.fromWei(((bal1.sub(bal2)).toString()).toString()), 4); + + let data = await I_VolumeRestrictionTM.getIndividualBucketDetailsToUser.call(account_investor3); + await print(data, account_investor3); + + // get the trade amount using the timestamp + let amt = (await I_VolumeRestrictionTM.getTotalTradedByUser.call(account_investor3, data[0].toString())) + .div(new BN(10).pow(new BN(18))).toString(); + + // Verify the storage changes + assert.equal(data[0].toString(), new BN(startTime).add(new BN(duration.days(data[2].toString())))); + assert.equal(web3.utils.fromWei(data[1]), 4); + assert.equal(data[2].toString(), 0); + assert.equal(data[3].toString(), new BN(startTimeDaily).add(new BN(duration.days(1)))); + assert.equal(amt, 4); + }); + + it("Should fail during transferring more tokens by investor3 -- Voilating the daily Limit", async() => { + await catchRevert( + I_SecurityToken.transfer(account_investor2, new BN(web3.utils.toWei("1")), {from: account_investor3}) + ); + }); + + it("Should remove the daily individual limit and transfer more tokens on a same day -- failed because of bad owner", async() => { + // remove the Individual daily restriction + await catchRevert( + I_VolumeRestrictionTM.removeIndividualDailyRestriction(account_investor3, {from: account_investor4}) + ); + }) + + it("Should remove the daily individual limit and transfer more tokens on a same day", async() => { + // remove the Individual daily restriction + let tx = await I_VolumeRestrictionTM.removeIndividualDailyRestriction(account_investor3, {from: token_owner}); + assert.equal(tx.logs[0].args._holder, account_investor3); + let dataAdd = await I_VolumeRestrictionTM.getRestrictionData.call(); + await printRestrictedData(dataAdd); + assert.equal(dataAdd[0].length, 3); + assert.equal(dataAdd[0][0], account_investor1); + assert.equal(dataAdd[0][2], account_investor3); + + let startTime = (await I_VolumeRestrictionTM.getIndividualRestriction.call(account_investor3))[1].toString(); + + // transfer more tokens on the same day + await I_SecurityToken.transfer(account_investor2, new BN(web3.utils.toWei("4")), {from: account_investor3}); + tempArray3[tempArray3.length -1] += 4; + let data = await I_VolumeRestrictionTM.getIndividualBucketDetailsToUser.call(account_investor3); + await print(data, account_investor3); + + // get the trade amount using the timestamp + let amt = (await I_VolumeRestrictionTM.getTotalTradedByUser.call(account_investor3, data[0].toString())) + .div(new BN(10).pow(new BN(18))).toString(); + + // Verify the storage changes + assert.equal(data[0].toString(), new BN(startTime).add(new BN(duration.days(data[2].toString())))); + assert.equal(web3.utils.fromWei(data[1]), 8); + assert.equal(data[2].toString(), 0); + assert.equal(data[3].toString(), 0); + assert.equal(amt, 8); + }); + + it("Should add the new Individual daily restriction and transact the tokens", async() => { + let newLatestTime = await getLatestTime(); + // add new restriction + let tx = await I_VolumeRestrictionTM.addIndividualDailyRestriction( + account_investor3, + new BN(web3.utils.toWei("2")), + newLatestTime.add(new BN(duration.days(1))), + newLatestTime.add(new BN(duration.days(4))), + 0, + { + from: token_owner + } + ); + + assert.equal(tx.logs[0].args._holder, account_investor3); + assert.equal(tx.logs[0].args._typeOfRestriction, 0); + assert.equal((tx.logs[0].args._allowedTokens).toString(), new BN(web3.utils.toWei("2"))); + let dataRestriction = await I_VolumeRestrictionTM.getIndividualDailyRestriction.call(account_investor3); + console.log(` + *** Individual Daily restriction data *** + Allowed Tokens: ${dataRestriction[0].div(new BN(10).pow(new BN(18))).toString()} + StartTime : ${dataRestriction[1].toString()} + Rolling Period in days : ${dataRestriction[2].toString()} + EndTime : ${dataRestriction[3].toString()} + Type of Restriction: ${dataRestriction[4].toString()} + `); + + let rollingPeriod = (await I_VolumeRestrictionTM.getIndividualRestriction.call(account_investor3))[2].toString(); + let startTime = (await I_VolumeRestrictionTM.getIndividualRestriction.call(account_investor3))[1].toString(); + // Increase the time by one day + await increaseTime(duration.days(1.1)); + + //sell tokens upto the limit + await I_SecurityToken.transfer(account_investor2, new BN(web3.utils.toWei("2")), {from: account_investor3}); + tempArray3.push(2); + + let data = await I_VolumeRestrictionTM.getIndividualBucketDetailsToUser.call(account_investor3); + await print(data, account_investor3); + + // get the trade amount using the timestamp + let amt = (await I_VolumeRestrictionTM.getTotalTradedByUser.call(account_investor3, data[0].toString())) + .div(new BN(10).pow(new BN(18))).toString(); + + // Verify the storage changes + assert.equal(data[0].toString(), new BN(startTime).add(new BN(duration.days(data[2].toString())))); + assert.equal(web3.utils.fromWei(data[1]), await calculateSum(rollingPeriod, tempArray3)); + assert.equal(data[2].toString(), 1); + assert.equal(data[3].toString(), dataRestriction[1].toString()); + assert.equal(amt, 2); + + // Fail to sell more tokens than the limit + await catchRevert( + I_SecurityToken.transfer(account_investor2, new BN(web3.utils.toWei("2")), {from: account_investor3}) + ); + }); + + it("Should fail to modify the individual daily restriction -- bad owner", async() => { + let newLatestTime = await getLatestTime(); + await catchRevert( + I_VolumeRestrictionTM.modifyIndividualDailyRestriction( + account_investor3, + new BN(web3.utils.toWei('3')), + newLatestTime, + newLatestTime.add(new BN(duration.days(5))), + 0, + { + from: account_polymath + } + ) + ); + }); + + it("Should modify the individual daily restriction", async() => { + let newLatestTime = await getLatestTime(); + await I_VolumeRestrictionTM.modifyIndividualDailyRestriction( + account_investor3, + new BN(web3.utils.toWei('3')), + 0, + newLatestTime.add(new BN(duration.days(5))), + 0, + { + from: token_owner + } + ); + + let dataRestriction = await I_VolumeRestrictionTM.getIndividualDailyRestriction.call(account_investor3); + console.log(` + *** Modify Individual Daily restriction data *** + Allowed Tokens: ${dataRestriction[0].div(new BN(10).pow(new BN(18))).toString()} + StartTime : ${dataRestriction[1].toString()} + Rolling Period in days : ${dataRestriction[2].toString()} + EndTime : ${dataRestriction[3].toString()} + Type of Restriction: ${dataRestriction[4].toString()} + `); + }); + + it("Should allow to sell to transfer more tokens by investor3", async() => { + let startTime = (await I_VolumeRestrictionTM.getIndividualRestriction.call(account_investor3))[1].toString(); + let startTimedaily = (await I_VolumeRestrictionTM.getIndividualDailyRestriction.call(account_investor3))[1].toString(); + let rollingPeriod = (await I_VolumeRestrictionTM.getIndividualRestriction.call(account_investor3))[2].toString(); + //sell tokens upto the limit + await I_SecurityToken.transfer(account_investor2, new BN(web3.utils.toWei("3")), {from: account_investor3}); + tempArray3[tempArray3.length -1] += 3; + + let data = await I_VolumeRestrictionTM.getIndividualBucketDetailsToUser.call(account_investor3); + await print(data, account_investor3); + + // get the trade amount using the timestamp + let amt = web3.utils.fromWei(await I_VolumeRestrictionTM.getTotalTradedByUser.call(account_investor3, data[0].toString())); + + // Verify the storage changes + assert.equal(data[0].toString(), new BN(startTime).add(new BN(duration.days(data[2].toString())))); + assert.equal(web3.utils.fromWei(data[1]), await calculateSum(rollingPeriod, tempArray3)); + assert.equal(data[2].toString(), 1); + assert.equal(data[3].toString(), startTimedaily); + assert.equal(amt, 5); + }); + + it("Should allow to transact the tokens on the other day", async() => { + let startTime = (await I_VolumeRestrictionTM.getIndividualRestriction.call(account_investor3))[1].toString(); + let startTimedaily = (await I_VolumeRestrictionTM.getIndividualDailyRestriction.call(account_investor3))[1].toString(); + let rollingPeriod = (await I_VolumeRestrictionTM.getIndividualRestriction.call(account_investor3))[2].toString(); + + await increaseTime(duration.days(1.1)); + //sell tokens upto the limit + await I_SecurityToken.transfer(account_investor2, new BN(web3.utils.toWei("2.36")), {from: account_investor3}); + tempArray3.push(2.36); + + let data = await I_VolumeRestrictionTM.getIndividualBucketDetailsToUser.call(account_investor3); + await print(data, account_investor3); + + // get the trade amount using the timestamp + let amt = web3.utils.fromWei(await I_VolumeRestrictionTM.getTotalTradedByUser.call(account_investor3, data[0].toString())); + + // Verify the storage changes + assert.equal(data[0].toString(), new BN(startTime).add(new BN(duration.days(data[2].toString())))); + assert.equal(web3.utils.fromWei(data[1]), await calculateSum(rollingPeriod, tempArray3)); + assert.equal(data[2].toString(), 2); + assert.equal(data[3].toString(), new BN(startTimedaily).add(new BN(duration.days(1)))); + assert.equal(amt, 2.36); + }); + + it("Should fail to transfer the tokens after completion of the total amount", async() => { + await catchRevert( + I_SecurityToken.transfer(account_investor2, new BN(web3.utils.toWei("0.3")), {from: account_investor3}) + ); + }) + + it("Should sell more tokens on the same day after changing the total supply", async() => { + await I_SecurityToken.mint(account_investor3, new BN(web3.utils.toWei("10")), {from: token_owner}); + + let startTime = (await I_VolumeRestrictionTM.getIndividualRestriction.call(account_investor3))[1].toString(); + let startTimedaily = (await I_VolumeRestrictionTM.getIndividualDailyRestriction.call(account_investor3))[1].toString(); + let rollingPeriod = (await I_VolumeRestrictionTM.getIndividualRestriction.call(account_investor3))[2].toString(); + + //sell tokens upto the limit + await I_SecurityToken.transfer(account_investor2, new BN(web3.utils.toWei(".50")), {from: account_investor3}); + tempArray3[tempArray3.length -1] += .50; + + let data = await I_VolumeRestrictionTM.getIndividualBucketDetailsToUser.call(account_investor3); + await print(data, account_investor3); + + // get the trade amount using the timestamp + let amt = web3.utils.fromWei(await I_VolumeRestrictionTM.getTotalTradedByUser.call(account_investor3, data[0].toString())); + + // Verify the storage changes + assert.equal(data[0].toString(), new BN(startTime).add(new BN(duration.days(data[2].toString())))); + assert.equal(web3.utils.fromWei(data[1]), await calculateSum(rollingPeriod, tempArray3)); + assert.equal(data[2].toString(), 2); + assert.equal(data[3].toString(), new BN(startTimedaily).add(new BN(duration.days(1)))); + assert.equal(amt, 2.86); + }); + + it("Should fail to transact tokens more than the allowed in the second rolling period", async() => { + let newLatestTime = await getLatestTime(); + await increaseTime(duration.days(4)); + let i + for (i = 0; i < 3; i++) { + tempArray3.push(0); + } + console.log(`Diff Days: ${(newLatestTime - ((await I_VolumeRestrictionTM.getIndividualBucketDetailsToUser.call(account_investor3))[0]).toString()) / 86400}`); + let allowedAmount = (tempArray3[0] + 1.1); + await catchRevert( + I_SecurityToken.transfer(account_investor2, new BN(web3.utils.toWei(allowedAmount.toString())), {from: account_investor3}) + ); + }) + + it("Should successfully to transact tokens in the second rolling period", async() => { + // Should transact freely tokens daily limit is also ended + + let startTime = (await I_VolumeRestrictionTM.getIndividualRestriction.call(account_investor3))[1].toString(); + let startTimedaily = (await I_VolumeRestrictionTM.getIndividualDailyRestriction.call(account_investor3))[1].toString(); + let rollingPeriod = (await I_VolumeRestrictionTM.getIndividualRestriction.call(account_investor3))[2].toString(); + let allowedAmount = (tempArray3[0] + 1); + //sell tokens upto the limit + await I_SecurityToken.transfer(account_investor2, new BN(web3.utils.toWei(allowedAmount.toString())), {from: account_investor3}); + + tempArray3.push(allowedAmount); + let data = await I_VolumeRestrictionTM.getIndividualBucketDetailsToUser.call(account_investor3); + await print(data, account_investor3); + + // get the trade amount using the timestamp + let amt = web3.utils.fromWei(await I_VolumeRestrictionTM.getTotalTradedByUser.call(account_investor3, data[0].toString())); + + // Verify the storage changes + assert.equal(data[0].toString(), new BN(startTime).add(new BN(duration.days(data[2].toString())))); + assert.equal(web3.utils.fromWei(data[1]), await calculateSum(rollingPeriod, tempArray3)); + assert.equal(data[2].toString(), 6); + assert.equal(data[3].toString(), new BN(startTimedaily).add(new BN(duration.days(1))).toString()); + assert.equal(amt, allowedAmount); + }); + + it("Should sell more tokens on the net day of rolling period", async() => { + await increaseTime(duration.days(3)); + + let startTime = (await I_VolumeRestrictionTM.getIndividualRestriction.call(account_investor3))[1].toString(); + let startTimedaily = (await I_VolumeRestrictionTM.getIndividualDailyRestriction.call(account_investor3))[1].toString(); + let rollingPeriod = (await I_VolumeRestrictionTM.getIndividualRestriction.call(account_investor3))[2].toString(); + + tempArray3.push(0); + tempArray3.push(0); + + //sell tokens upto the limit + await I_SecurityToken.transfer(account_investor2, new BN(web3.utils.toWei("7")), {from: account_investor3}); + + tempArray3.push(7) + let data = await I_VolumeRestrictionTM.getIndividualBucketDetailsToUser.call(account_investor3); + await print(data, account_investor3); + + // get the trade amount using the timestamp + let amt = web3.utils.fromWei(await I_VolumeRestrictionTM.getTotalTradedByUser.call(account_investor3, data[0].toString())); + + // Verify the storage changes + assert.equal(data[0].toString(), new BN(startTime).add(new BN(duration.days(data[2].toString())))); + assert.equal(web3.utils.fromWei(data[1]), await calculateSum(rollingPeriod, tempArray3)); + assert.equal(data[2].toString(), 9); + assert.equal(data[3].toString(), new BN(startTimedaily).add(new BN(duration.days(1)))); + assert.equal(amt, 7); + }) + + it("Should transfer after the 5 days", async() => { + await increaseTime(duration.days(4.5)); + + for (let i = 0; i <3; i++) { + tempArray3.push(0); + } + + let startTime = (await I_VolumeRestrictionTM.getIndividualRestriction.call(account_investor3))[1].toString(); + let startTimedaily = (await I_VolumeRestrictionTM.getIndividualDailyRestriction.call(account_investor3))[1].toString(); + let rollingPeriod = (await I_VolumeRestrictionTM.getIndividualRestriction.call(account_investor3))[2].toString(); + + await I_SecurityToken.transfer(account_investor3, new BN(web3.utils.toWei("25")), {from: account_investor2}); + //sell tokens upto the limit + await I_SecurityToken.transfer(account_investor2, new BN(web3.utils.toWei("8")), {from: account_investor3}); + tempArray3.push(8); + + let data = await I_VolumeRestrictionTM.getIndividualBucketDetailsToUser.call(account_investor3); + await print(data, account_investor3); + + // get the trade amount using the timestamp + let amt = web3.utils.fromWei(await I_VolumeRestrictionTM.getTotalTradedByUser.call(account_investor3, data[0].toString())); + + // Verify the storage changes + assert.equal(data[0].toString(), new BN(startTime).add(new BN(duration.days(data[2].toString())))); + assert.equal(web3.utils.fromWei(data[1]), await calculateSum(rollingPeriod, tempArray3)); + assert.equal(data[2].toString(), 13); + assert.equal(data[3].toString(), new BN(startTimedaily).add(new BN(duration.days(1)))); + assert.equal(amt, 8); + }); + + it("Should freely transfer the tokens after one day (completion of individual restriction)", async() => { + // increase one time + await increaseTime(duration.days(2)); + await I_SecurityToken.transfer(account_investor2, new BN(web3.utils.toWei("17")), {from: account_investor3}); + }); + }); + + describe("Test cases for the Default restrictions", async() => { + + it("Should add the investor 4 in the whitelist", async() => { + let newLatestTime = await getLatestTime(); + await I_GeneralTransferManager.modifyWhitelist( + account_investor4, + newLatestTime, + newLatestTime, + newLatestTime.add(new BN(duration.days(30))), + true, + { + from: token_owner + } + ); + }); + + it("Should mint some tokens to investor 4", async() => { + await I_SecurityToken.mint(account_investor4, new BN(web3.utils.toWei("20")), {from: token_owner}); + }); + + it("Should add the default daily restriction successfully", async() => { + let newLatestTime = await getLatestTime(); + await I_VolumeRestrictionTM.addDefaultDailyRestriction( + new BN(275).mul(new BN(10).pow(new BN(14))), + 0, + newLatestTime.add(new BN(duration.days(3))), + 1, + { + from: token_owner + } + ); + + let dataRestriction = await I_VolumeRestrictionTM.getDefaultDailyRestriction.call(); + console.log(` + *** Add Individual Daily restriction data *** + Allowed Tokens: ${dataRestriction[0].div(new BN(10).pow(new BN(16))).toString()} % of TotalSupply + StartTime : ${dataRestriction[1].toString()} + Rolling Period in days : ${dataRestriction[2].toString()} + EndTime : ${dataRestriction[3].toString()} + Type of Restriction: ${dataRestriction[4].toString()} + `); + }); + + it("Should fail to transfer above the daily limit", async() => { + await catchRevert( + I_SecurityToken.transfer(account_investor3, new BN(web3.utils.toWei("5")), {from: account_investor4}) + ) + }) + + it("Should transfer the token by investor 4", async() => { + let startTimedaily = (await I_VolumeRestrictionTM.getDefaultDailyRestriction.call())[1].toString(); + //sell tokens upto the limit + await I_SecurityToken.transfer(account_investor2, new BN(web3.utils.toWei("3.57")), {from: account_investor4}); + + let data = await I_VolumeRestrictionTM.getDefaultBucketDetailsToUser.call(account_investor4); + await print(data, account_investor3); + + // get the trade amount using the timestamp + let amt = web3.utils.fromWei(await I_VolumeRestrictionTM.getTotalTradedByUser.call(account_investor4, data[3].toString())); + + // Verify the storage changes + assert.equal(data[0].toString(), 0); + assert.equal(data[1].toString(), 0); + assert.equal(data[2].toString(), 0); + assert.equal(data[3].toString(), startTimedaily); + assert.equal(amt, 3.57); + }); + + it("Should transfer the tokens freely after ending the default daily restriction", async() => { + await increaseTime(duration.days(3) + 10); + //sell tokens upto the limit + let tx = await I_SecurityToken.transfer(account_investor2, new BN(web3.utils.toWei("5")), {from: account_investor4}); + assert.equal((tx.logs[0].args.value).toString(), new BN(web3.utils.toWei("5"))); + // Transfer the tokens again to investor 3 + await I_SecurityToken.transfer(account_investor3, new BN(web3.utils.toWei("40")), {from: account_investor2}); + }) + + it("Should successfully add the default restriction", async() => { + let newLatestTime = await getLatestTime(); + await I_VolumeRestrictionTM.addDefaultRestriction( + new BN(web3.utils.toWei("10")), + 0, + 5, + newLatestTime.add(new BN(duration.days(10))), + 0, + { + from: token_owner + } + ); + + let data = await I_VolumeRestrictionTM.getDefaultRestriction.call(); + assert.equal(data[0].toString(), new BN(web3.utils.toWei("10"))); + assert.equal(data[2].toString(), 5); + let dataRestriction = await I_VolumeRestrictionTM.getDefaultRestriction.call(); + console.log(` + *** Add Individual restriction data *** + Allowed Tokens: ${dataRestriction[0].div(new BN(10).pow(new BN(18))).toString()} + StartTime : ${dataRestriction[1].toString()} + Rolling Period in days : ${dataRestriction[2].toString()} + EndTime : ${dataRestriction[3].toString()} + Type of Restriction: ${dataRestriction[4].toString()} + `); + }); + + it("Should transfer tokens on by investor 3 (comes under the Default restriction)", async() => { + await increaseTime(10); + tempArray3.length = 0; + let startTime = (await I_VolumeRestrictionTM.getDefaultRestriction.call())[1].toString(); + let startTimedaily = (await I_VolumeRestrictionTM.getDefaultDailyRestriction.call())[1].toString(); + let rollingPeriod = (await I_VolumeRestrictionTM.getDefaultRestriction.call())[2].toString(); + //sell tokens upto the limit + await I_SecurityToken.transfer(account_investor2, new BN(web3.utils.toWei("5")), {from: account_investor3}); + tempArray3.push(5); + + let data = await I_VolumeRestrictionTM.getDefaultBucketDetailsToUser.call(account_investor3); + await print(data, account_investor3); + + // get the trade amount using the timestamp + let amt = web3.utils.fromWei(await I_VolumeRestrictionTM.getTotalTradedByUser.call(account_investor3, data[0].toString())); + + // Verify the storage changes + assert.equal(data[0].toString(), new BN(startTime).add(new BN(duration.days(data[2].toString())))); + assert.equal(web3.utils.fromWei(data[1]), await calculateSum(rollingPeriod, tempArray3)); + assert.equal(data[2].toString(), 0); + assert.equal(data[3].toString(), 0); + assert.equal(amt, 5); + + // Transfer tokens on another day + await increaseTime(duration.days(1)); + //sell tokens upto the limit + await I_SecurityToken.transfer(account_investor2, new BN(web3.utils.toWei("3")), {from: account_investor3}); + tempArray3.push(3); + + data = await I_VolumeRestrictionTM.getDefaultBucketDetailsToUser.call(account_investor3); + await print(data, account_investor3); + + // get the trade amount using the timestamp + amt = web3.utils.fromWei(await I_VolumeRestrictionTM.getTotalTradedByUser.call(account_investor3, data[0].toString())); + + // Verify the storage changes + assert.equal(data[0].toString(), new BN(startTime).add(new BN(duration.days(data[2].toString())))); + assert.equal(web3.utils.fromWei(data[1]), await calculateSum(rollingPeriod, tempArray3)); + assert.equal(data[2].toString(), 1); + assert.equal(data[3].toString(), 0); + assert.equal(amt, 3); + }); + + it("Should fail to transfer more tokens than the available default limit", async() => { + await catchRevert( + I_SecurityToken.transfer(account_investor2, new BN(web3.utils.toWei("3")), {from: account_investor3}) + ); + }); + + it("Should able to transfer tokens in the next rolling period", async() => { + let newLatestTime = await getLatestTime(); + await increaseTime(duration.days(4.1)); + console.log(`*** Diff days: ${(newLatestTime - ((await I_VolumeRestrictionTM.getDefaultBucketDetailsToUser.call(account_investor3))[0]).toString()) / 86400}`) + for (let i = 0; i < 3; i++) { + tempArray3.push(0); + } + + let startTime = (await I_VolumeRestrictionTM.getDefaultRestriction.call())[1].toString(); + let startTimedaily = (await I_VolumeRestrictionTM.getDefaultDailyRestriction.call())[1].toString(); + let rollingPeriod = (await I_VolumeRestrictionTM.getDefaultRestriction.call())[2].toString(); + + //sell tokens upto the limit + await I_SecurityToken.transfer(account_investor2, new BN(web3.utils.toWei("7")), {from: account_investor3}); + tempArray3.push(7); + + let data = await I_VolumeRestrictionTM.getDefaultBucketDetailsToUser.call(account_investor3); + await print(data, account_investor3); + + // get the trade amount using the timestamp + let amt = web3.utils.fromWei(await I_VolumeRestrictionTM.getTotalTradedByUser.call(account_investor3, data[0].toString())); + + // Verify the storage changes + assert.equal(data[0].toString(), new BN(startTime).add(new BN(duration.days(data[2].toString())))); + assert.equal(web3.utils.fromWei(data[1]), await calculateSum(rollingPeriod, tempArray3)); + assert.equal(data[2].toString(), 5); + assert.equal(data[3].toString(), 0); + assert.equal(amt, 7); + + // Try to transact more on the same day but fail + await catchRevert( + I_SecurityToken.transfer(account_investor2, new BN(web3.utils.toWei("1")), {from: account_investor3}) + ); + }); + + it("Should add the daily default restriction again", async() => { + let newLatestTime = await getLatestTime(); + await I_VolumeRestrictionTM.addDefaultDailyRestriction( + new BN(web3.utils.toWei("2")), + 0, + newLatestTime.add(new BN(duration.days(3))), + 0, + { + from: token_owner + } + ); + + let dataRestriction = await I_VolumeRestrictionTM.getDefaultDailyRestriction.call(); + console.log(` + *** Add Individual Daily restriction data *** + Allowed Tokens: ${dataRestriction[0].div(new BN(10).pow(new BN(16))).toString()} + StartTime : ${dataRestriction[1].toString()} + Rolling Period in days : ${dataRestriction[2].toString()} + EndTime : ${dataRestriction[3].toString()} + Type of Restriction: ${dataRestriction[4].toString()} + `); + }); + + it("Should not able to transfer tokens more than the default daily restriction", async() => { + await catchRevert( + I_SecurityToken.transfer(account_investor2, new BN(web3.utils.toWei("3")), {from: account_investor3}) + ); + }); + + it("Should able to transfer tokens within the limit of (daily default + default) restriction", async() => { + await increaseTime(duration.days(1)); + let startTime = (await I_VolumeRestrictionTM.getDefaultRestriction.call())[1].toString(); + let startTimedaily = (await I_VolumeRestrictionTM.getDefaultDailyRestriction.call())[1].toString(); + let rollingPeriod = (await I_VolumeRestrictionTM.getDefaultRestriction.call())[2].toString(); + //sell tokens upto the limit + await I_SecurityToken.transfer(account_investor2, new BN(web3.utils.toWei("2")), {from: account_investor3}); + tempArray3.push(2); + + let data = await I_VolumeRestrictionTM.getDefaultBucketDetailsToUser.call(account_investor3); + await print(data, account_investor3); + + // get the trade amount using the timestamp + let amt = web3.utils.fromWei(await I_VolumeRestrictionTM.getTotalTradedByUser.call(account_investor3, data[0].toString())); + + // Verify the storage changes + assert.equal(data[0].toString(), new BN(startTime).add(new BN(duration.days(data[2].toString())))); + assert.equal(web3.utils.fromWei(data[1]), await calculateSum(rollingPeriod, tempArray3)); + assert.equal(data[2].toString(), 6); + assert.equal(data[3].toString(), new BN(startTimedaily).add(new BN(duration.days(1)))); + assert.equal(amt, 2); + }); + }) + + describe("Test for the exemptlist", async() => { + + it("Should add the token holder in the exemption list -- failed because of bad owner", async() => { + await catchRevert( + I_VolumeRestrictionTM.changeExemptWalletList(account_investor4, true, {from: account_polymath}) + ); + }); + + it("Should add the token holder in the exemption list", async() => { + await I_VolumeRestrictionTM.changeExemptWalletList(account_investor4, true, {from: token_owner}); + console.log(await I_VolumeRestrictionTM.getExemptAddress.call()); + let beforeBal = await I_SecurityToken.balanceOf.call(account_investor4); + await I_SecurityToken.transfer(account_investor3, new BN(web3.utils.toWei("3")), {from: account_investor4}); + let afterBal = await I_SecurityToken.balanceOf.call(account_investor4); + let diff = beforeBal.sub(afterBal); + assert.equal(web3.utils.fromWei((diff.toString()).toString()), 3); + }); + + it("Should add multiple token holders to exemption list and check the getter value", async() => { + let holders = [account_investor1, account_investor3, account_investor2, account_delegate2]; + let change = [true, true, true, true]; + for (let i = 0; i < holders.length; i++) { + await I_VolumeRestrictionTM.changeExemptWalletList(holders[i], change[i], {from: token_owner}); + } + let data = await I_VolumeRestrictionTM.getExemptAddress.call(); + assert.equal(data.length, 5); + assert.equal(data[0], account_investor4); + assert.equal(data[1], account_investor1); + assert.equal(data[2], account_investor3); + assert.equal(data[3], account_investor2); + assert.equal(data[4], account_delegate2); + }); + + it("Should unexempt a particular address", async() => { + await I_VolumeRestrictionTM.changeExemptWalletList(account_investor1, false, {from: token_owner}); + let data = await I_VolumeRestrictionTM.getExemptAddress.call(); + assert.equal(data.length, 4); + assert.equal(data[0], account_investor4); + assert.equal(data[1], account_delegate2); + assert.equal(data[2], account_investor3); + assert.equal(data[3], account_investor2); + }); + + it("Should fail to unexempt the same address again", async() => { + await catchRevert( + I_VolumeRestrictionTM.changeExemptWalletList(account_investor1, false, {from: token_owner}) + ); + }); + + it("Should delete the last element of the exemption list", async() => { + await I_VolumeRestrictionTM.changeExemptWalletList(account_investor2, false, {from: token_owner}); + let data = await I_VolumeRestrictionTM.getExemptAddress.call(); + assert.equal(data.length, 3); + assert.equal(data[0], account_investor4); + assert.equal(data[1], account_delegate2); + assert.equal(data[2], account_investor3); + }); + + it("Should delete multiple investor from the exemption list", async() => { + let holders = [account_delegate2, account_investor4, account_investor3]; + let change = [false, false, false]; + for (let i = 0; i < holders.length; i++) { + await I_VolumeRestrictionTM.changeExemptWalletList(holders[i], change[i], {from: token_owner}); + } + let data = await I_VolumeRestrictionTM.getExemptAddress.call(); + assert.equal(data.length, 0); + }); + }); + + describe("Test for modify functions", async() => { + + it("Should add the individual restriction for multiple investor", async() => { + let newLatestTime = await getLatestTime(); + await I_VolumeRestrictionTM.addIndividualRestrictionMulti( + [account_investor3, account_delegate3], + [new BN(web3.utils.toWei("15")), new BN(1278).mul(new BN(10).pow(new BN(14)))], + [newLatestTime.add(new BN(duration.days(1))), newLatestTime.add(new BN(duration.days(2)))], + [15, 20], + [newLatestTime.add(new BN(duration.days(40))), newLatestTime.add(new BN(duration.days(60)))], + [0,1], + { + from: token_owner + } + ); + + let indi1 = await I_VolumeRestrictionTM.getIndividualRestriction.call(account_investor3); + let indi2 = await I_VolumeRestrictionTM.getIndividualRestriction.call(account_delegate3); + + assert.equal(indi1[0].div(new BN(10).pow(new BN(18))).toString(), 15); + assert.equal(indi2[0].div(new BN(10).pow(new BN(14))).toString(), 1278); + + assert.equal(indi1[2].toString(), 15); + assert.equal(indi2[2].toString(), 20); + + assert.equal(indi1[4].toString(), 0); + assert.equal(indi2[4].toString(), 1); + }); + + it("Should modify the details before the starttime passed", async() => { + let newLatestTime = await getLatestTime(); + await I_VolumeRestrictionTM.modifyIndividualRestrictionMulti( + [account_investor3, account_delegate3], + [new BN(1278).mul(new BN(10).pow(new BN(14))), new BN(web3.utils.toWei("15"))], + [newLatestTime.add(new BN(duration.days(1))), newLatestTime.add(new BN(duration.days(2)))], + [20, 15], + [newLatestTime.add(new BN(duration.days(40))), newLatestTime.add(new BN(duration.days(60)))], + [1,0], + { + from: token_owner + } + ); + + let indi1 = await I_VolumeRestrictionTM.getIndividualRestriction.call(account_investor3); + let indi2 = await I_VolumeRestrictionTM.getIndividualRestriction.call(account_delegate3); + + assert.equal(indi2[0].div(new BN(10).pow(new BN(18))).toString(), 15); + assert.equal(indi1[0].div(new BN(10).pow(new BN(14))).toString(), 1278); + + assert.equal(indi2[2].toString(), 15); + assert.equal(indi1[2].toString(), 20); + + assert.equal(indi2[4].toString(), 0); + assert.equal(indi1[4].toString(), 1); + }); + + }); + + describe("VolumeRestriction Transfer Manager Factory test cases", async() => { + + it("Should get the exact details of the factory", async() => { + assert.equal(await I_VolumeRestrictionTMFactory.getSetupCost.call(),0); + assert.equal((await I_VolumeRestrictionTMFactory.getTypes.call())[0],2); + assert.equal(web3.utils.toAscii(await I_VolumeRestrictionTMFactory.getName.call()) + .replace(/\u0000/g, ''), + "VolumeRestrictionTM", + "Wrong Module added"); + assert.equal(await I_VolumeRestrictionTMFactory.description.call(), + "Manage transfers based on the volume of tokens that needs to be transact", + "Wrong Module added"); + assert.equal(await I_VolumeRestrictionTMFactory.title.call(), + "Volume Restriction Transfer Manager", + "Wrong Module added"); + assert.equal(await I_VolumeRestrictionTMFactory.getInstructions.call(), + "Module used to restrict the volume of tokens traded by the token holders", + "Wrong Module added"); + assert.equal(await I_VolumeRestrictionTMFactory.version.call(), "1.0.0"); + }); + + it("Should get the tags of the factory", async() => { + let tags = await I_VolumeRestrictionTMFactory.getTags.call(); + assert.equal(tags.length, 5); + assert.equal(web3.utils.toAscii(tags[0]).replace(/\u0000/g, ''), "Maximum Volume"); + }); + }); + +}); diff --git a/test/z_blacklist_transfer_manager.js b/test/z_blacklist_transfer_manager.js new file mode 100644 index 000000000..54e249a1f --- /dev/null +++ b/test/z_blacklist_transfer_manager.js @@ -0,0 +1,996 @@ +import latestTime from './helpers/latestTime'; +import { duration, ensureException, promisifyLogWatch, latestBlock } from './helpers/utils'; +import takeSnapshot, { increaseTime, revertToSnapshot } from './helpers/time'; +import { encodeProxyCall, encodeModuleCall } from './helpers/encodeCall'; +import { setUpPolymathNetwork, deployGPMAndVerifyed, deployBlacklistTMAndVerified } from "./helpers/createInstances"; +import { catchRevert } from "./helpers/exceptions"; + +const GeneralTransferManager = artifacts.require("./GeneralTransferManager"); +const BlacklistTransferManager = artifacts.require("./BlacklistTransferManager"); +const SecurityToken = artifacts.require("./SecurityToken.sol"); + +const Web3 = require('web3'); +let BN = Web3.utils.BN; +const web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545")) // Hardcoded development port + +contract('BlacklistTransferManager', accounts => { + + // Accounts Variable declaration + let account_polymath; + let account_issuer; + let token_owner; + let account_investor1; + let account_investor2; + let account_investor3; + let account_investor4; + let account_investor5; + + // investor Details + let fromTime = latestTime(); + let toTime = latestTime(); + let expiryTime = toTime + duration.days(15); + + let message = "Transaction Should Fail!"; + + // Contract Instance Declaration + let I_GeneralPermissionManagerFactory; + let I_SecurityTokenRegistryProxy; + let I_GeneralTransferManagerFactory; + let I_BlacklistTransferManagerFactory; + let I_GeneralPermissionManager; + let I_BlacklistTransferManager; + let P_BlacklistTransferManagerFactory; + let P_BlacklistTransferManager; + let I_GeneralTransferManager; + let I_ExchangeTransferManager; + let I_ModuleRegistry; + let I_ModuleRegistryProxy; + let I_MRProxied; + let I_STRProxied; + let I_FeatureRegistry; + let I_SecurityTokenRegistry; + let I_STFactory; + let I_SecurityToken; + let I_PolyToken; + let I_PolymathRegistry; + + // SecurityToken Details + const name = "Team"; + const symbol = "sap"; + const tokenDetails = "This is equity type of issuance"; + const decimals = 18; + const contact = "team@polymath.network"; + + // Module key + const delegateManagerKey = 1; + const transferManagerKey = 2; + const stoKey = 3; + + // Initial fee for ticker registry and security token registry + const initRegFee = web3.utils.toWei("250"); + + // BlacklistTransferManager details + const holderCount = 2; // Maximum number of token holders + const STRProxyParameters = ['address', 'address', 'uint256', 'uint256', 'address', 'address']; + const MRProxyParameters = ['address', 'address']; + let bytesSTO = encodeModuleCall(['uint256'], [holderCount]); + let currentTime; + + before(async() => { + currentTime = new BN(await latestTime()); + // Accounts setup + account_polymath = accounts[0]; + account_issuer = accounts[1]; + + token_owner = account_issuer; + + account_investor1 = accounts[7]; + account_investor2 = accounts[8]; + account_investor3 = accounts[9]; + account_investor4 = accounts[5]; + account_investor5 = accounts[6]; + + let instances = await setUpPolymathNetwork(account_polymath, token_owner); + + [ + I_PolymathRegistry, + I_PolyToken, + I_FeatureRegistry, + I_ModuleRegistry, + I_ModuleRegistryProxy, + I_MRProxied, + I_GeneralTransferManagerFactory, + I_STFactory, + I_SecurityTokenRegistry, + I_SecurityTokenRegistryProxy, + I_STRProxied + ] = instances; + + // STEP 2: Deploy the GeneralDelegateManagerFactory + [I_GeneralPermissionManagerFactory] = await deployGPMAndVerifyed(account_polymath, I_MRProxied, new BN(0)); + + // STEP 3(a): Deploy the PercentageTransferManager + [I_BlacklistTransferManagerFactory] = await deployBlacklistTMAndVerified(account_polymath, I_MRProxied, new BN(0)); + + // STEP 4(b): Deploy the PercentageTransferManager + [P_BlacklistTransferManagerFactory] = await deployBlacklistTMAndVerified(account_polymath, I_MRProxied, new BN(web3.utils.toWei("500", "ether"))); + // ----------- POLYMATH NETWORK Configuration ------------ + + // Printing all the contract addresses + console.log(` + --------------------- Polymath Network Smart Contracts: --------------------- + PolymathRegistry: ${I_PolymathRegistry.address} + SecurityTokenRegistryProxy: ${I_SecurityTokenRegistryProxy.address} + SecurityTokenRegistry: ${I_SecurityTokenRegistry.address} + ModuleRegistry: ${I_ModuleRegistry.address} + ModuleRegistryProxy: ${I_ModuleRegistryProxy.address} + FeatureRegistry: ${I_FeatureRegistry.address} + + STFactory: ${I_STFactory.address} + GeneralTransferManagerFactory: ${I_GeneralTransferManagerFactory.address} + GeneralPermissionManagerFactory: ${I_GeneralPermissionManagerFactory.address} + + BlacklistTransferManagerFactory: ${I_BlacklistTransferManagerFactory.address} + ----------------------------------------------------------------------------- + `); + }); + + describe("Generate the SecurityToken", async() => { + + it("Should register the ticker before the generation of the security token", async () => { + await I_PolyToken.approve(I_STRProxied.address, initRegFee, { from: token_owner}); + let tx = await I_STRProxied.registerTicker(token_owner, symbol, contact, { from : token_owner }); + assert.equal(tx.logs[0].args._owner, token_owner); + assert.equal(tx.logs[0].args._ticker, symbol.toUpperCase()); + }); + + it("Should generate the new security token with the same symbol as registered above", async () => { + await I_PolyToken.approve(I_STRProxied.address, initRegFee, { from: token_owner}); + let _blockNo = latestBlock(); + let tx = await I_STRProxied.generateSecurityToken(name, symbol, tokenDetails, false, { from: token_owner }); + // Verify the successful generation of the security token + assert.equal(tx.logs[2].args._ticker, symbol.toUpperCase(), "SecurityToken doesn't get deployed"); + + I_SecurityToken = await SecurityToken.at(tx.logs[2].args._securityTokenAddress); + + const log = (await I_SecurityToken.getPastEvents('ModuleAdded', {filter: {transactionHash: tx.transactionHash}}))[0]; + + // Verify that GeneralTransferManager module get added successfully or not + assert.equal(log.args._types[0].toString(), 2); + assert.equal( + web3.utils.toAscii(log.args._name) + .replace(/\u0000/g, ''), + "GeneralTransferManager" + ); + }); + + it("Should intialize the auto attached modules", async () => { + let moduleData = (await I_SecurityToken.getModulesByType(2))[0]; + I_GeneralTransferManager = await GeneralTransferManager.at(moduleData); + + }); + + it("Should successfully attach the BlacklistTransferManager factory with the security token", async () => { + await I_PolyToken.getTokens(web3.utils.toWei("500", "ether"), token_owner); + await catchRevert ( + I_SecurityToken.addModule(P_BlacklistTransferManagerFactory.address, bytesSTO, web3.utils.toWei("500", "ether"), 0, { + from: token_owner + }) + ); + }); + + it("Should successfully attach the BlacklistTransferManager factory with the security token", async () => { + let snapId = await takeSnapshot(); + await I_PolyToken.transfer(I_SecurityToken.address, web3.utils.toWei("500", "ether"), {from: token_owner}); + const tx = await I_SecurityToken.addModule(P_BlacklistTransferManagerFactory.address, bytesSTO, web3.utils.toWei("500", "ether"), 0, { from: token_owner }); + assert.equal(tx.logs[3].args._types[0].toString(), transferManagerKey, "BlacklistTransferManager doesn't get deployed"); + assert.equal( + web3.utils.toAscii(tx.logs[3].args._name) + .replace(/\u0000/g, ''), + "BlacklistTransferManager", + "BlacklistTransferManagerFactory module was not added" + ); + P_BlacklistTransferManager = await BlacklistTransferManager.at(tx.logs[3].args._module); + await revertToSnapshot(snapId); + }); + + it("Should successfully attach the BlacklistTransferManager with the security token", async () => { + const tx = await I_SecurityToken.addModule(I_BlacklistTransferManagerFactory.address, bytesSTO, 0, 0, { from: token_owner }); + console.log(tx); + assert.equal(tx.logs[2].args._types[0].toString(), transferManagerKey, "BlacklistTransferManager doesn't get deployed"); + assert.equal( + web3.utils.toAscii(tx.logs[2].args._name) + .replace(/\u0000/g, ''), + "BlacklistTransferManager", + "BlacklistTransferManager module was not added" + ); + I_BlacklistTransferManager = await BlacklistTransferManager.at(tx.logs[2].args._module); + }); + + }); + + describe("Buy tokens using on-chain whitelist", async() => { + + it("Should Buy the tokens", async() => { + // Add the Investor in to the whitelist + + let tx = await I_GeneralTransferManager.modifyWhitelist( + account_investor1, + currentTime, + currentTime, + currentTime.add(new BN(duration.days(50))), + true, + { + from: account_issuer + }); + assert.equal(tx.logs[0].args._investor.toLowerCase(), account_investor1.toLowerCase(), "Failed in adding the investor in whitelist"); + + // Jump time + await increaseTime(5000); + + // Mint some tokens + await I_SecurityToken.mint(account_investor1, web3.utils.toWei('5', 'ether'), { from: token_owner }); + + assert.equal( + (await I_SecurityToken.balanceOf(account_investor1)).toString(), + web3.utils.toWei('5', 'ether') + ); + + }); + + it("Should Buy some more tokens", async() => { + // Add the Investor in to the whitelist + + let tx = await I_GeneralTransferManager.modifyWhitelist( + account_investor2, + currentTime, + currentTime, + currentTime.add(new BN(duration.days(50))), + true, + { + from: account_issuer + }); + + assert.equal(tx.logs[0].args._investor.toLowerCase(), account_investor2.toLowerCase(), "Failed in adding the investor in whitelist"); + + // Mint some tokens + await I_SecurityToken.mint(account_investor2, web3.utils.toWei('2', 'ether'), { from: token_owner }); + + assert.equal( + (await I_SecurityToken.balanceOf(account_investor2)).toString(), + web3.utils.toWei('2', 'ether') + ); + }); + + it("Should Buy some more tokens", async() => { + // Add the Investor in to the whitelist + + let tx = await I_GeneralTransferManager.modifyWhitelist( + account_investor3, + currentTime, + currentTime, + currentTime.add(new BN(duration.days(50))), + true, + { + from: account_issuer + }); + + assert.equal(tx.logs[0].args._investor.toLowerCase(), account_investor3.toLowerCase(), "Failed in adding the investor in whitelist"); + + // Mint some tokens + await I_SecurityToken.mint(account_investor3, web3.utils.toWei('2', 'ether'), { from: token_owner }); + + assert.equal( + (await I_SecurityToken.balanceOf(account_investor3)).toString(), + web3.utils.toWei('2', 'ether') + ); + }); + + it("Should Buy some more tokens", async() => { + // Add the Investor in to the whitelist + + let tx = await I_GeneralTransferManager.modifyWhitelist( + account_investor4, + currentTime, + currentTime, + currentTime.add(new BN(duration.days(50))), + true, + { + from: account_issuer + }); + + assert.equal(tx.logs[0].args._investor.toLowerCase(), account_investor4.toLowerCase(), "Failed in adding the investor in whitelist"); + + // Mint some tokens + await I_SecurityToken.mint(account_investor4, web3.utils.toWei('2', 'ether'), { from: token_owner }); + + assert.equal( + (await I_SecurityToken.balanceOf(account_investor4)).toString(), + web3.utils.toWei('2', 'ether') + ); + }); + + it("Should Buy some more tokens", async() => { + // Add the Investor in to the whitelist + + let tx = await I_GeneralTransferManager.modifyWhitelist( + account_investor5, + currentTime, + currentTime, + currentTime.add(new BN(duration.days(50))), + true, + { + from: account_issuer + }); + + assert.equal(tx.logs[0].args._investor.toLowerCase(), account_investor5.toLowerCase(), "Failed in adding the investor in whitelist"); + + // Mint some tokens + await I_SecurityToken.mint(account_investor5, web3.utils.toWei('2', 'ether'), { from: token_owner }); + + assert.equal( + (await I_SecurityToken.balanceOf(account_investor5)).toString(), + web3.utils.toWei('2', 'ether') + ); + }); + + + it("Should add the blacklist", async() => { + //Add the new blacklist + currentTime = new BN(await latestTime()); + let tx = await I_BlacklistTransferManager.addBlacklistType(currentTime.add(new BN(1000)), currentTime.add(new BN(3000)), web3.utils.fromAscii("a_blacklist"), 20, { from: token_owner }); + assert.equal(web3.utils.hexToUtf8(tx.logs[0].args._blacklistName), "a_blacklist", "Failed in adding the type in blacklist"); + }); + + it("Should fail in adding the blacklist as blacklist type already exist", async() => { + await catchRevert( + I_BlacklistTransferManager.addBlacklistType(currentTime.add(new BN(1000)), currentTime.add(new BN(3000)), web3.utils.fromAscii("a_blacklist"), 20, { + from: token_owner + }) + ); + }); + + it("Should fail in adding the blacklist as the blacklist name is invalid", async() => { + await catchRevert( + I_BlacklistTransferManager.addBlacklistType(currentTime.add(new BN(1000)), currentTime.add(new BN(3000)), web3.utils.fromAscii(""), 20, { + from: token_owner + }) + ); + }); + + it("Should fail in adding the blacklist as the start date is invalid", async() => { + await catchRevert( + I_BlacklistTransferManager.addBlacklistType(0, currentTime.add(new BN(3000)), web3.utils.fromAscii("b_blacklist"), 20, { + from: token_owner + }) + ); + }); + + it("Should fail in adding the blacklist as the dates are invalid", async() => { + await catchRevert( + I_BlacklistTransferManager.addBlacklistType(currentTime.add(new BN(4000)), currentTime.add(new BN(3000)), web3.utils.fromAscii("b_blacklist"), 20, { + from: token_owner + }) + ); + }); + + it("Should fail in adding the blacklist because only owner can add the blacklist", async() => { + await catchRevert( + I_BlacklistTransferManager.addBlacklistType(currentTime.add(new BN(1000)), currentTime.add(new BN(3000)), web3.utils.fromAscii("b_blacklist"), 20, { + from: account_investor1 + }) + ); + }); + + it("Should fail in adding the blacklist because repeat period is less than the difference of start time and end time", async() => { + await catchRevert( + I_BlacklistTransferManager.addBlacklistType(currentTime.add(new BN(1000)), currentTime.add(new BN(duration.days(2))), web3.utils.fromAscii("b_blacklist"), 1, { + from: token_owner + }) + ); + }); + + it("Should add the mutiple blacklist", async() => { + //Add the new blacklist + let startTime = [currentTime.add(new BN(2000)),currentTime.add(new BN(3000))]; + let endTime = [currentTime.add(new BN(5000)),currentTime.add(new BN(8000))]; + let name = [web3.utils.fromAscii("y_blacklist"),web3.utils.fromAscii("z_blacklist")]; + let repeatTime = [15,30]; + let tx = await I_BlacklistTransferManager.addBlacklistTypeMulti(startTime, endTime, name, repeatTime, { from: token_owner }); + + let event_data = tx.logs; + for (var i = 0; i < event_data.length; i++) { + let blacklistName = event_data[i].args._blacklistName; + assert.equal(web3.utils.hexToUtf8(blacklistName), web3.utils.hexToUtf8(name[i]), "Failed in adding the blacklist"); + } + }); + + it("Should fail in adding the mutiple blacklist because only owner can add it", async() => { + //Add the new blacklist + let startTime = [currentTime.add(new BN(2000)),currentTime.add(new BN(3000))]; + let endTime = [currentTime.add(new BN(5000)),currentTime.add(new BN(8000))]; + let name = [web3.utils.fromAscii("y_blacklist"),web3.utils.fromAscii("z_blacklist")]; + let repeatTime = [15,30]; + await catchRevert( + I_BlacklistTransferManager.addBlacklistTypeMulti(startTime, endTime, name, repeatTime, { + from: account_investor1 + }) + ); + }); + + it("Should fail in adding the mutiple blacklist because array lenth are different", async() => { + //Add the new blacklist + let startTime = [currentTime.add(new BN(2000)),currentTime.add(new BN(3000))]; + let endTime = [currentTime.add(new BN(5000)),currentTime.add(new BN(8000))]; + let name = [web3.utils.fromAscii("y_blacklist"),web3.utils.fromAscii("z_blacklist"),web3.utils.fromAscii("w_blacklist")]; + let repeatTime = [15,30]; + await catchRevert( + I_BlacklistTransferManager.addBlacklistTypeMulti(startTime, endTime, name, repeatTime, { + from: token_owner + }) + ); + }); + + it("Should modify the blacklist", async() => { + //Modify the existing blacklist + let tx = await I_BlacklistTransferManager.modifyBlacklistType(currentTime.add(new BN(2000)), currentTime.add(new BN(3000)), web3.utils.fromAscii("a_blacklist"), 20, { from: token_owner }); + assert.equal(web3.utils.hexToUtf8(tx.logs[0].args._blacklistName), "a_blacklist", "Failed in modifying the startdate of blacklist"); + + }); + + it("Should fail in modifying the blacklist as the name is invalid", async() => { + await catchRevert( + I_BlacklistTransferManager.modifyBlacklistType(currentTime.add(new BN(2000)), currentTime.add(new BN(3000)), web3.utils.fromAscii(""), 20, { + from: token_owner + }) + ); + }); + + it("Should fail in modifying the blacklist as the dates are invalid", async() => { + await catchRevert( + I_BlacklistTransferManager.modifyBlacklistType(currentTime.add(new BN(4000)), currentTime.add(new BN(3000)), web3.utils.fromAscii("b_blacklist"), 20, { + from: token_owner + }) + ); + }); + + it("Should fail in modifying the blacklist as the repeat in days is invalid", async() => { + await catchRevert( + I_BlacklistTransferManager.modifyBlacklistType(currentTime.add(new BN(2000)), currentTime.add(new BN(3000)), web3.utils.fromAscii("b_blacklist"), 0, { + from: token_owner + }) + ); + }); + + + it("Should fail in modifying the blacklist as only owner can modify the blacklist", async() => { + await catchRevert( + I_BlacklistTransferManager.modifyBlacklistType(currentTime.add(new BN(1000)), currentTime.add(new BN(3000)), web3.utils.fromAscii("a_blacklist"), 20, { + from: account_investor1 + }) + ); + }); + + it("Should fail in modifying the blacklist as blacklist type doesnot exist", async() => { + await catchRevert( + I_BlacklistTransferManager.modifyBlacklistType(currentTime.add(new BN(1000)), currentTime.add(new BN(3000)), web3.utils.fromAscii("b_blacklist"), 20, { + from: token_owner + }) + ); + }); + + it("Should modify the mutiple blacklist", async() => { + //Add the new blacklist + let startTime = [currentTime.add(new BN(3000)),currentTime.add(new BN(3000))]; + let endTime = [currentTime.add(new BN(5000)),currentTime.add(new BN(7000))]; + let name = [web3.utils.fromAscii("y_blacklist"),web3.utils.fromAscii("z_blacklist")]; + let repeatTime = [15,30]; + let tx = await I_BlacklistTransferManager.modifyBlacklistTypeMulti(startTime, endTime, name, repeatTime, { from: token_owner }); + + let event_data = tx.logs; + for (var i = 0; i < event_data.length; i++) { + let blacklistName = event_data[i].args._blacklistName; + assert.equal(web3.utils.hexToUtf8(blacklistName), web3.utils.hexToUtf8(name[i]), "Failed in adding the blacklist"); + } + }); + + it("Should fail in modifying the mutiple blacklist because only owner can add it", async() => { + //Add the new blacklist + let startTime = [currentTime.add(new BN(3000)),currentTime.add(new BN(3000))]; + let endTime = [currentTime.add(new BN(5000)),currentTime.add(new BN(7000))]; + let name = [web3.utils.fromAscii("y_blacklist"),web3.utils.fromAscii("z_blacklist")]; + let repeatTime = [15,30]; + await catchRevert( + I_BlacklistTransferManager.modifyBlacklistTypeMulti(startTime, endTime, name, repeatTime, { + from: account_investor1 + }) + ); + }); + + it("Should fail in modifying the mutiple blacklist because array length are different", async() => { + //Add the new blacklist + let startTime = [currentTime.add(new BN(3000)),currentTime.add(new BN(3000))]; + let endTime = [currentTime.add(new BN(5000)),currentTime.add(new BN(7000))]; + let name = [web3.utils.fromAscii("y_blacklist"),web3.utils.fromAscii("z_blacklist"),web3.utils.fromAscii("w_blacklist")]; + let repeatTime = [15,30]; + await catchRevert( + I_BlacklistTransferManager.modifyBlacklistTypeMulti(startTime, endTime, name, repeatTime, { + from: token_owner + }) + ); + }); + + it("Should add investor to the blacklist", async() => { + //Add investor to the existing blacklist + let tx = await I_BlacklistTransferManager.addInvestorToBlacklist(account_investor1, web3.utils.fromAscii("a_blacklist"), { from: token_owner }); + assert.equal(tx.logs[0].args._investor.toLowerCase(), account_investor1.toLowerCase(), "Failed in adding the investor to the blacklist"); + + }); + + it("Should fail in adding the investor to the blacklist because only owner can add the investor", async() => { + await catchRevert( + I_BlacklistTransferManager.addInvestorToBlacklist(account_investor2, web3.utils.fromAscii("a_blacklist"), { + from: account_investor1 + }) + ); + }); + + it("Should fail in adding the investor to the blacklist as investor address is invalid", async() => { + await catchRevert( + I_BlacklistTransferManager.addInvestorToBlacklist("0x0000000000000000000000000000000000000000", web3.utils.fromAscii("a_blacklist"), { + from: token_owner + }) + ); + }); + + + it("Should fail in adding the investor to the non existing blacklist", async() => { + await catchRevert( + I_BlacklistTransferManager.addInvestorToBlacklist(account_investor2, web3.utils.fromAscii("b_blacklist"), { + from: token_owner + }) + ); + }); + + it("Should get the list of investors associated to blacklist", async() => { + let perm = await I_BlacklistTransferManager.getListOfAddresses.call(web3.utils.fromAscii("a_blacklist")); + assert.equal(perm.length, 1); + }); + + it("Should fail in getting the list of investors from the non existing blacklist", async() => { + await catchRevert( + I_BlacklistTransferManager.getListOfAddresses.call(web3.utils.fromAscii("b_blacklist")) + ); + }); + + it("Should investor be able to transfer token because current time is less than the blacklist start time", async() => { + //Trasfer tokens + await I_SecurityToken.transfer(account_investor2, web3.utils.toWei('1', 'ether'), { from: account_investor1 }); + assert.equal( + (await I_SecurityToken.balanceOf(account_investor2)).toString(), + web3.utils.toWei('3', 'ether') + ); + }); + + it("Should investor be able to transfer token as it is not in blacklist time period", async() => { + // Jump time + await increaseTime(duration.seconds(4000)); + + //Trasfer tokens + await I_SecurityToken.transfer(account_investor2, web3.utils.toWei('1', 'ether'), { from: account_investor1 }); + assert.equal( + (await I_SecurityToken.balanceOf(account_investor2)).toString(), + web3.utils.toWei('4', 'ether') + ); + }); + + it("Should fail in transfer the tokens as the investor in blacklist", async() => { + // Jump time + await increaseTime(duration.days(20) - 1500); + await catchRevert( + I_SecurityToken.transfer(account_investor2, web3.utils.toWei('1', 'ether'), { + from: account_investor1 + }) + ); + }); + + it("Should investor is able transfer the tokens- because BlacklistTransferManager is paused", async() => { + await I_BlacklistTransferManager.pause({from:token_owner}); + //Trasfer tokens + await I_SecurityToken.transfer(account_investor2, web3.utils.toWei('1', 'ether'), { from: account_investor1 }); + assert.equal( + (await I_SecurityToken.balanceOf(account_investor2)).toString(), + web3.utils.toWei('5', 'ether') + ); + }); + + it("Should investor fail in transfer token as it is in blacklist time period", async() => { + + await I_BlacklistTransferManager.unpause({from:token_owner}); + currentTime = new BN(await latestTime()); + await I_BlacklistTransferManager.addBlacklistType(currentTime.add(new BN(500)), currentTime.add(new BN(4000)), web3.utils.fromAscii("k_blacklist"), 8, { from: token_owner }); + + await I_BlacklistTransferManager.addInvestorToBlacklist(account_investor2, web3.utils.fromAscii("k_blacklist"), { from: token_owner }); + // Jump time + await increaseTime(3500); + + //Trasfer tokens + await catchRevert( + I_SecurityToken.transfer(account_investor3, web3.utils.toWei('1', 'ether'), { + from: account_investor2 + }) + ) + }); + + it("Should investor be able to transfer token as it is not in blacklist time period", async() => { + // Jump time + await increaseTime(1000); + + //Trasfer tokens + await I_SecurityToken.transfer(account_investor3, web3.utils.toWei('1', 'ether'), { from: account_investor2 }); + assert.equal( + (await I_SecurityToken.balanceOf(account_investor3)).toString(), + web3.utils.toWei('3', 'ether') + ); + }); + + it("Should investor fail in transfer token as it is in blacklist time period", async() => { + + // Jump time + await increaseTime(duration.days(8) - 1000); + //Trasfer tokens + await catchRevert( + I_SecurityToken.transfer(account_investor3, web3.utils.toWei('1', 'ether'), { + from: account_investor2 + }) + ); + }); + + it("Should investor fail in transfer token as it is in blacklist time period", async() => { + currentTime = new BN(await latestTime()); + await I_BlacklistTransferManager.addBlacklistType(currentTime.add(new BN(5000)), currentTime.add(new BN(8000)), web3.utils.fromAscii("l_blacklist"), 5, { from: token_owner }); + + await I_BlacklistTransferManager.addInvestorToBlacklist(account_investor3, web3.utils.fromAscii("l_blacklist"), { from: token_owner }); + // Jump time + await increaseTime(5500); + + //Trasfer tokens + await catchRevert( + I_SecurityToken.transfer(account_investor4, web3.utils.toWei('1', 'ether'), { + from: account_investor3 + }) + ); + }); + + it("Should investor be able to transfer token as it is not in blacklist time period", async() => { + // Jump time + await increaseTime(3000); + + //Trasfer tokens + await I_SecurityToken.transfer(account_investor4, web3.utils.toWei('1', 'ether'), { from: account_investor3 }); + assert.equal( + (await I_SecurityToken.balanceOf(account_investor4)).toString(), + web3.utils.toWei('3', 'ether') + ); + }); + + it("Should investor fail in transfer token as it is in blacklist time period", async() => { + + // Jump time + await increaseTime(duration.days(5) - 3000); + //Trasfer tokens + await catchRevert( + I_SecurityToken.transfer(account_investor4, web3.utils.toWei('1', 'ether'), { + from: account_investor3 + }) + ); + }); + + + it("Should delete the blacklist type", async() => { + currentTime = new BN(await latestTime()); + await I_BlacklistTransferManager.addBlacklistType(currentTime.add(new BN(1000)), currentTime.add(new BN(3000)), web3.utils.fromAscii("b_blacklist"), 20, { from: token_owner }); + let tx = await I_BlacklistTransferManager.deleteBlacklistType(web3.utils.fromAscii("b_blacklist"), { from: token_owner }); + assert.equal(web3.utils.hexToUtf8(tx.logs[0].args._blacklistName), "b_blacklist", "Failed in deleting the blacklist"); + + }); + + it("Only owner have the permission to delete the blacklist type", async() => { + currentTime = new BN(await latestTime()); + await I_BlacklistTransferManager.addBlacklistType(currentTime.add(new BN(1000)), currentTime.add(new BN(3000)), web3.utils.fromAscii("b_blacklist"), 20, { from: token_owner }); + await catchRevert( + I_BlacklistTransferManager.deleteBlacklistType(web3.utils.fromAscii("b_blacklist"), { + from: account_investor1 + }) + ); + }); + + it("Should fail in deleting the blacklist type as the blacklist has associated addresses", async() => { + await catchRevert( + I_BlacklistTransferManager.deleteBlacklistType(web3.utils.fromAscii("a_blacklist"), { + from: token_owner + }) + ); + }); + + it("Should fail in deleting the blacklist type as the blacklist doesnot exist", async() => { + await catchRevert( + I_BlacklistTransferManager.deleteBlacklistType(web3.utils.fromAscii("c_blacklist"), { + from: token_owner + }) + ); + }); + + it("Should delete the mutiple blacklist type", async() => { + let name = [web3.utils.fromAscii("y_blacklist"),web3.utils.fromAscii("z_blacklist")]; + let tx = await I_BlacklistTransferManager.deleteBlacklistTypeMulti(name, { from: token_owner }); + + let event_data = tx.logs; + for (var i = 0; i < event_data.length; i++) { + let blacklistName = event_data[i].args._blacklistName; + assert.equal(web3.utils.hexToUtf8(blacklistName), web3.utils.hexToUtf8(name[i]), "Failed in deleting the blacklist"); + } + + }); + + it("Should fail in deleting multiple blacklist type because only owner can do it.", async() => { + let name = [web3.utils.fromAscii("b_blacklist"),web3.utils.fromAscii("a_blacklist")]; + await catchRevert( + I_BlacklistTransferManager.deleteBlacklistTypeMulti(name, { + from: account_investor1 + }) + ); + }); + + it("Should delete the investor from all the associated blacklist", async() => { + currentTime = new BN(await latestTime()); + let data = await I_BlacklistTransferManager.getBlacklistNamesToUser.call(account_investor1); + await I_BlacklistTransferManager.addBlacklistType(currentTime.add(new BN(1000)), currentTime.add(new BN(3000)), web3.utils.fromAscii("g_blacklist"), 20, { from: token_owner }); + await I_BlacklistTransferManager.addInvestorToBlacklist(account_investor1, web3.utils.fromAscii("g_blacklist"), { from: token_owner }); + let tx = await I_BlacklistTransferManager.deleteInvestorFromAllBlacklist(account_investor1, { from: token_owner }); + assert.equal(tx.logs[0].args._investor.toLowerCase(), account_investor1.toLowerCase(), "Failed in deleting the investor from the blacklist"); + }); + + it("Only owner has the permission to delete the investor from all the blacklist type", async() => { + await I_BlacklistTransferManager.addInvestorToBlacklist(account_investor1,web3.utils.fromAscii("g_blacklist"), { from: token_owner }); + await catchRevert( + I_BlacklistTransferManager.deleteInvestorFromAllBlacklist(account_investor1, { + from: account_investor2 + }) + ) + }); + + it("Should fail in deleting the investor from all the associated blacklist as th address is invalid", async() => { + await catchRevert( + I_BlacklistTransferManager.deleteInvestorFromAllBlacklist("0x0000000000000000000000000000000000000000", { + from: token_owner + }) + ); + }); + + it("Should fail in deleting the investor because investor is not associated to any blacklist", async() => { + await catchRevert( + I_BlacklistTransferManager.deleteInvestorFromAllBlacklist(account_investor5, { + from: token_owner + }) + ); + }); + + it("Should delete the mutiple investor from all the associated blacklist", async() => { + await I_BlacklistTransferManager.addInvestorToBlacklist(account_investor5, web3.utils.fromAscii("g_blacklist"), { from: token_owner }); + await I_BlacklistTransferManager.addInvestorToBlacklist(account_investor2, web3.utils.fromAscii("g_blacklist"), { from: token_owner }); + let investor = [account_investor5,account_investor2]; + let tx = await I_BlacklistTransferManager.deleteInvestorFromAllBlacklistMulti(investor, { from: token_owner }); + let event_data = tx.logs; + assert.equal(event_data[0].args._investor, investor[0], "Failed in deleting the blacklist"); + assert.equal(event_data[1].args._investor, investor[1], "Failed in deleting the blacklist"); + assert.equal(event_data[2].args._investor, investor[1], "Failed in deleting the blacklist"); + }); + + it("Should fail in deleting the mutiple investor from all the associated blacklist because only owner can do it.", async() => { + await I_BlacklistTransferManager.addInvestorToBlacklist(account_investor5, web3.utils.fromAscii("g_blacklist"), { from: token_owner }); + await I_BlacklistTransferManager.addInvestorToBlacklist(account_investor2, web3.utils.fromAscii("g_blacklist"), { from: token_owner }); + let investor = [account_investor5,account_investor2]; + await catchRevert( + I_BlacklistTransferManager.deleteInvestorFromAllBlacklistMulti(investor, { + from: account_investor1 + }) + ); + }); + + it("Should delete the mutiple investor from particular associated blacklists", async() => { + await I_BlacklistTransferManager.addBlacklistType(currentTime.add(new BN(1000)), currentTime.add(new BN(3000)), web3.utils.fromAscii("s_blacklist"), 20, { from: token_owner }); + await I_BlacklistTransferManager.addInvestorToBlacklist(account_investor5, web3.utils.fromAscii("s_blacklist"), { from: token_owner }); + // await I_BlacklistTransferManager.addInvestorToBlacklist(account_investor2, "g_blacklist", { from: token_owner }); + let investor = [account_investor5,account_investor2]; + let blacklistName = [web3.utils.fromAscii("s_blacklist"),web3.utils.fromAscii("g_blacklist")]; + let tx = await I_BlacklistTransferManager.deleteMultiInvestorsFromBlacklistMulti(investor,blacklistName, { from: token_owner }); + let event_data = tx.logs; + for (var i = 0; i < event_data.length; i++) { + let investorName = event_data[i].args._investor; + assert.equal(investorName.toLowerCase(), investor[i].toLowerCase(), "Failed in deleting the blacklist"); + } + }); + + it("Should fail in deleting the mutiple investor from particular associated blacklist because only owner can do it.", async() => { + await I_BlacklistTransferManager.addInvestorToBlacklist(account_investor5, web3.utils.fromAscii("s_blacklist"), { from: token_owner }); + await I_BlacklistTransferManager.addInvestorToBlacklist(account_investor2, web3.utils.fromAscii("g_blacklist"), { from: token_owner }); + let investor = [account_investor5,account_investor2]; + let blacklistName = [web3.utils.fromAscii("s_blacklist"),web3.utils.fromAscii("g_blacklist")]; + await catchRevert( + I_BlacklistTransferManager.deleteMultiInvestorsFromBlacklistMulti(investor,blacklistName, { + from: account_investor1 + }) + ); + }); + + it("Should fail in deleting the mutiple investor from particular associated blacklist because array length is incorrect.", async() => { + let investor = [account_investor5]; + let blacklistName = [web3.utils.fromAscii("s_blacklist"),web3.utils.fromAscii("g_blacklist")]; + await catchRevert( + I_BlacklistTransferManager.deleteMultiInvestorsFromBlacklistMulti(investor,blacklistName, { + from: token_owner + }) + ); + }); + + it("Should delete the investor from the blacklist type", async() => { + await I_BlacklistTransferManager.addBlacklistType(currentTime.add(new BN(1000)), currentTime.add(new BN(3000)), web3.utils.fromAscii("f_blacklist"), 20, { from: token_owner }); + await I_BlacklistTransferManager.addInvestorToBlacklist(account_investor1, web3.utils.fromAscii("f_blacklist"), { from: token_owner }); + await I_BlacklistTransferManager.addInvestorToBlacklist(account_investor5, web3.utils.fromAscii("f_blacklist"), { from: token_owner }); + await I_BlacklistTransferManager.addBlacklistType(currentTime.add(new BN(500)), currentTime.add(new BN(8000)), web3.utils.fromAscii("q_blacklist"), 10, { from: token_owner }); + await I_BlacklistTransferManager.addInvestorToBlacklist(account_investor1, web3.utils.fromAscii("q_blacklist"), { from: token_owner }); + let tx = await I_BlacklistTransferManager.deleteInvestorFromBlacklist(account_investor1, web3.utils.fromAscii("f_blacklist"), { from: token_owner }); + assert.equal(tx.logs[0].args._investor.toLowerCase(), account_investor1.toLowerCase(), "Failed in deleting the investor from the blacklist"); + + }); + + it("Only owner can delete the investor from the blacklist type", async() => { + await I_BlacklistTransferManager.addInvestorToBlacklist(account_investor1, web3.utils.fromAscii("f_blacklist"), { from: token_owner }); + await catchRevert( + I_BlacklistTransferManager.deleteInvestorFromBlacklist(account_investor1, web3.utils.fromAscii("f_blacklist"), { + from: account_investor2 + }) + ); + }); + + it("Should fail in deleting the investor because the investor address is invalid", async() => { + await catchRevert( + I_BlacklistTransferManager.deleteInvestorFromBlacklist("0x0000000000000000000000000000000000000000", web3.utils.fromAscii("f_blacklist"), { + from: token_owner + }) + ); + }); + + it("Should fail in deleting the investor because the investor is not associated to blacklist", async() => { + await I_BlacklistTransferManager.deleteInvestorFromBlacklist(account_investor1, web3.utils.fromAscii("f_blacklist"), { from: token_owner }); + await catchRevert( + I_BlacklistTransferManager.deleteInvestorFromBlacklist(account_investor1, web3.utils.fromAscii("f_blacklist"), { + from: token_owner + }) + ); + }); + + it("Should fail in deleting the investor because the blacklist name is invalid", async() => { + await catchRevert( + I_BlacklistTransferManager.deleteInvestorFromBlacklist(account_investor1, web3.utils.fromAscii(""), { + from: token_owner + }) + ); + }); + + it("Should add investor and new blacklist type", async() => { + let tx = await I_BlacklistTransferManager.addInvestorToNewBlacklist(currentTime.add(new BN(1000)), currentTime.add(new BN(3000)), web3.utils.fromAscii("c_blacklist"), 20, account_investor3, { from: token_owner }); + assert.equal(web3.utils.hexToUtf8(tx.logs[0].args._blacklistName), "c_blacklist", "Failed in adding the blacklist"); + assert.equal(tx.logs[1].args._investor, account_investor3, "Failed in adding the investor to blacklist"); + + }); + + it("Should fail in adding the investor and new blacklist type", async() => { + await catchRevert( + I_BlacklistTransferManager.addInvestorToNewBlacklist(currentTime.add(new BN(1000)), currentTime.add(new BN(3000)), web3.utils.fromAscii("c_blacklist"), 20, account_investor3, { + from: account_investor2 + }) + ); + }); + + it("Should add mutiple investor to blacklist", async() => { + await I_BlacklistTransferManager.addBlacklistType(currentTime.add(new BN(1000)), currentTime.add(new BN(3000)), web3.utils.fromAscii("d_blacklist"), 20, { from: token_owner }); + let investor = [account_investor4,account_investor5]; + let tx = await I_BlacklistTransferManager.addInvestorToBlacklistMulti([account_investor4,account_investor5], web3.utils.fromAscii("d_blacklist"), { from: token_owner }); + + let event_data = tx.logs; + for (var i = 0; i < event_data.length; i++) { + let user = event_data[i].args._investor; + assert.equal(user, investor[i], "Failed in adding the investor to blacklist"); + } + + }); + + it("Should fail in adding the mutiple investor to the blacklist", async() => { + await catchRevert( + I_BlacklistTransferManager.addInvestorToBlacklistMulti([account_investor4,account_investor5], web3.utils.fromAscii("b_blacklist"), { + from: account_investor1 + }) + ); + }); + + it("Should add mutiple investor to the mutiple blacklist", async() => { + await I_BlacklistTransferManager.addBlacklistType(currentTime.add(new BN(1000)), currentTime.add(new BN(3000)), web3.utils.fromAscii("m_blacklist"), 20, { from: token_owner }); + await I_BlacklistTransferManager.addBlacklistType(currentTime.add(new BN(1000)), currentTime.add(new BN(3000)), web3.utils.fromAscii("n_blacklist"), 20, { from: token_owner }); + let investor = [account_investor4,account_investor5]; + let blacklistName =[web3.utils.fromAscii("m_blacklist"),web3.utils.fromAscii("n_blacklist")]; + let tx = await I_BlacklistTransferManager.addMultiInvestorToBlacklistMulti(investor, blacklistName, { from: token_owner }); + + let event_data = tx.logs; + for (var i = 0; i < event_data.length; i++) { + let user = event_data[i].args._investor; + let blacklist = event_data[i].args._blacklistName; + assert.equal(user, investor[i], "Failed in adding the investor to blacklist"); + assert.equal(web3.utils.hexToUtf8(blacklist), web3.utils.hexToUtf8(blacklistName[i]), "Failed in adding the investor to blacklist"); + } + + }); + + it("Should fail in adding the mutiple investor to the mutiple blacklist because only owner can do it.", async() => { + let investor = [account_investor4,account_investor5]; + let blacklistName = [ web3.utils.fromAscii("m_blacklist"), web3.utils.fromAscii("n_blacklist")]; + await I_BlacklistTransferManager.deleteMultiInvestorsFromBlacklistMulti(investor,blacklistName, { from: token_owner }); + await catchRevert( + I_BlacklistTransferManager.addMultiInvestorToBlacklistMulti(investor, blacklistName, { + from: account_investor1 + }) + ); + }); + + it("Should fail in adding mutiple investor to the mutiple blacklist because array length is not same", async() => { + let investor = [account_investor4,account_investor5]; + let blacklistName =[web3.utils.fromAscii("m_blacklist")]; + await catchRevert( + I_BlacklistTransferManager.addMultiInvestorToBlacklistMulti(investor, blacklistName, { + from: token_owner + }) + ); + }); + + it("Should get the init function", async() => { + let byte = await I_BlacklistTransferManager.getInitFunction.call(); + assert.equal(web3.utils.toAscii(byte).replace(/\u0000/g, ''), 0); + }); + + it("Should get the permission", async() => { + let perm = await I_BlacklistTransferManager.getPermissions.call(); + assert.equal(perm.length, 1); + }); + }); + + describe("Test cases for the factory", async() => { + it("Should get the exact details of the factory", async() => { + assert.equal(await I_BlacklistTransferManagerFactory.setupCost.call(),0); + assert.equal((await I_BlacklistTransferManagerFactory.getTypes.call())[0],2); + assert.equal(web3.utils.toAscii(await I_BlacklistTransferManagerFactory.getName.call()) + .replace(/\u0000/g, ''), + "BlacklistTransferManager", + "Wrong Module added"); + assert.equal(await I_BlacklistTransferManagerFactory.description.call(), + "Automate blacklist to restrict selling", + "Wrong Module added"); + assert.equal(await I_BlacklistTransferManagerFactory.title.call(), + "Blacklist Transfer Manager", + "Wrong Module added"); + assert.equal(await I_BlacklistTransferManagerFactory.getInstructions.call(), + "Allows an issuer to blacklist the addresses.", + "Wrong Module added"); + assert.equal(await I_BlacklistTransferManagerFactory.version.call(), + "2.1.0", + "Wrong Module added"); + + }); + + it("Should get the tags of the factory", async() => { + let tags = await I_BlacklistTransferManagerFactory.getTags.call(); + assert.equal(web3.utils.toAscii(tags[0]).replace(/\u0000/g, ''),"Blacklist"); + }); + }); + +}); diff --git a/test/z_fuzz_test_adding_removing_modules_ST.js b/test/z_fuzz_test_adding_removing_modules_ST.js new file mode 100644 index 000000000..194287801 --- /dev/null +++ b/test/z_fuzz_test_adding_removing_modules_ST.js @@ -0,0 +1,310 @@ +import latestTime from './helpers/latestTime'; +import {signData} from './helpers/signData'; +import { pk } from './helpers/testprivateKey'; +import { duration, promisifyLogWatch, latestBlock } from './helpers/utils'; +import { takeSnapshot, increaseTime, revertToSnapshot } from './helpers/time'; +import { catchRevert } from "./helpers/exceptions"; +import { setUpPolymathNetwork, + deployGPMAndVerifyed, + deployCountTMAndVerifyed, + deployLockupVolumeRTMAndVerified, + deployPercentageTMAndVerified, + deployManualApprovalTMAndVerifyed +} from "./helpers/createInstances"; +import { encodeModuleCall } from "./helpers/encodeCall"; + +const SecurityToken = artifacts.require('./SecurityToken.sol'); +const GeneralTransferManager = artifacts.require('./GeneralTransferManager'); +const GeneralPermissionManager = artifacts.require('./GeneralPermissionManager'); + +// modules for test +const CountTransferManager = artifacts.require("./CountTransferManager"); +const ManualApprovalTransferManager = artifacts.require('./ManualApprovalTransferManager'); +const VolumeRestrictionTransferManager = artifacts.require('./LockUpTransferManager'); +const PercentageTransferManager = artifacts.require('./PercentageTransferManager'); + + + +const Web3 = require('web3'); +const BN = Web3.utils.BN; +const web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545")) // Hardcoded development port + +contract('GeneralPermissionManager', accounts => { + + // Accounts Variable declaration + let account_polymath; + let account_issuer; + let token_owner; + let token_owner_pk; + let account_investor1; + let account_investor2; + let account_investor3; + let account_investor4; + let account_delegate; + let account_delegate2; + // investor Details + let fromTime = latestTime(); + let toTime = latestTime(); + let expiryTime = toTime + duration.days(15); + + let message = "Transaction Should Fail!"; + + // Contract Instance Declaration + let I_GeneralPermissionManagerFactory; + let P_GeneralPermissionManagerFactory; + let I_SecurityTokenRegistryProxy; + let P_GeneralPermissionManager; + let I_GeneralTransferManagerFactory; + let I_GeneralPermissionManager; + let I_GeneralTransferManager; + let I_ModuleRegistryProxy; + let I_ModuleRegistry; + let I_FeatureRegistry; + let I_SecurityTokenRegistry; + let I_DummySTOFactory; + let I_STFactory; + let I_SecurityToken; + let I_MRProxied; + let I_STRProxied; + let I_PolyToken; + let I_PolymathRegistry; + + + //Define all modules for test + let I_CountTransferManagerFactory; + let I_CountTransferManager; + + let I_ManualApprovalTransferManagerFactory; + let I_ManualApprovalTransferManager; + + let I_VolumeRestrictionTransferManagerFactory; + let I_VolumeRestrictionTransferManager; + + let I_PercentageTransferManagerFactory; + let I_PercentageTransferManager; + + // SecurityToken Details + const name = "Team"; + const symbol = "sap"; + const tokenDetails = "This is equity type of issuance"; + const decimals = 18; + const contact = "team@polymath.network"; + const delegateDetails = "Hello I am legit delegate"; + const STVRParameters = ["bool", "uint256", "bool"]; + + // Module key + const delegateManagerKey = 1; + const transferManagerKey = 2; + const stoKey = 3; + + // Initial fee for ticker registry and security token registry + const initRegFee = web3.utils.toWei("250"); + + let _details = "details holding for test"; + let testRepeat = 20; + + // define factories and modules for fuzz test + var factoriesAndModules = [ + { factory: 'I_CountTransferManagerFactory', module: 'CountTransferManager'}, + { factory: 'I_ManualApprovalTransferManagerFactory', module: 'ManualApprovalTransferManager'}, + { factory: 'I_VolumeRestrictionTransferManagerFactory', module: 'VolumeRestrictionTransferManager'}, + { factory: 'I_PercentageTransferManagerFactory', module: 'PercentageTransferManager'}, + ]; + + let totalModules = factoriesAndModules.length; + let bytesSTO; + + + before(async () => { + // Accounts setup + account_polymath = accounts[0]; + account_issuer = accounts[1]; + + token_owner = account_issuer; + token_owner_pk = pk.account_1; + + account_investor1 = accounts[8]; + account_investor2 = accounts[9]; + account_investor3 = accounts[5]; + account_investor4 = accounts[6]; + account_delegate = accounts[7]; + // account_delegate2 = accounts[6]; + + + // Step 1: Deploy the genral PM ecosystem + let instances = await setUpPolymathNetwork(account_polymath, token_owner); + + [ + I_PolymathRegistry, + I_PolyToken, + I_FeatureRegistry, + I_ModuleRegistry, + I_ModuleRegistryProxy, + I_MRProxied, + I_GeneralTransferManagerFactory, + I_STFactory, + I_SecurityTokenRegistry, + I_SecurityTokenRegistryProxy, + I_STRProxied + ] = instances; + + // STEP 5: Deploy the GeneralDelegateManagerFactory + [I_GeneralPermissionManagerFactory] = await deployGPMAndVerifyed(account_polymath, I_MRProxied, 0); + // STEP 6: Deploy the GeneralDelegateManagerFactory + [P_GeneralPermissionManagerFactory] = await deployGPMAndVerifyed(account_polymath, I_MRProxied, web3.utils.toWei("500")); + + [I_CountTransferManagerFactory] = await deployCountTMAndVerifyed(account_polymath, I_MRProxied, 0); + + // Deploy Modules + [I_CountTransferManagerFactory] = await deployCountTMAndVerifyed(account_polymath, I_MRProxied, 0); + [I_ManualApprovalTransferManagerFactory] = await deployManualApprovalTMAndVerifyed(account_polymath, I_MRProxied, 0); + [I_VolumeRestrictionTransferManagerFactory] = await deployLockupVolumeRTMAndVerified(account_polymath, I_MRProxied, 0); + [I_PercentageTransferManagerFactory] = await deployPercentageTMAndVerified(account_polymath, I_MRProxied, 0); + + // Printing all the contract addresses + console.log(` + --------------------- Polymath Network Smart Contracts: --------------------- + PolymathRegistry: ${I_PolymathRegistry.address} + SecurityTokenRegistryProxy: ${I_SecurityTokenRegistryProxy.address} + SecurityTokenRegistry: ${I_SecurityTokenRegistry.address} + ModuleRegistryProxy ${I_ModuleRegistryProxy.address} + ModuleRegistry: ${I_ModuleRegistry.address} + FeatureRegistry: ${I_FeatureRegistry.address} + + STFactory: ${I_STFactory.address} + GeneralTransferManagerFactory: ${I_GeneralTransferManagerFactory.address} + GeneralPermissionManagerFactory: ${I_GeneralPermissionManagerFactory.address} + ----------------------------------------------------------------------------- + `); + }); + + describe("Generate the SecurityToken", async () => { + it("Should register the ticker before the generation of the security token", async () => { + await I_PolyToken.approve(I_STRProxied.address, initRegFee, { from: token_owner }); + let tx = await I_STRProxied.registerTicker(token_owner, symbol, contact, { from: token_owner }); + assert.equal(tx.logs[0].args._owner, token_owner); + assert.equal(tx.logs[0].args._ticker, symbol.toUpperCase()); + }); + + it("Should generate the new security token with the same symbol as registered above", async () => { + await I_PolyToken.approve(I_STRProxied.address, initRegFee, { from: token_owner }); + let _blockNo = latestBlock(); + let tx = await I_STRProxied.generateSecurityToken(name, symbol, tokenDetails, false, { from: token_owner }); + + // Verify the successful generation of the security token + assert.equal(tx.logs[2].args._ticker, symbol.toUpperCase(), "SecurityToken doesn't get deployed"); + + I_SecurityToken = await SecurityToken.at(tx.logs[2].args._securityTokenAddress); + + const log = (await I_SecurityToken.getPastEvents('ModuleAdded', {filter: {transactionHash: tx.transactionHash}}))[0]; + + // Verify that GeneralTransferManager module get added successfully or not + assert.equal(log.args._types[0].toNumber(), 2); + assert.equal(web3.utils.toAscii(log.args._name).replace(/\u0000/g, ""), "GeneralTransferManager"); + }); + + it("Should intialize the auto attached modules", async () => { + let moduleData = (await I_SecurityToken.getModulesByType(2))[0]; + I_GeneralTransferManager = await GeneralTransferManager.at(moduleData); + }); + + it("Should successfully attach the General permission manager factory with the security token -- failed because Token is not paid", async () => { + let errorThrown = false; + await I_PolyToken.getTokens(web3.utils.toWei("500", "ether"), token_owner); + await catchRevert( + I_SecurityToken.addModule(P_GeneralPermissionManagerFactory.address, "0x0", web3.utils.toWei("500", "ether"), 0, { from: token_owner }) + ); + }); + + it("Should successfully attach the General permission manager factory with the security token", async () => { + let snapId = await takeSnapshot(); + await I_PolyToken.transfer(I_SecurityToken.address, web3.utils.toWei("500", "ether"), { from: token_owner }); + const tx = await I_SecurityToken.addModule( + P_GeneralPermissionManagerFactory.address, + "0x0", + web3.utils.toWei("500", "ether"), + 0, + { from: token_owner } + ); + assert.equal(tx.logs[3].args._types[0].toNumber(), delegateManagerKey, "General Permission Manager doesn't get deployed"); + assert.equal( + web3.utils.toAscii(tx.logs[3].args._name).replace(/\u0000/g, ""), + "GeneralPermissionManager", + "GeneralPermissionManagerFactory module was not added" + ); + P_GeneralPermissionManager = await GeneralPermissionManager.at(tx.logs[3].args._module); + await revertToSnapshot(snapId); + }); + + it("Should successfully attach the General permission manager factory with the security token", async () => { + const tx = await I_SecurityToken.addModule(I_GeneralPermissionManagerFactory.address, "0x0", 0, 0, { from: token_owner }); + assert.equal(tx.logs[2].args._types[0].toNumber(), delegateManagerKey, "General Permission Manager doesn't get deployed"); + assert.equal( + web3.utils.toAscii(tx.logs[2].args._name).replace(/\u0000/g, ""), + "GeneralPermissionManager", + "GeneralPermissionManagerFactory module was not added" + ); + I_GeneralPermissionManager = await GeneralPermissionManager.at(tx.logs[2].args._module); + }); + }); + + + + describe("adding and removing different modules", async () => { + + it("should pass test for randomly adding and removing modules ", async () => { + + console.log("1"); + // fuzz test loop over total times of testRepeat + for (var i = 0; i < testRepeat; i++) { + + console.log("1.2"); + + // choose a random module with in the totalMoudules available + let random = factoriesAndModules[Math.floor(Math.random() * Math.floor(totalModules))]; + let randomFactory = eval(random.factory); + let randomModule = eval(random.module); + console.log("choosen factory "+ random.factory); + console.log("choosen module "+ random.module); + + //calculate the data needed for different modules + if (random.module == 'CountTransferManager' || random.module == 'ManualApprovalTransferManager' || random.module == 'VolumeRestrictionTransferManager' ){ + const holderCount = 2; // Maximum number of token holders + bytesSTO = encodeModuleCall(["uint256"], [holderCount]); + } else if (random.module == 'PercentageTransferManager'){ + console.log("PTM 01"); + const holderPercentage = 70 * 10**16; + bytesSTO = web3.eth.abi.encodeFunctionCall({ + name: 'configure', + type: 'function', + inputs: [{ + type: 'uint256', + name: '_maxHolderPercentage' + },{ + type: 'bool', + name: '_allowPrimaryIssuance' + } + ] + }, [holderPercentage, false]); + console.log("encoded."); + } else { + console.log("no data defined for choosen module "+random.module); + } + + // attach it to the ST + let tx = await I_SecurityToken.addModule(randomFactory.address, bytesSTO, 0, 0, { from: token_owner }); + console.log("1.3"); + let randomModuleInstance = await randomModule.at(tx.logs[2].args._module); + console.log("successfully attached module " + randomModuleInstance.address); + + // remove it from the ST + tx = await I_SecurityToken.archiveModule(randomModuleInstance.address, { from: token_owner }); + console.log("1.4"); + tx = await I_SecurityToken.removeModule(randomModuleInstance.address, { from: token_owner }); + console.log("successfully removed module " + randomModuleInstance.address); + + } + }) + }); + +}); diff --git a/test/z_fuzzer_volumn_restriction_transfer_manager.js b/test/z_fuzzer_volumn_restriction_transfer_manager.js new file mode 100644 index 000000000..0cf2fc02f --- /dev/null +++ b/test/z_fuzzer_volumn_restriction_transfer_manager.js @@ -0,0 +1,723 @@ +import latestTime from './helpers/latestTime'; +import {signData} from './helpers/signData'; +import { pk } from './helpers/testprivateKey'; +import { duration, promisifyLogWatch, latestBlock } from './helpers/utils'; +import { takeSnapshot, increaseTime, revertToSnapshot } from './helpers/time'; +import { catchRevert } from "./helpers/exceptions"; +import { setUpPolymathNetwork, deployVRTMAndVerifyed } from "./helpers/createInstances"; + +const SecurityToken = artifacts.require('./SecurityToken.sol'); +const GeneralTransferManager = artifacts.require('./GeneralTransferManager.sol'); +const VolumeRestrictionTM = artifacts.require('./VolumeRestrictionTM.sol'); + +const Web3 = require('web3'); +const BN = Web3.utils.BN; +const web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545")) // Hardcoded development port + +contract('VolumeRestrictionTransferManager', accounts => { + + // Accounts Variable declaration + let account_polymath; + let account_issuer; + let token_owner; + let token_owner_pk; + let account_investor1; + let account_investor2; + let account_investor3; + let account_investor4; + let account_delegate; + let account_delegate2; + let account_delegate3; + // investor Details + let fromTime = currentTime; + let toTime = currentTime; + let expiryTime = toTime + duration.days(15); + + let message = "Transaction Should Fail!"; + + // Contract Instance Declaration + let I_VolumeRestrictionTMFactory; + let P_VolumeRestrictionTMFactory; + let I_SecurityTokenRegistryProxy; + let P_VolumeRestrictionTM; + let I_GeneralTransferManagerFactory; + let I_VolumeRestrictionTM; + let I_GeneralTransferManager; + let I_ModuleRegistryProxy; + let I_ModuleRegistry; + let I_FeatureRegistry; + let I_SecurityTokenRegistry; + let I_DummySTOFactory; + let I_STFactory; + let I_SecurityToken; + let I_MRProxied; + let I_STRProxied; + let I_PolyToken; + let I_PolymathRegistry; + + // SecurityToken Details + const name = "Team"; + const symbol = "sap"; + const tokenDetails = "This is equity type of issuance"; + const decimals = 18; + const contact = "team@polymath.network"; + const delegateDetails = "Hello I am legit delegate"; + + // Module key + const delegateManagerKey = 1; + const transferManagerKey = 2; + const stoKey = 3; + + let tempAmount = new BN(0); + let tempArray = new Array(); + let tempArray3 = new Array(); + let tempArrayGlobal = new Array(); + + // Initial fee for ticker registry and security token registry + const initRegFee = web3.utils.toWei("250"); + + async function print(data, account) { + console.log(` + Latest timestamp: ${data[0].toString()} + SumOfLastPeriod: ${data[1].dividedBy(new BN(10).pow(18)).toString()} + Days Covered: ${data[2].toString()} + Latest timestamp daily: ${data[3].toString()} + Individual Total Trade on latestTimestamp : ${(await I_VolumeRestrictionTM.getTotalTradedByUser.call(account, data[0])) + .dividedBy(new BN(10).pow(18)).toString()} + Individual Total Trade on daily latestTimestamp : ${(await I_VolumeRestrictionTM.getTotalTradedByUser.call(account, data[3])) + .dividedBy(new BN(10).pow(18)).toString()} + `) + } + + async function calculateSum(rollingPeriod, tempArray) { + let sum = 0; + let start = 0; + if (tempArray.length >= rollingPeriod) + start = tempArray.length - rollingPeriod; + for (let i = start; i < tempArray.length; i++) { + sum += tempArray[i]; + } + return sum; + } + + async function printIR(data) { + console.log(` + Allowed Tokens : ${web3.utils.fromWei(data[0])} + StartTime : ${data[1].toString()} + Rolling Period : ${data[2].toString()} + EndTime : ${data[3].toString()} + Restriction Type: ${data[4].toString() == 0 ? "Fixed" : "Percentage"} + `) + } + let currentTime; + before(async() => { + // Accounts setup + currentTime = new BN(await latestTime()); + account_polymath = accounts[0]; + account_issuer = accounts[1]; + + token_owner = account_issuer; + token_owner_pk = pk.account_1; + + account_investor1 = accounts[8]; + account_investor2 = accounts[9]; + account_investor3 = accounts[4]; + account_investor4 = accounts[3]; + account_delegate = accounts[7]; + account_delegate2 = accounts[6]; + account_delegate3 = accounts[5]; + + // Step 1: Deploy the genral PM ecosystem + let instances = await setUpPolymathNetwork(account_polymath, token_owner); + + [ + I_PolymathRegistry, + I_PolyToken, + I_FeatureRegistry, + I_ModuleRegistry, + I_ModuleRegistryProxy, + I_MRProxied, + I_GeneralTransferManagerFactory, + I_STFactory, + I_SecurityTokenRegistry, + I_SecurityTokenRegistryProxy, + I_STRProxied + ] = instances; + + // STEP 5: Deploy the VolumeRestrictionTMFactory + [I_VolumeRestrictionTMFactory] = await deployVRTMAndVerifyed(account_polymath, I_MRProxied, 0); + // STEP 6: Deploy the VolumeRestrictionTMFactory + [P_VolumeRestrictionTMFactory] = await deployVRTMAndVerifyed(account_polymath, I_MRProxied, web3.utils.toWei("500")); + + // Printing all the contract addresses + console.log(` + --------------------- Polymath Network Smart Contracts: --------------------- + PolymathRegistry: ${I_PolymathRegistry.address} + SecurityTokenRegistryProxy: ${I_SecurityTokenRegistryProxy.address} + SecurityTokenRegistry: ${I_SecurityTokenRegistry.address} + ModuleRegistryProxy ${I_ModuleRegistryProxy.address} + ModuleRegistry: ${I_ModuleRegistry.address} + FeatureRegistry: ${I_FeatureRegistry.address} + + STFactory: ${I_STFactory.address} + GeneralTransferManagerFactory: ${I_GeneralTransferManagerFactory.address} + VolumeRestrictionTMFactory: ${I_VolumeRestrictionTMFactory.address} + ----------------------------------------------------------------------------- + `); + }); + + describe("Generate the SecurityToken", async () => { + it("Should register the ticker before the generation of the security token", async () => { + await I_PolyToken.approve(I_STRProxied.address, initRegFee, { from: token_owner }); + let tx = await I_STRProxied.registerTicker(token_owner, symbol, contact, { from: token_owner }); + assert.equal(tx.logs[0].args._owner, token_owner); + assert.equal(tx.logs[0].args._ticker, symbol.toUpperCase()); + }); + + it("Should generate the new security token with the same symbol as registered above", async () => { + await I_PolyToken.approve(I_STRProxied.address, initRegFee, { from: token_owner }); + + let tx = await I_STRProxied.generateSecurityToken(name, symbol, tokenDetails, true, { from: token_owner }); + console.log(tx.logs); + + // Verify the successful generation of the security token + assert.equal(tx.logs[2].args._ticker, symbol.toUpperCase(), "SecurityToken doesn't get deployed"); + + I_SecurityToken = await SecurityToken.at(tx.logs[2].args._securityTokenAddress); + + const log = (await I_SecurityToken.getPastEvents('ModuleAdded', {filter: {transactionHash: tx.transactionHash}}))[0]; + + // Verify that GeneralTransferManager module get added successfully or not + assert.equal(log.args._types[0].toString(), 2); + assert.equal(web3.utils.toAscii(log.args._name).replace(/\u0000/g, ""), "GeneralTransferManager"); + }); + + it("Should intialize the auto attached modules", async () => { + let moduleData = (await I_SecurityToken.getModulesByType(2))[0]; + I_GeneralTransferManager = await GeneralTransferManager.at(moduleData); + }); + }); + + describe("Attach the VRTM", async() => { + it("Deploy the VRTM and attach with the ST", async()=> { + let tx = await I_SecurityToken.addModule(I_VolumeRestrictionTMFactory.address, "0x0", 0, 0, {from: token_owner }); + assert.equal(tx.logs[2].args._moduleFactory, I_VolumeRestrictionTMFactory.address); + assert.equal( + web3.utils.toUtf8(tx.logs[2].args._name), + "VolumeRestrictionTM", + "VolumeRestrictionTMFactory doesn not added"); + I_VolumeRestrictionTM = await VolumeRestrictionTM.at(tx.logs[2].args._module); + }); + + it("Transfer some tokens to different account", async() => { + // Add tokens in to the whitelist + currentTime = new BN(await latestTime()); + await I_GeneralTransferManager.modifyWhitelistMulti( + [account_investor1, account_investor2, account_investor3], + [currentTime, currentTime, currentTime], + [currentTime, currentTime, currentTime], + [currentTime.add(new BN(duration.days(60))), currentTime.add(new BN(duration.days(60))), currentTime.add(new BN(duration.days(60)))], + [true, true, true], + { + from: token_owner + } + ); + + // Mint some tokens and transferred to whitelisted addresses + await I_SecurityToken.mint(account_investor1, web3.utils.toWei("100", "ether"), {from: token_owner}); + await I_SecurityToken.mint(account_investor2, web3.utils.toWei("30", "ether"), {from: token_owner}); + await I_SecurityToken.mint(account_investor3, web3.utils.toWei("30", "ether"), {from: token_owner}); + + }); + + }); + + describe("Fuzz test", async () => { + + it("Should work with multiple transaction within 1 day with Individual and daily Restrictions", async() => { + // let snapId = await takeSnapshot(); + + var testRepeat = 0; + + for (var i = 0; i < testRepeat; i++) { + + console.log("fuzzer number " + i); + + var individualRestrictTotalAmount = Math.floor(Math.random() * 10); + if ( individualRestrictTotalAmount == 0 ) { + individualRestrictTotalAmount = 1; + } + + var dailyRestrictionAmount = Math.floor(Math.random() * 10); + if ( dailyRestrictionAmount == 0 ) { + dailyRestrictionAmount = 1; + } + var rollingPeriod = 2; + var sumOfLastPeriod = 0; + + console.log("a"); + currentTime = new BN(await latestTime()); + // 1 - add individual restriction with a random number + let tx = await I_VolumeRestrictionTM.addIndividualRestriction( + account_investor1, + web3.utils.toWei(individualRestrictTotalAmount.toString()), + currentTime.add(new BN(duration.seconds(2))), + rollingPeriod, + currentTime.add(new BN(duration.days(3))), + 0, + { + from: token_owner + } + ); + currentTime = new BN(await latestTime()); + console.log("b"); + tx = await I_VolumeRestrictionTM.addIndividualDailyRestriction( + account_investor1, + web3.utils.toWei(dailyRestrictionAmount.toString()), + currentTime.add(new BN(duration.seconds(1))), + currentTime.add(new BN(duration.days(4))), + 0, + { + from: token_owner + } + ); + + console.log("c"); + var txNumber = 10; // define fuzz test amount for tx within 24 hrs + currentTime = new BN(await latestTime()); + for (var j=0; j individualRestrictTotalAmount || accumulatedTxValue > dailyRestrictionAmount) { + console.log("tx should fail"); + + await catchRevert( + I_SecurityToken.transfer(account_investor3, web3.utils.toWei(transactionAmount.toString()), {from: account_investor1}) + ); + + console.log("tx failed as expected due to over limit"); + + } else if (accumulatedTxValue <= individualRestrictTotalAmount && accumulatedTxValue <= dailyRestrictionAmount) { + console.log("tx should succeed"); + + await I_SecurityToken.transfer(account_investor3, web3.utils.toWei(transactionAmount.toString()), {from: account_investor1}); + + sumOfLastPeriod = sumOfLastPeriod + transactionAmount; + + console.log("tx succeeded"); + } + console.log("2"); + } + + // await revertToSnapshot(snapId); + await I_VolumeRestrictionTM.removeIndividualRestriction(account_investor1, {from: token_owner}); + await I_VolumeRestrictionTM.removeIndividualDailyRestriction(account_investor1, {from: token_owner}); + } + }); + + it("Should work with fuzz test for individual restriction and general restriction", async() => { + // let snapId = await takeSnapshot(); + var testRepeat = 0; + + for (var i = 0; i < testRepeat; i++) { + + console.log("fuzzer number " + i); + + var individualRestrictTotalAmount = Math.floor(Math.random() * 10); + if (individualRestrictTotalAmount == 0 ) { + individualRestrictTotalAmount = 1; + } + var defaultRestrictionAmount = Math.floor(Math.random() * 10); + var rollingPeriod = 2; + var sumOfLastPeriod = 0; + + console.log("a"); + currentTime = new BN(await latestTime()); + // 1 - add individual restriction with a random number + let tx = await I_VolumeRestrictionTM.addIndividualRestriction( + account_investor1, + web3.utils.toWei(individualRestrictTotalAmount.toString()), + currentTime.add(new BN(duration.seconds(1))), + rollingPeriod, + currentTime.add(new BN(duration.days(3))), + 0, + { + from: token_owner + } + ); + currentTime = new BN(await latestTime()); + console.log("b"); + tx = await I_VolumeRestrictionTM.addDefaultRestriction( + account_investor1, + currentTime.add(new BN(duration.seconds(1))), + rollingPeriod, + currentTime.add(new BN(duration.days(4))), + 0, + { + from: token_owner + } + ); + currentTime = new BN(await latestTime()); + console.log("c"); + var txNumber = 10; // define fuzz test amount for tx + + for (var j = 0; j < txNumber; j++) { + await increaseTime(duration.seconds(5)); + currentTime = new BN(await latestTime()); + console.log("2"); + + // generate a random amount + var transactionAmount = Math.floor(Math.random() * 10); + var accumulatedTxValue = transactionAmount + sumOfLastPeriod; + + console.log("sumOfLastPeriod is " + sumOfLastPeriod + " transactionAmount is " + transactionAmount + " individualRestrictTotalAmount is " + individualRestrictTotalAmount + " defaultRestrictionAmount is " + defaultRestrictionAmount); + + + // check against daily and total restrictions to determine if the transaction should pass or not + if (accumulatedTxValue > individualRestrictTotalAmount || accumulatedTxValue > defaultRestrictionAmount) { + console.log("tx should fail"); + + await catchRevert( + I_SecurityToken.transfer(account_investor3, web3.utils.toWei(transactionAmount.toString()), {from: account_investor1}) + ); + + console.log("tx failed as expected due to over limit"); + } else if (accumulatedTxValue <= individualRestrictTotalAmount) { + + console.log("tx should succeed"); + + await I_SecurityToken.transfer(account_investor3, web3.utils.toWei(transactionAmount.toString()), {from: account_investor1}); + + sumOfLastPeriod = sumOfLastPeriod + transactionAmount; + + console.log("tx succeeded"); + } + console.log("3"); + }; + + + // remove individual restriction and it should fall to default restriction + await I_VolumeRestrictionTM.removeIndividualRestriction(account_investor1, {from: token_owner}); + console.log("individual restriction now removed --> fall back to default restriction"); + + for (var j=0; j defaultRestrictionAmount) { + console.log("tx should fail"); + await catchRevert( + I_SecurityToken.transfer(account_investor3, web3.utils.toWei(transactionAmount.toString()), {from: account_investor1}) + ); + console.log("tx failed as expected due to over limit"); + } else if ( accumulatedTxValue <= defaultRestrictionAmount ) { + + console.log("tx should succeed"); + + await I_SecurityToken.transfer(account_investor3, web3.utils.toWei(transactionAmount.toString()), {from: account_investor1}); + + sumOfLastPeriod = sumOfLastPeriod + transactionAmount; + + console.log("tx succeeded"); + } + console.log("5"); + } + + + // await revertToSnapshot(snapId); + await I_VolumeRestrictionTM.removeDefaultRestriction(account_investor1, {from: token_owner}); + } + + }); + + + + it("Should work with fuzz test for randomly adding / removing individual daily restriction and perform multipel transactions", async() => { + + + var testRepeat = 0; + var txNumber = 10; + var dailyRestriction = false; + var startTime = 1; + var sumOfLastPeriod = 0; + var accumulatedTimeIncrease = 0; + var dailyLimitUsed = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + + for (var i = 0; i < testRepeat; i++) { + + // randomly add or removing existing daily restriction + var random_action = Math.random() >= 0.5; // true -> add false -> remove + + if (dailyRestriction === false && random_action === true) { + console.log("1"); + + var dailyRestrictionAmount = Math.floor(Math.random() * 10); + if (dailyRestrictionAmount === 0) { + dailyRestrictionAmount = 1; + } + + // add daily restriction + let tx = await I_VolumeRestrictionTM.addIndividualDailyRestriction( + account_investor1, + web3.utils.toWei(dailyRestrictionAmount.toString()), + currentTime.add(new BN(duration.seconds(startTime))), + currentTime.add(new BN(duration.days(50))), + 0, + { + from: token_owner + } + ); + + dailyRestriction = true; + + console.log("added daily restriction"); + } else if (dailyRestriction === true && random_action === false) { + console.log("2"); + // remove daily restriction + await I_VolumeRestrictionTM.removeIndividualDailyRestriction(account_investor1, {from: token_owner}); + console.log("removed daily restriction"); + + dailyRestriction = false; + } + + // perform multiple transactions + + for (var j = 0; j < txNumber; j++) { + var timeIncreaseBetweenTx = Math.floor(Math.random() * 10) * 3600; + + await increaseTime(duration.seconds(timeIncreaseBetweenTx)); + currentTime = new BN(await latestTime()); + accumulatedTimeIncrease = timeIncreaseBetweenTx + accumulatedTimeIncrease; + console.log("4"); + + // generate a random amount + var transactionAmount = Math.floor(Math.random() * 10); + + // check today's limit + var dayNumber = Math.floor(accumulatedTimeIncrease/(24*3600)) + 1; + + var todayLimitUsed = dailyLimitUsed[dayNumber]; + + console.log("todayLimitUsed is " + todayLimitUsed + " transactionAmount is " + transactionAmount + " dayNumber is " + dayNumber + " dailyRestrictionAmount is " + dailyRestrictionAmount); + + // check against daily and total restrictions to determine if the transaction should pass or not + if ((todayLimitUsed + transactionAmount) > dailyRestrictionAmount) { + console.log("tx should fail"); + + await catchRevert( + I_SecurityToken.transfer(account_investor3, web3.utils.toWei(transactionAmount.toString()), {from: account_investor1}) + ); + + console.log("tx failed as expected due to over limit"); + } else if ((todayLimitUsed + transactionAmount) <= dailyRestrictionAmount) { + + console.log("tx should succeed"); + + await I_SecurityToken.transfer(account_investor3, web3.utils.toWei(transactionAmount.toString()), {from: account_investor1}); + + dailyLimitUsed[dayNumber] = dailyLimitUsed[dayNumber] + transactionAmount; + + console.log("tx succeeded"); + } + console.log("5"); + } + + if (dailyRestriction === true) { + + // remove daily restriction + await I_VolumeRestrictionTM.removeIndividualDailyRestriction(account_investor1, {from: token_owner}); + console.log("removed daily restriction"); + } + + } + + }); + + + + + it("should work in all cases if a sender is added in the exception list", async () => { + var testRepeat = 0; + + for (var i = 0; i < testRepeat; i++) { + console.log("fuzzer number " + i); + + var individualRestrictTotalAmount = Math.floor(Math.random() * 10); + if (individualRestrictTotalAmount === 0 ) { + individualRestrictTotalAmount = 1; + } + var defaultRestrictionAmount = Math.floor(Math.random() * 10); + var rollingPeriod = 2; + var sumOfLastPeriod = 0; + + console.log("a"); + + // 1 - add individual restriction with a random number + let tx = await I_VolumeRestrictionTM.addIndividualRestriction( + account_investor1, + web3.utils.toWei(individualRestrictTotalAmount.toString()), + currentTime.add(new BN(duration.seconds(1))), + rollingPeriod, + currentTime.add(new BN(duration.days(3))), + 0, + { + from: token_owner + } + ); + + tx = await I_VolumeRestrictionTM.changeExemptWalletList(account_investor1, true, {from: token_owner}); + + console.log("b"); + + var txNumber = 10; // define fuzz test amount for tx + + for (var j = 0; j < txNumber; j++) { + await increaseTime(duration.seconds(5)); + currentTime = new BN(await latestTime()); + console.log("2"); + + // generate a random amount + var transactionAmount = Math.floor(Math.random() * 10); + var accumulatedTxValue = transactionAmount + sumOfLastPeriod; + + console.log("sumOfLastPeriod is " + sumOfLastPeriod + " transactionAmount is " + transactionAmount + " individualRestrictTotalAmount is " + individualRestrictTotalAmount + " defaultRestrictionAmount is " + defaultRestrictionAmount); + + // check against daily and total restrictions to determine if the transaction should pass or not + if (accumulatedTxValue > individualRestrictTotalAmount) { + console.log("tx should fail but still succeed due to investor in exempt list"); + + await I_SecurityToken.transfer(account_investor3, web3.utils.toWei(transactionAmount.toString()), {from: account_investor1}); + + console.log("tx passed as expected"); + } else if (accumulatedTxValue <= individualRestrictTotalAmount) { + + console.log("tx should succeed"); + + await I_SecurityToken.transfer(account_investor3, web3.utils.toWei(transactionAmount.toString()), {from: account_investor1}); + + sumOfLastPeriod = sumOfLastPeriod + transactionAmount; + + console.log("tx succeeded"); + } + console.log("3" + txNumber); + }; + + await I_VolumeRestrictionTM.removeIndividualRestriction(account_investor1, {from: token_owner}); + console.log("removed daily restriction"); + } + }); + + it("Should work if IR is modified", async () => { + + console.log(`\t\t Starting of the IR modification test case`.blue); + + var testRepeat = 1; + + for (var i = 0; i < testRepeat; i++) { + console.log("\t\t fuzzer number " + i); + let precision = 100; + var individualRestrictionTotalAmount = Math.floor(Math.random() * (10 * precision - 1 * precision) + 1 * precision) / (1*precision); + var rollingPeriod = 2; + var sumOfLastPeriod = 0; + + console.log(`\t\t Add individual restriction with TotalAmount: ${individualRestrictionTotalAmount}\n`.green); + + // 1 - add individual restriction with a random number + let tx = await I_VolumeRestrictionTM.addIndividualRestriction( + account_investor1, + web3.utils.toWei(individualRestrictionTotalAmount.toString()), + currentTime.add(new BN(duration.days(2))), + rollingPeriod, + currentTime.add(new BN(duration.days(5))), + 0, + { + from: token_owner + } + ); + + console.log(`\t\t Restriction successfully added \n`); + + var txNumber = 10; // define fuzz test amount for tx + + for (var j = 0; j < txNumber; j++) { + console.log(`\t\t Test number: ${j}\n`); + + // modify IR + var newIR = Math.floor(Math.random() * (10 * precision - 1 * precision) + 1 * precision) / (1*precision); + console.log("Original Restriction"); + printIR(await I_VolumeRestrictionTM.getIndividualRestriction(account_investor1, {from: token_owner})); + currentTime = new BN(await latestTime()); + console.log(`\t\t Modification of the IR with new startTime: ${currentTime + duration.days(1+j)} and new total amount: ${newIR} `.green); + + await I_VolumeRestrictionTM.modifyIndividualRestriction( + account_investor1, + web3.utils.toWei(newIR.toString()), + currentTime.add(new BN(duration.days(1+j))), + rollingPeriod, + currentTime.add(new BN(duration.days(5+j))), + 0, + { from: token_owner } + ); + console.log("Modified Restriction"); + printIR(await I_VolumeRestrictionTM.getIndividualRestriction(account_investor1, {from: token_owner})); + console.log(`\t\t Successfully IR modified`); + let snapId = await takeSnapshot(); + await increaseTime(duration.days(2+j)); + + // generate a random amount + var transactionAmount = Math.floor(Math.random() * (10 * precision - 1 * precision) + 1 * precision) / (1*precision); + + // check against daily and total restrictions to determine if the transaction should pass or not + console.log("Transaction Amount: " + transactionAmount); + console.log("newIR Amount: " + newIR); + console.log("currentTime: " + await latestTime()); + if (transactionAmount > newIR) { + console.log("\t\t Tx should fail"); + + await catchRevert ( + I_SecurityToken.transfer(account_investor3, web3.utils.toWei(transactionAmount.toString()), {from: account_investor1}) + ); + + console.log("\t\t Tx failed as expected"); + } else if (transactionAmount <= newIR) { + + console.log("\t\t Tx should succeed"); + + await I_SecurityToken.transfer(account_investor3, web3.utils.toWei(transactionAmount.toString()), {from: account_investor1}); + + console.log("\t\t Tx succeeded"); + } + await revertToSnapshot(snapId); + console.log("\t\t Finished test number "+j); + }; + + await I_VolumeRestrictionTM.removeIndividualRestriction(account_investor1, {from: token_owner}); + console.log("\t\t Removed daily restriction"); + } + + }); + + }); + +}); diff --git a/test/z_general_permission_manager_fuzzer.js b/test/z_general_permission_manager_fuzzer.js index 2614e6d5d..7f7933801 100644 --- a/test/z_general_permission_manager_fuzzer.js +++ b/test/z_general_permission_manager_fuzzer.js @@ -18,7 +18,7 @@ const SecurityToken = artifacts.require("./SecurityToken.sol"); const GeneralTransferManager = artifacts.require("./GeneralTransferManager"); const GeneralPermissionManager = artifacts.require("./GeneralPermissionManager"); const CountTransferManager = artifacts.require("./CountTransferManager"); -const VolumeRestrictionTransferManager = artifacts.require("./LockupVolumeRestrictionTM"); +const VolumeRestrictionTransferManager = artifacts.require("./VolumeRestrictionTM"); const PercentageTransferManager = artifacts.require("./PercentageTransferManager"); const ManualApprovalTransferManager = artifacts.require("./ManualApprovalTransferManager"); @@ -97,6 +97,7 @@ contract("GeneralPermissionManager Fuzz", async (accounts) => { let bytesSTO = encodeModuleCall(["uint256"], [holderCount]); let _details = "details holding for test"; + let _description = "some description"; let testRepeat = 20; // permission manager fuzz test @@ -181,7 +182,7 @@ contract("GeneralPermissionManager Fuzz", async (accounts) => { it("Should generate the new security token with the same symbol as registered above", async () => { await I_PolyToken.approve(I_STRProxied.address, initRegFee, { from: token_owner }); - + let tx = await I_STRProxied.generateSecurityToken(name, symbol, tokenDetails, false, { from: token_owner }); // Verify the successful generation of the security token @@ -672,6 +673,7 @@ contract("GeneralPermissionManager Fuzz", async (accounts) => { account_investor4, new BN(web3.utils.toWei("2", "ether")), nextTime, + web3.utils.fromAscii(_details), { from: accounts[j] } ); @@ -694,6 +696,7 @@ contract("GeneralPermissionManager Fuzz", async (accounts) => { account_investor4, new BN(web3.utils.toWei("2", "ether")), nextTime, + web3.utils.fromAscii(_details), { from: accounts[j] } ) ); @@ -704,6 +707,7 @@ contract("GeneralPermissionManager Fuzz", async (accounts) => { account_investor4, new BN(web3.utils.toWei("2", "ether")), nextTime, + web3.utils.fromAscii(_details), { from: token_owner } ); @@ -719,102 +723,5 @@ contract("GeneralPermissionManager Fuzz", async (accounts) => { await revertToSnapshot(snapId); } }); - - it("should pass fuzz test for addManualBlocking and revokeManualBlocking with perm TRANSFER_APPROVAL", async () => { - console.log("1"); - await I_ManualApprovalTransferManager.addManualBlocking(account_investor1, account_investor2, currentTime.add(new BN(duration.days(1))), { - from: token_owner - }); - console.log("2"); - await I_ManualApprovalTransferManager.revokeManualBlocking(account_investor1, account_investor2, { from: token_owner }); - console.log("3"); - - // fuzz test loop over total times of testRepeat, inside each loop, we use a variable j to randomly choose an account out of the 10 default accounts - for (var i = 2; i < testRepeat; i++) { - let snapId = await takeSnapshot(); - - var j = Math.floor(Math.random() * 10); - if (j === 1 || j === 0) { - j = 2; - } // exclude account 1 & 0 because they might come with default perms - - // add account as a Delegate if it is not - if ((await I_GeneralPermissionManager.checkDelegate(accounts[j])) !== true) { - await I_GeneralPermissionManager.addDelegate(accounts[j], web3.utils.fromAscii(_details), { from: token_owner }); - } - - // target permission should alaways be false for each test before assigning - if ( - (await I_GeneralPermissionManager.checkPermission( - accounts[j], - I_ManualApprovalTransferManager.address, - web3.utils.fromAscii("TRANSFER_APPROVAL") - )) === true - ) { - await I_GeneralPermissionManager.changePermission( - accounts[j], - I_ManualApprovalTransferManager.address, - web3.utils.fromAscii("TRANSFER_APPROVAL"), - false, - { from: token_owner } - ); - } - - // assign a random perm - let randomPerms = perms[Math.floor(Math.random() * Math.floor(totalPerms))]; - await I_GeneralPermissionManager.changePermission(accounts[j], I_ManualApprovalTransferManager.address, web3.utils.fromAscii(randomPerms), true, { - from: token_owner - }); - - if (randomPerms === "TRANSFER_APPROVAL") { - console.log("Test number " + i + " with account " + j + " and perm TRANSFER_APPROVAL " + " should pass"); - let nextTime = new BN(await latestTime()).add(new BN(duration.days(1))); - await I_ManualApprovalTransferManager.addManualBlocking( - account_investor1, - account_investor2, - nextTime, - { - from: accounts[j] - } - ); - - console.log("2"); - await I_ManualApprovalTransferManager.revokeManualBlocking(account_investor1, account_investor2, { from: accounts[j] }); - - console.log("Test number " + i + " with account " + j + " and perm TRANSFER_APPROVAL passed as expected"); - } else { - console.log("Test number " + i + " with account " + j + " and perm " + randomPerms + " should failed"); - let nextTime = new BN(await latestTime()).add(new BN(duration.days(1))); - await catchRevert( - I_ManualApprovalTransferManager.addManualBlocking( - account_investor1, - account_investor2, - nextTime, - { - from: accounts[j] - } - ) - ); - - nextTime = new BN(await latestTime()).add(new BN(duration.days(1))); - await I_ManualApprovalTransferManager.addManualBlocking( - account_investor1, - account_investor2, - nextTime, - { - from: token_owner - } - ); - - await catchRevert( - I_ManualApprovalTransferManager.revokeManualBlocking(account_investor1, account_investor2, { from: accounts[j] }) - ); - - console.log("Test number " + i + " with account " + j + " and perm " + randomPerms + " failed as expected"); - } - - await revertToSnapshot(snapId); - } - }); }); }); diff --git a/test/z_vesting_escrow_wallet.js b/test/z_vesting_escrow_wallet.js new file mode 100644 index 000000000..b5aba3441 --- /dev/null +++ b/test/z_vesting_escrow_wallet.js @@ -0,0 +1,1221 @@ +import {deployGPMAndVerifyed, deployVestingEscrowWalletAndVerifyed, setUpPolymathNetwork} from "./helpers/createInstances"; +import latestTime from "./helpers/latestTime"; +import {duration as durationUtil, latestBlock, promisifyLogWatch} from "./helpers/utils"; +import {catchRevert} from "./helpers/exceptions"; +import {increaseTime} from "./helpers/time"; +import {encodeModuleCall} from "./helpers/encodeCall"; + +const SecurityToken = artifacts.require('./SecurityToken.sol'); +const GeneralTransferManager = artifacts.require('./GeneralTransferManager'); +const GeneralPermissionManager = artifacts.require("./GeneralPermissionManager"); +const VestingEscrowWallet = artifacts.require('./VestingEscrowWallet.sol'); + +const Web3 = require('web3'); +const BN = Web3.utils.BN; +const web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));// Hardcoded development port + +contract('VestingEscrowWallet', accounts => { + + const CREATED = 0; + const STARTED = 1; + const COMPLETED = 2; + + // Accounts Variable declaration + let account_polymath; + let token_owner; + let wallet_admin; + let account_beneficiary1; + let account_beneficiary2; + let account_beneficiary3; + + let beneficiaries; + + let message = "Transaction Should Fail!"; + + // Contract Instance Declaration + let I_SecurityTokenRegistryProxy; + let I_GeneralPermissionManagerFactory; + let I_GeneralTransferManagerFactory; + let I_VestingEscrowWalletFactory; + let I_GeneralPermissionManager; + let I_VestingEscrowWallet; + let I_GeneralTransferManager; + let I_ModuleRegistryProxy; + let I_ModuleRegistry; + let I_FeatureRegistry; + let I_SecurityTokenRegistry; + let I_STRProxied; + let I_MRProxied; + let I_STFactory; + let I_SecurityToken; + let I_PolyToken; + let I_PolymathRegistry; + + // SecurityToken Details + const name = "Team"; + const symbol = "sap"; + const tokenDetails = "This is equity type of issuance"; + const decimals = 18; + const contact = "team@polymath.network"; + const delegateDetails = web3.utils.toHex("Hello I am legit delegate"); + + // Module key + const delegateManagerKey = 1; + const transferManagerKey = 2; + const stoKey = 3; + + // Initial fee for ticker registry and security token registry + const initRegFee = new BN(web3.utils.toWei("250")); + + let currentTime; + const address_zero = "0x0000000000000000000000000000000000000000"; + + before(async () => { + currentTime = new BN(await latestTime()); + // Accounts setup + account_polymath = accounts[0]; + token_owner = accounts[1]; + wallet_admin = accounts[2]; + + account_beneficiary1 = accounts[6]; + account_beneficiary2 = accounts[7]; + account_beneficiary3 = accounts[8]; + + beneficiaries = [ + account_beneficiary1, + account_beneficiary2, + account_beneficiary3 + ]; + + // Step 1: Deploy the genral PM ecosystem + let instances = await setUpPolymathNetwork(account_polymath, token_owner); + + [ + I_PolymathRegistry, + I_PolyToken, + I_FeatureRegistry, + I_ModuleRegistry, + I_ModuleRegistryProxy, + I_MRProxied, + I_GeneralTransferManagerFactory, + I_STFactory, + I_SecurityTokenRegistry, + I_SecurityTokenRegistryProxy, + I_STRProxied + ] = instances; + + // STEP 2: Deploy the GeneralDelegateManagerFactory + [I_GeneralPermissionManagerFactory] = await deployGPMAndVerifyed(account_polymath, I_MRProxied, 0); + + // STEP 3: Deploy the VestingEscrowWallet + [I_VestingEscrowWalletFactory] = await deployVestingEscrowWalletAndVerifyed(account_polymath, I_MRProxied, 0); + + // Printing all the contract addresses + console.log(` + --------------------- Polymath Network Smart Contracts: --------------------- + PolymathRegistry: ${I_PolymathRegistry.address} + SecurityTokenRegistryProxy: ${I_SecurityTokenRegistryProxy.address} + SecurityTokenRegistry: ${I_SecurityTokenRegistry.address} + ModuleRegistry: ${I_ModuleRegistry.address} + ModuleRegistryProxy: ${I_ModuleRegistryProxy.address} + FeatureRegistry: ${I_FeatureRegistry.address} + + STFactory: ${I_STFactory.address} + GeneralTransferManagerFactory: ${I_GeneralTransferManagerFactory.address} + GeneralPermissionManagerFactory: ${I_GeneralPermissionManagerFactory.address} + + I_VestingEscrowWalletFactory: ${I_VestingEscrowWalletFactory.address} + ----------------------------------------------------------------------------- + `); + }); + + describe("Generate the SecurityToken", async() => { + + it("Should register the ticker before the generation of the security token", async () => { + await I_PolyToken.approve(I_STRProxied.address, initRegFee, { from: token_owner }); + let tx = await I_STRProxied.registerTicker(token_owner, symbol, contact, { from : token_owner }); + assert.equal(tx.logs[0].args._owner, token_owner); + assert.equal(tx.logs[0].args._ticker, symbol.toUpperCase()); + }); + + it("Should generate the new security token with the same symbol as registered above", async () => { + await I_PolyToken.approve(I_STRProxied.address, initRegFee, { from: token_owner }); + + let tx = await I_STRProxied.generateSecurityToken(name, symbol, tokenDetails, false, { from: token_owner }); + + // Verify the successful generation of the security token + assert.equal(tx.logs[2].args._ticker, symbol.toUpperCase(), "SecurityToken doesn't get deployed"); + + I_SecurityToken = await SecurityToken.at(tx.logs[2].args._securityTokenAddress); + + const log = (await I_SecurityToken.getPastEvents('ModuleAdded', {filter: {transactionHash: tx.transactionHash}}))[0]; + + // Verify that GeneralTransferManager module get added successfully or not + assert.equal(log.args._types[0].toString(), 2); + assert.equal(web3.utils.toAscii(log.args._name).replace(/\u0000/g, ""), "GeneralTransferManager"); + }); + + it("Should intialize the auto attached modules", async () => { + let moduleData = (await I_SecurityToken.getModulesByType(2))[0]; + I_GeneralTransferManager = await GeneralTransferManager.at(moduleData); + + }); + + it("Should successfully attach the General permission manager factory with the security token", async () => { + const tx = await I_SecurityToken.addModule(I_GeneralPermissionManagerFactory.address, "0x", new BN(0), new BN(0), { from: token_owner }); + assert.equal(tx.logs[2].args._types[0].toString(), delegateManagerKey, "General Permission Manager doesn't get deployed"); + assert.equal( + web3.utils.toAscii(tx.logs[2].args._name).replace(/\u0000/g, ""), + "GeneralPermissionManager", + "GeneralPermissionManagerFactory module was not added" + ); + I_GeneralPermissionManager = await GeneralPermissionManager.at(tx.logs[2].args._module); + }); + + it("Should successfully attach the VestingEscrowWallet with the security token", async () => { + let bytesData = encodeModuleCall( + ["address"], + [token_owner] + ); + + await I_SecurityToken.changeGranularity(1, {from: token_owner}); + const tx = await I_SecurityToken.addModule(I_VestingEscrowWalletFactory.address, bytesData, 0, 0, { from: token_owner }); + + assert.equal(tx.logs[2].args._types[0].toString(), 6, "VestingEscrowWallet doesn't get deployed"); + assert.equal( + web3.utils.toAscii(tx.logs[2].args._name) + .replace(/\u0000/g, ''), + "VestingEscrowWallet", + "VestingEscrowWallet module was not added" + ); + I_VestingEscrowWallet = await VestingEscrowWallet.at(tx.logs[2].args._module); + }); + + it("Should Buy the tokens for token_owner", async() => { + // Add the Investor in to the whitelist + let tx = await I_GeneralTransferManager.modifyWhitelist( + token_owner, + currentTime, + currentTime, + currentTime.add(new BN(durationUtil.days(10))), + true, + { + from: token_owner, + gas: 6000000 + } + ); + + assert.equal(tx.logs[0].args._investor.toLowerCase(), token_owner.toLowerCase(), "Failed in adding the token_owner in whitelist"); + + // Mint some tokens + await I_SecurityToken.mint(token_owner, web3.utils.toHex(web3.utils.toWei('1', 'ether')), { from: token_owner }); + + assert.equal( + (await I_SecurityToken.balanceOf(token_owner)).toString(), + web3.utils.toWei('1', 'ether') + ); + + }); + + it("Should whitelist investors", async() => { + // Add the Investor in to the whitelist + let tx = await I_GeneralTransferManager.modifyWhitelistMulti( + [I_VestingEscrowWallet.address, account_beneficiary1, account_beneficiary2, account_beneficiary3], + [currentTime, currentTime, currentTime, currentTime], + [currentTime, currentTime, currentTime, currentTime], + [currentTime.add(new BN(durationUtil.days(10))), currentTime.add(new BN(durationUtil.days(10))), currentTime.add(new BN(durationUtil.days(10))), currentTime.add(new BN(durationUtil.days(10)))], + [true, true, true, true], + { + from: token_owner, + gas: 6000000 + }); + + assert.equal(tx.logs[0].args._investor, I_VestingEscrowWallet.address); + assert.equal(tx.logs[1].args._investor, account_beneficiary1); + assert.equal(tx.logs[2].args._investor, account_beneficiary2); + assert.equal(tx.logs[3].args._investor, account_beneficiary3); + }); + + it("Should successfully add the delegate", async() => { + let tx = await I_GeneralPermissionManager.addDelegate(wallet_admin, delegateDetails, { from: token_owner}); + assert.equal(tx.logs[0].args._delegate, wallet_admin); + }); + + it("Should provide the permission", async() => { + let tx = await I_GeneralPermissionManager.changePermission( + wallet_admin, + I_VestingEscrowWallet.address, + web3.utils.toHex("ADMIN"), + true, + {from: token_owner} + ); + assert.equal(tx.logs[0].args._delegate, wallet_admin); + }); + + it("Should get the permission", async () => { + let perm = await I_VestingEscrowWallet.getPermissions.call(); + assert.equal(web3.utils.toAscii(perm[0]).replace(/\u0000/g, ""), "ADMIN"); + }); + + it("Should get the tags of the factory", async () => { + let tags = await I_VestingEscrowWalletFactory.getTags.call(); + assert.equal(tags.length, 2); + assert.equal(web3.utils.toAscii(tags[0]).replace(/\u0000/g, ""), "Vested"); + assert.equal(web3.utils.toAscii(tags[1]).replace(/\u0000/g, ""), "Escrow Wallet"); + }); + + it("Should get the instructions of the factory", async () => { + assert.equal( + (await I_VestingEscrowWalletFactory.getInstructions.call()).replace(/\u0000/g, ""), + "Issuer can deposit tokens to the contract and create the vesting schedule for the given address (Affiliate/Employee). These address can withdraw tokens according to there vesting schedule." + ); + }); + + }); + + describe("Depositing and withdrawing tokens", async () => { + + it("Should not be able to change treasury wallet -- fail because address is invalid", async () => { + await catchRevert( + I_VestingEscrowWallet.changeTreasuryWallet(address_zero, {from: token_owner}) + ); + }); + + it("Should not be able to deposit -- fail because of permissions check", async () => { + await catchRevert( + I_VestingEscrowWallet.changeTreasuryWallet(account_beneficiary1, {from: account_beneficiary1}) + ); + }); + + it("Should change treasury wallet", async () => { + const tx = await I_VestingEscrowWallet.changeTreasuryWallet(account_beneficiary1, {from: token_owner}); + + assert.equal(tx.logs[0].args._newWallet, account_beneficiary1); + assert.equal(tx.logs[0].args._oldWallet, token_owner); + let treasuryWallet = await I_VestingEscrowWallet.treasuryWallet.call(); + assert.equal(treasuryWallet, account_beneficiary1); + + await I_VestingEscrowWallet.changeTreasuryWallet(token_owner, {from: token_owner}); + }); + + it("Should fail to deposit zero amount of tokens", async () => { + await catchRevert( + I_VestingEscrowWallet.depositTokens(0, {from: token_owner}) + ); + }); + + it("Should not be able to deposit -- fail because of permissions check", async () => { + let numberOfTokens = 25000; + await I_SecurityToken.approve(I_VestingEscrowWallet.address, numberOfTokens, { from: token_owner }); + await catchRevert( + I_VestingEscrowWallet.depositTokens(25000, {from: account_beneficiary1}) + ); + }); + + it("Should deposit tokens for new vesting schedules", async () => { + let numberOfTokens = 25000; + await I_SecurityToken.approve(I_VestingEscrowWallet.address, numberOfTokens, { from: token_owner }); + const tx = await I_VestingEscrowWallet.depositTokens(numberOfTokens, {from: token_owner}); + + assert.equal(tx.logs[0].args._numberOfTokens, numberOfTokens); + + let unassignedTokens = await I_VestingEscrowWallet.unassignedTokens.call(); + assert.equal(unassignedTokens, numberOfTokens); + + let balance = await I_SecurityToken.balanceOf.call(I_VestingEscrowWallet.address); + assert.equal(balance.toString(), numberOfTokens); + }); + + it("Should not be able to withdraw tokens to a treasury -- fail because of permissions check", async () => { + await catchRevert( + I_VestingEscrowWallet.sendToTreasury(10, {from: account_beneficiary1}) + ); + }); + + it("Should not be able to withdraw tokens to a treasury -- fail because of zero amount", async () => { + await catchRevert( + I_VestingEscrowWallet.sendToTreasury(0, {from: wallet_admin}) + ); + }); + + it("Should not be able to withdraw tokens to a treasury -- fail because amount is greater than unassigned tokens", async () => { + let numberOfTokens = 25000 * 2; + await catchRevert( + I_VestingEscrowWallet.sendToTreasury(numberOfTokens, {from: wallet_admin}) + ); + }); + + it("Should withdraw tokens to a treasury", async () => { + let numberOfTokens = 25000; + const tx = await I_VestingEscrowWallet.sendToTreasury(numberOfTokens, {from: wallet_admin}); + + assert.equal(tx.logs[0].args._numberOfTokens, numberOfTokens); + + let unassignedTokens = await I_VestingEscrowWallet.unassignedTokens.call(); + assert.equal(unassignedTokens, 0); + + let balance = await I_SecurityToken.balanceOf.call(I_VestingEscrowWallet.address); + assert.equal(balance.toString(), 0); + }); + + it("Should not be able to push available tokens -- fail because of permissions check", async () => { + currentTime = new BN(await latestTime()); + let templateName = web3.utils.toHex("template-01"); + let numberOfTokens = 75000; + let duration = new BN(durationUtil.seconds(30)); + let frequency = new BN(durationUtil.seconds(10)); + let timeShift = new BN(durationUtil.seconds(100)); + let startTime = currentTime.add(timeShift); + await I_SecurityToken.approve(I_VestingEscrowWallet.address, numberOfTokens, { from: token_owner }); + await I_VestingEscrowWallet.depositTokens(numberOfTokens, {from: token_owner}); + await I_VestingEscrowWallet.addSchedule(account_beneficiary3, templateName, numberOfTokens, duration, frequency, startTime, {from: wallet_admin}); + await increaseTime(durationUtil.seconds(110)); + + await catchRevert( + I_VestingEscrowWallet.pushAvailableTokens(account_beneficiary3, {from: account_beneficiary1}) + ); + }); + + it("Should not be able to remove template -- fail because template is used", async () => { + await catchRevert( + I_VestingEscrowWallet.removeTemplate(web3.utils.toHex("template-01"), {from: wallet_admin}) + ); + }); + + it("Should push available tokens to the beneficiary address", async () => { + let numberOfTokens = 75000; + const tx = await I_VestingEscrowWallet.pushAvailableTokens(account_beneficiary3, {from: wallet_admin}); + assert.equal(tx.logs[0].args._beneficiary, account_beneficiary3); + assert.equal(tx.logs[0].args._numberOfTokens.toString(), numberOfTokens / 3); + + let balance = await I_SecurityToken.balanceOf.call(account_beneficiary3); + assert.equal(balance.toString(), numberOfTokens / 3); + + await I_SecurityToken.transfer(token_owner, balance, {from: account_beneficiary3}); + }); + + it("Should fail to modify vesting schedule -- fail because schedule already started", async () => { + let templateName = web3.utils.toHex("template-01"); + let startTime = currentTime + 100; + await catchRevert( + I_VestingEscrowWallet.modifySchedule(account_beneficiary3, templateName, startTime, {from: wallet_admin}) + ); + + await I_VestingEscrowWallet.revokeAllSchedules(account_beneficiary3, {from: wallet_admin}); + await I_VestingEscrowWallet.removeTemplate(templateName, {from: wallet_admin}); + let unassignedTokens = await I_VestingEscrowWallet.unassignedTokens.call(); + await I_VestingEscrowWallet.sendToTreasury(unassignedTokens, {from: wallet_admin}); + }); + + it("Should fail to modify vesting schedule -- fail because date in the past", async () => { + await catchRevert( + I_VestingEscrowWallet.modifySchedule(account_beneficiary3, web3.utils.toHex("template-01"), currentTime - 1000, {from: wallet_admin}) + ); + }); + + it("Should withdraw available tokens to the beneficiary address", async () => { + currentTime = new BN(await latestTime()); + let templateName = web3.utils.toHex("template-02"); + let numberOfTokens = 33000; + let duration = new BN(durationUtil.seconds(30)); + let frequency = new BN(durationUtil.seconds(10)); + let timeShift = new BN(durationUtil.seconds(100)); + let startTime = currentTime.add(timeShift); + await I_SecurityToken.approve(I_VestingEscrowWallet.address, numberOfTokens, { from: token_owner }); + await I_VestingEscrowWallet.depositTokens(numberOfTokens, {from: token_owner}); + await I_VestingEscrowWallet.addSchedule(account_beneficiary3, templateName, numberOfTokens, duration, frequency, startTime, {from: wallet_admin}); + await increaseTime(durationUtil.seconds(130)); + + const tx = await I_VestingEscrowWallet.pullAvailableTokens({from: account_beneficiary3}); + assert.equal(tx.logs[0].args._beneficiary, account_beneficiary3); + assert.equal(tx.logs[0].args._numberOfTokens.toString(), numberOfTokens); + + let balance = await I_SecurityToken.balanceOf.call(account_beneficiary3); + assert.equal(balance.toString(), numberOfTokens); + + let schedule = await I_VestingEscrowWallet.getSchedule.call(account_beneficiary3, templateName); + checkSchedule(schedule, numberOfTokens, duration, frequency, startTime, COMPLETED); + + await I_SecurityToken.transfer(token_owner, balance, {from: account_beneficiary3}); + await I_VestingEscrowWallet.revokeAllSchedules(account_beneficiary3, {from: wallet_admin}); + await I_VestingEscrowWallet.removeTemplate(templateName, {from: wallet_admin}); + }); + + it("Should withdraw available tokens 2 times by 3 schedules to the beneficiary address", async () => { + currentTime = new BN(await latestTime()); + let schedules = [ + { + templateName: web3.utils.toHex("template-1-01"), + numberOfTokens: new BN(100000), + duration: new BN(durationUtil.minutes(4)), + frequency: new BN(durationUtil.minutes(1)) + }, + { + templateName: web3.utils.toHex("template-1-02"), + numberOfTokens: new BN(30000), + duration: new BN(durationUtil.minutes(6)), + frequency: new BN(durationUtil.minutes(1)) + }, + { + templateName: web3.utils.toHex("template-1-03"), + numberOfTokens: new BN(2000), + duration: new BN(durationUtil.minutes(10)), + frequency: new BN(durationUtil.minutes(1)) + } + ]; + + let timeShift = new BN(durationUtil.seconds(100)); + let totalNumberOfTokens = getTotalNumberOfTokens(schedules); + await I_SecurityToken.approve(I_VestingEscrowWallet.address, totalNumberOfTokens, {from: token_owner}); + await I_VestingEscrowWallet.depositTokens(totalNumberOfTokens, {from: token_owner}); + for (let i = 0; i < schedules.length; i++) { + let templateName = schedules[i].templateName; + let numberOfTokens = schedules[i].numberOfTokens; + let duration = schedules[i].duration; + let frequency = schedules[i].frequency; + let startTime = currentTime.add(timeShift); + await I_VestingEscrowWallet.addSchedule(account_beneficiary3, templateName, numberOfTokens, duration, frequency, startTime, {from: wallet_admin}); + } + let stepCount = 6; + await increaseTime(durationUtil.minutes(stepCount) + durationUtil.seconds(100)); + + let numberOfTokens = 100000 + (30000 / 6 * stepCount) + (2000 / 10 * stepCount); + const tx = await I_VestingEscrowWallet.pullAvailableTokens({from: account_beneficiary3}); + + assert.equal(tx.logs[0].args._beneficiary, account_beneficiary3); + assert.equal(tx.logs[0].args._numberOfTokens.toString(), 100000); + assert.equal(tx.logs[1].args._beneficiary, account_beneficiary3); + assert.equal(tx.logs[1].args._numberOfTokens.toString(), 30000 / 6 * stepCount); + assert.equal(tx.logs[2].args._beneficiary, account_beneficiary3); + assert.equal(tx.logs[2].args._numberOfTokens.toString(), 2000 / 10 * stepCount); + + let balance = await I_SecurityToken.balanceOf.call(account_beneficiary3); + assert.equal(balance.toString(), numberOfTokens); + + stepCount = 4; + await increaseTime(durationUtil.minutes(stepCount) + durationUtil.seconds(100)); + + const tx2 = await I_VestingEscrowWallet.pullAvailableTokens({from: account_beneficiary3}); + assert.equal(tx2.logs[0].args._beneficiary, account_beneficiary3); + assert.equal(tx2.logs[0].args._numberOfTokens.toString(), 2000 / 10 * stepCount); + + balance = await I_SecurityToken.balanceOf.call(account_beneficiary3); + assert.equal(balance.toString(), totalNumberOfTokens); + + await I_SecurityToken.transfer(token_owner, balance, {from: account_beneficiary3}); + await I_VestingEscrowWallet.revokeAllSchedules(account_beneficiary3, {from: wallet_admin}); + for (let i = 0; i < schedules.length; i++) { + await I_VestingEscrowWallet.removeTemplate(schedules[i].templateName, {from: wallet_admin}); + } + }); + + }); + + describe("Adding, modifying and revoking vesting schedule", async () => { + let template_2_01 = web3.utils.toHex("template-2-01"); + + let schedules = [ + { + templateName: web3.utils.toHex("template-2-01"), + numberOfTokens: 100000, + duration: new BN(durationUtil.years(4)), + frequency: new BN(durationUtil.years(1)), + // startTime: currentTime.add(new BN(durationUtil.days(1))) + }, + { + templateName: web3.utils.toHex("template-2-02"), + numberOfTokens: 30000, + duration: new BN(durationUtil.weeks(6)), + frequency: new BN(durationUtil.weeks(1)), + // startTime: currentTime.add(new BN(durationUtil.days(2))) + }, + { + templateName: web3.utils.toHex("template-2-03"), + numberOfTokens: 2000, + duration: new BN(durationUtil.days(10)), + frequency: new BN(durationUtil.days(2)), + // startTime: currentTime.add(new BN(durationUtil.days(3))) + } + ]; + + it("Should fail to add vesting schedule to the beneficiary address -- fail because address in invalid", async () => { + await catchRevert( + I_VestingEscrowWallet.addSchedule(address_zero, template_2_01, 100000, 4, 1, currentTime.add(new BN(durationUtil.days(1))), {from: wallet_admin}) + ); + }); + + it("Should fail to add vesting schedule to the beneficiary address -- fail because start date in the past", async () => { + await catchRevert( + I_VestingEscrowWallet.addSchedule(account_beneficiary1, template_2_01, 100000, 4, 1, currentTime.add(new BN(durationUtil.days(1))), {from: wallet_admin}) + ); + }); + + it("Should fail to add vesting schedule to the beneficiary address -- fail because number of tokens is 0", async () => { + await catchRevert( + I_VestingEscrowWallet.addSchedule(account_beneficiary1, template_2_01, 0, 4, 1, currentTime.add(new BN(durationUtil.days(1))), {from: wallet_admin}) + ); + }); + + it("Should fail to add vesting schedule to the beneficiary address -- fail because duration can't be divided entirely by frequency", async () => { + await catchRevert( + I_VestingEscrowWallet.addSchedule(account_beneficiary1, template_2_01, 100000, 4, 3, currentTime.add(new BN(durationUtil.days(1))), {from: wallet_admin}) + ); + }); + + it("Should fail to add vesting schedule to the beneficiary address -- fail because number of tokens can't be divided entirely by period count", async () => { + await catchRevert( + I_VestingEscrowWallet.addSchedule(account_beneficiary1, template_2_01, 5, 4, 1, currentTime.add(new BN(durationUtil.days(1))), {from: wallet_admin}) + ); + }); + + it("Should fail to get vesting schedule -- fail because address is invalid", async () => { + await catchRevert( + I_VestingEscrowWallet.getSchedule(address_zero, template_2_01) + ); + }); + + it("Should fail to get vesting schedule -- fail because schedule not found", async () => { + + await catchRevert( + I_VestingEscrowWallet.getSchedule(account_beneficiary1, template_2_01) + ); + }); + + it("Should fail to get count of vesting schedule -- fail because address is invalid", async () => { + await catchRevert( + I_VestingEscrowWallet.getScheduleCount(address_zero) + ); + }); + + it("Should not be able to add schedule -- fail because of permissions check", async () => { + let templateName = schedules[0].templateName; + let numberOfTokens = schedules[0].numberOfTokens; + let duration = schedules[0].duration; + let frequency = schedules[0].frequency; + let startTime = currentTime.add(new BN(durationUtil.days(1))); + currentTime = new BN(await latestTime()); + await I_SecurityToken.approve(I_VestingEscrowWallet.address, numberOfTokens, {from: token_owner}); + await I_VestingEscrowWallet.depositTokens(numberOfTokens, {from: token_owner}); + await catchRevert( + I_VestingEscrowWallet.addSchedule(account_beneficiary1, templateName, numberOfTokens, duration, frequency, startTime, {from: account_beneficiary1}) + ); + let unassignedTokens = await I_VestingEscrowWallet.unassignedTokens.call(); + await I_VestingEscrowWallet.sendToTreasury(unassignedTokens, {from: wallet_admin}); + }); + + it("Should add vesting schedule to the beneficiary address", async () => { + currentTime = new BN(await latestTime()); + let templateName = schedules[0].templateName; + let numberOfTokens = schedules[0].numberOfTokens; + let duration = schedules[0].duration; + let frequency = schedules[0].frequency; + let startTime = currentTime.add(new BN(durationUtil.days(1))); + await I_SecurityToken.approve(I_VestingEscrowWallet.address, numberOfTokens, {from: token_owner}); + await I_VestingEscrowWallet.depositTokens(numberOfTokens, {from: token_owner}); + const tx = await I_VestingEscrowWallet.addSchedule(account_beneficiary1, templateName, numberOfTokens, duration, frequency, startTime, {from: wallet_admin}); + + checkTemplateLog(tx.logs[0], templateName, numberOfTokens, duration, frequency); + checkScheduleLog(tx.logs[1], account_beneficiary1, templateName, startTime); + + let scheduleCount = await I_VestingEscrowWallet.getScheduleCount.call(account_beneficiary1); + assert.equal(scheduleCount, 1); + + let schedule = await I_VestingEscrowWallet.getSchedule.call(account_beneficiary1, templateName); + checkSchedule(schedule, numberOfTokens, duration, frequency, startTime, CREATED); + + let templates = await I_VestingEscrowWallet.getTemplateNames.call(account_beneficiary1); + assert.equal(web3.utils.hexToUtf8(templates[0]), web3.utils.hexToUtf8(templateName)); + }); + + it("Should add vesting schedule without depositing to the beneficiary address", async () => { + currentTime = new BN(await latestTime()); + let templateName = web3.utils.toHex("template-2-01-2"); + let numberOfTokens = schedules[0].numberOfTokens; + let duration = schedules[0].duration; + let frequency = schedules[0].frequency; + let startTime = currentTime.add(new BN(durationUtil.days(1))); + await I_SecurityToken.approve(I_VestingEscrowWallet.address, numberOfTokens, {from: token_owner}); + const tx = await I_VestingEscrowWallet.addSchedule(account_beneficiary1, templateName, numberOfTokens, duration, frequency, startTime, {from: token_owner}); + + checkTemplateLog(tx.logs[0], templateName, numberOfTokens, duration, frequency); + assert.equal(tx.logs[1].args._numberOfTokens, numberOfTokens); + checkScheduleLog(tx.logs[2], account_beneficiary1, templateName, startTime); + + let scheduleCount = await I_VestingEscrowWallet.getScheduleCount.call(account_beneficiary1); + assert.equal(scheduleCount, 2); + + let schedule = await I_VestingEscrowWallet.getSchedule.call(account_beneficiary1, templateName); + checkSchedule(schedule, numberOfTokens, duration, frequency, startTime, CREATED); + + await I_VestingEscrowWallet.revokeSchedule(account_beneficiary1, templateName, {from: wallet_admin}); + await I_VestingEscrowWallet.removeTemplate(templateName, {from: wallet_admin}); + let unassignedTokens = await I_VestingEscrowWallet.unassignedTokens.call(); + await I_VestingEscrowWallet.sendToTreasury(unassignedTokens, {from: wallet_admin}); + }); + + it("Should fail to modify vesting schedule -- fail because schedule not found", async () => { + currentTime = new BN(await latestTime()); + let templateName = web3.utils.toHex("template-2-03"); + let startTime = currentTime.add(new BN(durationUtil.days(1))); + await catchRevert( + I_VestingEscrowWallet.modifySchedule(account_beneficiary1, templateName, startTime, {from: wallet_admin}) + ); + }); + + it("Should not be able to modify schedule -- fail because of permissions check", async () => { + await catchRevert( + I_VestingEscrowWallet.modifySchedule(account_beneficiary1, web3.utils.toHex("template-2-01"), currentTime.add(new BN(100)), {from: account_beneficiary1}) + ); + }); + + it("Should modify vesting schedule for the beneficiary's address", async () => { + currentTime = new BN(await latestTime()); + let templateName = web3.utils.toHex("template-2-01"); + let numberOfTokens = schedules[0].numberOfTokens; + let duration = schedules[0].duration; + let frequency = schedules[0].frequency; + let startTime = currentTime.add(new BN(durationUtil.days(2))); + const tx = await I_VestingEscrowWallet.modifySchedule(account_beneficiary1, templateName, startTime, {from: wallet_admin}); + + checkScheduleLog(tx.logs[0], account_beneficiary1, templateName, startTime); + + let scheduleCount = await I_VestingEscrowWallet.getScheduleCount.call(account_beneficiary1); + assert.equal(scheduleCount.toString(), 1); + + let schedule = await I_VestingEscrowWallet.getSchedule.call(account_beneficiary1, templateName); + checkSchedule(schedule, numberOfTokens, duration, frequency, startTime, CREATED); + }); + + it("Should not be able to revoke schedule -- fail because of permissions check", async () => { + await catchRevert( + I_VestingEscrowWallet.revokeSchedule(account_beneficiary1, web3.utils.toHex("template-2-01"), {from: account_beneficiary1}) + ); + }); + + it("Should revoke vesting schedule from the beneficiary address", async () => { + let templateName = web3.utils.toHex("template-2-01"); + const tx = await I_VestingEscrowWallet.revokeSchedule(account_beneficiary1, templateName, {from: wallet_admin}); + let unassignedTokens = await I_VestingEscrowWallet.unassignedTokens.call(); + await I_VestingEscrowWallet.sendToTreasury(unassignedTokens, {from: wallet_admin}); + + assert.equal(tx.logs[0].args._beneficiary, account_beneficiary1); + assert.equal(web3.utils.hexToUtf8(tx.logs[0].args._templateName), web3.utils.hexToUtf8(templateName)); + + let scheduleCount = await I_VestingEscrowWallet.getScheduleCount.call(account_beneficiary1); + assert.equal(scheduleCount, 0); + + await I_VestingEscrowWallet.removeTemplate(templateName, {from: wallet_admin}) + }); + + it("Should fail to revoke vesting schedule -- fail because address is invalid", async () => { + await catchRevert( + I_VestingEscrowWallet.revokeSchedule(address_zero, web3.utils.toHex("template-2-01"), {from: wallet_admin}) + ); + }); + + it("Should fail to revoke vesting schedule -- fail because schedule not found", async () => { + await catchRevert( + I_VestingEscrowWallet.revokeSchedule(account_beneficiary1, web3.utils.toHex("template-2-02"), {from: wallet_admin}) + ); + }); + + it("Should fail to revoke vesting schedules -- fail because address is invalid", async () => { + await catchRevert( + I_VestingEscrowWallet.revokeAllSchedules(address_zero, {from: wallet_admin}) + ); + }); + + it("Should add 3 vesting schedules to the beneficiary address", async () => { + currentTime = new BN(await latestTime()); + let startTimes = [ + currentTime.add(new BN(durationUtil.days(1))), + currentTime.add(new BN(durationUtil.days(2))), + currentTime.add(new BN(durationUtil.days(3))) + ]; + let totalNumberOfTokens = getTotalNumberOfTokens(schedules); + await I_SecurityToken.approve(I_VestingEscrowWallet.address, totalNumberOfTokens, {from: token_owner}); + await I_VestingEscrowWallet.depositTokens(totalNumberOfTokens, {from: token_owner}); + for (let i = 0; i < schedules.length; i++) { + let templateName = schedules[i].templateName; + let numberOfTokens = schedules[i].numberOfTokens; + let duration = schedules[i].duration; + let frequency = schedules[i].frequency; + let startTime = startTimes[i]; + const tx = await I_VestingEscrowWallet.addSchedule(account_beneficiary2, templateName, numberOfTokens, duration, frequency, startTime, {from: wallet_admin}); + + checkTemplateLog(tx.logs[0], templateName, numberOfTokens, duration, frequency); + checkScheduleLog(tx.logs[1], account_beneficiary2, templateName, startTime); + + let scheduleCount = await I_VestingEscrowWallet.getScheduleCount.call(account_beneficiary2); + assert.equal(scheduleCount, i + 1); + + let schedule = await I_VestingEscrowWallet.getSchedule.call(account_beneficiary2, templateName); + checkSchedule(schedule, numberOfTokens, duration, frequency, startTime, CREATED); + } + }); + + it("Should not be able to revoke schedules -- fail because of permissions check", async () => { + await catchRevert( + I_VestingEscrowWallet.revokeAllSchedules(account_beneficiary1, {from: account_beneficiary1}) + ); + }); + + it("Should revoke 1 of 3 vesting schedule from the beneficiary address", async () => { + let templateName = schedules[1].templateName; + const tx = await I_VestingEscrowWallet.revokeSchedule(account_beneficiary2, templateName, {from: wallet_admin}); + let unassignedTokens = await I_VestingEscrowWallet.unassignedTokens.call(); + await I_VestingEscrowWallet.sendToTreasury(unassignedTokens, {from: wallet_admin}); + + assert.equal(tx.logs[0].args._beneficiary, account_beneficiary2); + assert.equal(web3.utils.hexToUtf8(tx.logs[0].args._templateName), web3.utils.hexToUtf8(templateName)); + + let scheduleCount = await I_VestingEscrowWallet.getScheduleCount.call(account_beneficiary2); + assert.equal(scheduleCount, 2); + }); + + it("Should revoke 2 vesting schedules from the beneficiary address", async () => { + const tx = await I_VestingEscrowWallet.revokeAllSchedules(account_beneficiary2, {from: wallet_admin}); + let unassignedTokens = await I_VestingEscrowWallet.unassignedTokens.call(); + await I_VestingEscrowWallet.sendToTreasury(unassignedTokens, {from: wallet_admin}); + + assert.equal(tx.logs[0].args._beneficiary, account_beneficiary2); + + let scheduleCount = await I_VestingEscrowWallet.getScheduleCount.call(account_beneficiary2); + assert.equal(scheduleCount, 0); + }); + + it("Should push available tokens during revoking vesting schedule", async () => { + currentTime = new BN(await latestTime()); + let schedules = [ + { + templateName: web3.utils.toHex("template-3-01"), + numberOfTokens: new BN(100000), + duration: new BN(durationUtil.minutes(4)), + frequency: new BN(durationUtil.minutes(1)) + }, + { + templateName: web3.utils.toHex("template-3-02"), + numberOfTokens: new BN(30000), + duration: new BN(durationUtil.minutes(6)), + frequency: new BN(durationUtil.minutes(1)) + }, + { + templateName: web3.utils.toHex("template-3-03"), + numberOfTokens: new BN(2000), + duration: new BN(durationUtil.minutes(10)), + frequency: new BN(durationUtil.minutes(1)) + } + ]; + + let totalNumberOfTokens = getTotalNumberOfTokens(schedules); + await I_SecurityToken.approve(I_VestingEscrowWallet.address, totalNumberOfTokens, {from: token_owner}); + await I_VestingEscrowWallet.depositTokens(totalNumberOfTokens, {from: token_owner}); + for (let i = 0; i < schedules.length; i++) { + let templateName = schedules[i].templateName; + let numberOfTokens = schedules[i].numberOfTokens; + let duration = schedules[i].duration; + let frequency = schedules[i].frequency; + let startTime = currentTime.add(new BN(100)); + await I_VestingEscrowWallet.addSchedule(account_beneficiary3, templateName, numberOfTokens, duration, frequency, startTime, {from: wallet_admin}); + } + let stepCount = 3; + await increaseTime(durationUtil.minutes(stepCount) + durationUtil.seconds(100)); + + const tx = await I_VestingEscrowWallet.revokeSchedule(account_beneficiary3, web3.utils.toHex("template-3-01"), {from: wallet_admin}); + assert.equal(tx.logs[0].args._beneficiary, account_beneficiary3); + assert.equal(tx.logs[0].args._numberOfTokens.toString(), 100000 / 4 * stepCount); + + let balance = await I_SecurityToken.balanceOf.call(account_beneficiary3); + assert.equal(balance.toString(), 100000 / 4 * stepCount); + + stepCount = 7; + await increaseTime(durationUtil.minutes(stepCount)); + + const tx2 = await I_VestingEscrowWallet.revokeAllSchedules(account_beneficiary3, {from: wallet_admin}); + assert.equal(tx2.logs[0].args._beneficiary, account_beneficiary3); + assert.equal(tx2.logs[0].args._numberOfTokens.toString(), 2000); + assert.equal(tx2.logs[1].args._beneficiary, account_beneficiary3); + assert.equal(tx2.logs[1].args._numberOfTokens.toString(), 30000); + + for (let i = 0; i < schedules.length; i++) { + await I_VestingEscrowWallet.removeTemplate(schedules[i].templateName, {from: wallet_admin}); + } + + balance = await I_SecurityToken.balanceOf.call(account_beneficiary3); + assert.equal(balance.toString(), totalNumberOfTokens - 100000 / 4); + + await I_SecurityToken.transfer(token_owner, balance, {from: account_beneficiary3}); + let unassignedTokens = await I_VestingEscrowWallet.unassignedTokens.call(); + await I_VestingEscrowWallet.sendToTreasury(unassignedTokens, {from: wallet_admin}); + }); + + }); + + describe("Adding, using and removing templates", async () => { + let schedules = [ + { + templateName: web3.utils.toHex("template-4-01"), + numberOfTokens: 100000, + duration: new BN(durationUtil.years(4)), + frequency: new BN(durationUtil.years(1)), + // startTime: currentTime.add(new BN(durationUtil.days(1))) + }, + { + templateName: web3.utils.toHex("template-4-02"), + numberOfTokens: 30000, + duration: new BN(durationUtil.weeks(6)), + frequency: new BN(durationUtil.weeks(1)), + // startTime: currentTime.add(new BN(durationUtil.days(2))) + }, + { + templateName: web3.utils.toHex("template-4-03"), + numberOfTokens: 2000, + duration: new BN(durationUtil.days(10)), + frequency: new BN(durationUtil.days(2)), + // startTime: currentTime.add(new BN(durationUtil.days(3))) + } + ]; + + it("Should not be able to add template -- fail because of permissions check", async () => { + await catchRevert( + I_VestingEscrowWallet.addTemplate(web3.utils.toHex("template-4-01"), 25000, 4, 1, {from: account_beneficiary1}) + ); + }); + + it("Should not be able to add template -- fail because of invalid name", async () => { + await catchRevert( + I_VestingEscrowWallet.addTemplate(web3.utils.toHex(""), 25000, 4, 1, {from: wallet_admin}) + ); + }); + + it("Should add 3 Templates", async () => { + let oldTemplateCount = await I_VestingEscrowWallet.getTemplateCount.call(); + for (let i = 0; i < schedules.length; i++) { + let templateName = schedules[i].templateName; + let numberOfTokens = schedules[i].numberOfTokens; + let duration = schedules[i].duration; + let frequency = schedules[i].frequency; + const tx = await I_VestingEscrowWallet.addTemplate(templateName, numberOfTokens, duration, frequency, {from: wallet_admin}); + + assert.equal(web3.utils.hexToUtf8(tx.logs[0].args._name), web3.utils.hexToUtf8(templateName)); + assert.equal(tx.logs[0].args._numberOfTokens.toString(), numberOfTokens); + assert.equal(tx.logs[0].args._duration.toString(), duration); + assert.equal(tx.logs[0].args._frequency.toString(), frequency); + } + let templateNames = await I_VestingEscrowWallet.getAllTemplateNames.call(); + + for (let i = 0, j = oldTemplateCount; i < schedules.length; i++, j++) { + assert.equal(web3.utils.hexToUtf8(templateNames[j]), web3.utils.hexToUtf8(schedules[i].templateName)); + } + }); + + it("Should not be able to add template -- fail because template already exists", async () => { + await catchRevert( + I_VestingEscrowWallet.addTemplate(web3.utils.toHex("template-4-01"), 25000, 4, 1, {from: wallet_admin}) + ); + }); + + it("Should not be able to remove template -- fail because of permissions check", async () => { + await catchRevert( + I_VestingEscrowWallet.removeTemplate(web3.utils.toHex("template-4-02"), {from: account_beneficiary1}) + ); + }); + + it("Should not be able to remove template -- fail because template not found", async () => { + await catchRevert( + I_VestingEscrowWallet.removeTemplate(web3.utils.toHex("template-444-02"), {from: wallet_admin}) + ); + }); + + it("Should remove template", async () => { + const tx = await I_VestingEscrowWallet.removeTemplate(web3.utils.toHex("template-4-02"), {from: wallet_admin}); + + assert.equal(web3.utils.hexToUtf8(tx.logs[0].args._name), "template-4-02"); + }); + + it("Should fail to add vesting schedule from template -- fail because template not found", async () => { + await catchRevert( + I_VestingEscrowWallet.addScheduleFromTemplate(account_beneficiary1, web3.utils.toHex("template-4-02"), currentTime, {from: wallet_admin}) + ); + }); + + it("Should not be able to add schedule from template -- fail because of permissions check", async () => { + await catchRevert( + I_VestingEscrowWallet.addScheduleFromTemplate(account_beneficiary1, web3.utils.toHex("template-4-01"), currentTime, {from: account_beneficiary1}) + ); + }); + + it("Should not be able to add vesting schedule from template -- fail because template not found", async () => { + await catchRevert( + I_VestingEscrowWallet.addScheduleFromTemplate(account_beneficiary1, web3.utils.toHex("template-777"), currentTime + 100, {from: wallet_admin}) + ); + }); + + it("Should add vesting schedule from template", async () => { + let templateName = schedules[2].templateName; + let numberOfTokens = schedules[2].numberOfTokens; + let duration = schedules[2].duration; + let frequency = schedules[2].frequency; + let startTime = currentTime.add(new BN(durationUtil.days(3))); + currentTime = new BN(await latestTime()); + await I_SecurityToken.approve(I_VestingEscrowWallet.address, numberOfTokens, { from: token_owner }); + await I_VestingEscrowWallet.depositTokens(numberOfTokens, {from: token_owner}); + const tx = await I_VestingEscrowWallet.addScheduleFromTemplate(account_beneficiary1, templateName, startTime, {from: wallet_admin}); + + checkScheduleLog(tx.logs[0], account_beneficiary1, templateName, startTime); + + let scheduleCount = await I_VestingEscrowWallet.getScheduleCount.call(account_beneficiary1); + assert.equal(scheduleCount, 1); + + let schedule = await I_VestingEscrowWallet.getSchedule.call(account_beneficiary1, templateName); + checkSchedule(schedule, numberOfTokens, duration, frequency, startTime, CREATED); + + await I_VestingEscrowWallet.revokeSchedule(account_beneficiary1, templateName, {from: wallet_admin}); + let unassignedTokens = await I_VestingEscrowWallet.unassignedTokens.call(); + await I_VestingEscrowWallet.sendToTreasury(unassignedTokens, {from: wallet_admin}); + }); + + it("Should not be able to add vesting schedule from template -- fail because template already added", async () => { + let templateName = schedules[2].templateName; + await catchRevert( + I_VestingEscrowWallet.addScheduleFromTemplate(account_beneficiary1, templateName, currentTime.add(new BN(100)), {from: wallet_admin}) + ); + }); + + it("Should fail to remove template", async () => { + await catchRevert( + I_VestingEscrowWallet.removeTemplate(web3.utils.toHex("template-4-02"), {from: wallet_admin}) + ); + }); + + it("Should remove 2 Templates", async () => { + let templateCount = await I_VestingEscrowWallet.getTemplateCount.call({from: wallet_admin}); + + await I_VestingEscrowWallet.removeTemplate(web3.utils.toHex("template-4-01"), {from: wallet_admin}); + await I_VestingEscrowWallet.removeTemplate(web3.utils.toHex("template-4-03"), {from: wallet_admin}); + + let templateCountAfterRemoving = await I_VestingEscrowWallet.getTemplateCount.call({from: wallet_admin}); + assert.equal(templateCount - templateCountAfterRemoving, 2); + }); + + }); + + describe("Tests for multi operations", async () => { + + let templateNames = [web3.utils.toHex("template-5-01"), web3.utils.toHex("template-5-02"), web3.utils.toHex("template-5-03")]; + + it("Should not be able to add schedules to the beneficiaries -- fail because of permissions check", async () => { + currentTime = new BN(await latestTime()); + let startTime = currentTime.add(new BN(100)); + let startTimes = [startTime, startTime, startTime]; + await catchRevert( + I_VestingEscrowWallet.addScheduleMulti(beneficiaries, templateNames, [10000, 10000, 10000], [4, 4, 4], [1, 1, 1], startTimes, {from: account_beneficiary1}) + ); + }); + + it("Should not be able to add schedules to the beneficiaries -- fail because of arrays sizes mismatch", async () => { + let startTime = currentTime.add(new BN(100)); + let startTimes = [startTime, startTime, startTime]; + let totalNumberOfTokens = 60000; + await I_SecurityToken.approve(I_VestingEscrowWallet.address, totalNumberOfTokens, {from: token_owner}); + await I_VestingEscrowWallet.depositTokens(totalNumberOfTokens, {from: token_owner}); + await catchRevert( + I_VestingEscrowWallet.addScheduleMulti(beneficiaries, templateNames, [20000, 30000, 10000], [4, 4], [1, 1, 1], startTimes, {from: wallet_admin}) + ); + let unassignedTokens = await I_VestingEscrowWallet.unassignedTokens.call(); + await I_VestingEscrowWallet.sendToTreasury(unassignedTokens, {from: wallet_admin}); + }); + + it("Should add schedules for 3 beneficiaries", async () => { + let numberOfTokens = [15000, 15000, 15000]; + let durations = [new BN(durationUtil.seconds(50)), new BN(durationUtil.seconds(50)), new BN(durationUtil.seconds(50))]; + let frequencies = [new BN(durationUtil.seconds(10)), new BN(durationUtil.seconds(10)), new BN(durationUtil.seconds(10))]; + let timeShift = new BN(durationUtil.seconds(100)); + let startTimes = [currentTime.add(timeShift), currentTime.add(timeShift), currentTime.add(timeShift)]; + + let totalNumberOfTokens = 60000; + await I_SecurityToken.approve(I_VestingEscrowWallet.address, totalNumberOfTokens, {from: token_owner}); + await I_VestingEscrowWallet.depositTokens(totalNumberOfTokens, {from: token_owner}); + + let tx = await I_VestingEscrowWallet.addScheduleMulti(beneficiaries, templateNames, numberOfTokens, durations, frequencies, startTimes, {from: wallet_admin}); + + for (let i = 0; i < beneficiaries.length; i++) { + let templateName = templateNames[i]; + let beneficiary = beneficiaries[i]; + checkTemplateLog(tx.logs[i* 2], templateName, numberOfTokens[i], durations[i], frequencies[i]); + checkScheduleLog(tx.logs[i * 2 + 1], beneficiary, templateName, startTimes[i]); + + let scheduleCount = await I_VestingEscrowWallet.getScheduleCount.call(beneficiary); + assert.equal(scheduleCount, 1); + + let schedule = await I_VestingEscrowWallet.getSchedule.call(beneficiary, templateName); + checkSchedule(schedule, numberOfTokens[i], durations[i], frequencies[i], startTimes[i], CREATED); + } + }); + + it("Should not be able modify vesting schedule for 3 beneficiary's addresses -- fail because of arrays sizes mismatch", async () => { + let timeShift = new BN(durationUtil.seconds(100)); + let startTimes = [currentTime.add(timeShift), currentTime.add(timeShift), currentTime.add(timeShift)]; + + await catchRevert( + I_VestingEscrowWallet.modifyScheduleMulti(beneficiaries, [web3.utils.toHex("template-5-01")], startTimes, {from: wallet_admin}) + ); + }); + + it("Should not be able to modify schedules for the beneficiaries -- fail because of permissions check", async () => { + let timeShift = new BN(durationUtil.seconds(100)); + let startTimes = [currentTime.add(timeShift), currentTime.add(timeShift), currentTime.add(timeShift)]; + + await catchRevert( + I_VestingEscrowWallet.modifyScheduleMulti(beneficiaries, templateNames, startTimes, {from: account_beneficiary1}) + ); + }); + + it("Should modify vesting schedule for 3 beneficiary's addresses", async () => { + let numberOfTokens = [new BN(15000), new BN(15000), new BN(15000)]; + let durations = [new BN(durationUtil.seconds(50)), new BN(durationUtil.seconds(50)), new BN(durationUtil.seconds(50))]; + let frequencies = [new BN(durationUtil.seconds(10)), new BN(durationUtil.seconds(10)), new BN(durationUtil.seconds(10))]; + let timeShift = new BN(durationUtil.seconds(100)); + let startTimes = [currentTime.add(timeShift), currentTime.add(timeShift), currentTime.add(timeShift)]; + + const tx = await I_VestingEscrowWallet.modifyScheduleMulti(beneficiaries, templateNames, startTimes, {from: wallet_admin}); + await increaseTime(110); + + for (let i = 0; i < beneficiaries.length; i++) { + let log = tx.logs[i]; + let beneficiary = beneficiaries[i]; + checkScheduleLog(log, beneficiary, templateNames[i], startTimes[i]); + + let scheduleCount = await I_VestingEscrowWallet.getScheduleCount.call(beneficiary); + assert.equal(scheduleCount, 1); + + let schedule = await I_VestingEscrowWallet.getSchedule.call(beneficiary, templateNames[i]); + checkSchedule(schedule, numberOfTokens[i], durations[i], frequencies[i], startTimes[i], STARTED); + } + }); + + it("Should not be able to send available tokens to the beneficiaries addresses -- fail because of array size", async () => { + await catchRevert( + I_VestingEscrowWallet.pushAvailableTokensMulti(new BN(0), new BN(3), {from: wallet_admin}) + ); + }); + + it("Should not be able to send available tokens to the beneficiaries -- fail because of permissions check", async () => { + await catchRevert( + I_VestingEscrowWallet.pushAvailableTokensMulti(new BN(0), new BN(2), {from: account_beneficiary1}) + ); + }); + + it("Should send available tokens to the beneficiaries addresses", async () => { + const tx = await I_VestingEscrowWallet.pushAvailableTokensMulti(0, 2, {from: wallet_admin}); + + for (let i = 0; i < beneficiaries.length; i++) { + let log = tx.logs[i]; + let beneficiary = beneficiaries[i]; + assert.equal(log.args._numberOfTokens.toString(), 3000); + + let balance = await I_SecurityToken.balanceOf.call(beneficiary); + assert.equal(balance.toString(), 3000); + + await I_SecurityToken.transfer(token_owner, balance, {from: beneficiary}); + await I_VestingEscrowWallet.revokeAllSchedules(beneficiary, {from: wallet_admin}); + await I_VestingEscrowWallet.removeTemplate(templateNames[i], {from: wallet_admin}); + let unassignedTokens = await I_VestingEscrowWallet.unassignedTokens.call(); + await I_VestingEscrowWallet.sendToTreasury(unassignedTokens, {from: wallet_admin}); + } + }); + + it("Should not be able to add schedules from template to the beneficiaries -- fail because of permissions check", async () => { + let templateName = web3.utils.toHex("template-6-01"); + let numberOfTokens = 18000; + let duration = durationUtil.weeks(3); + let frequency = durationUtil.weeks(1); + let templateNames = [templateName, templateName, templateName]; + let startTime = currentTime.add(new BN(durationUtil.seconds(100))); + let startTimes = [startTime, startTime, startTime]; + + let totalNumberOfTokens = numberOfTokens * 3; + await I_SecurityToken.approve(I_VestingEscrowWallet.address, totalNumberOfTokens, {from: token_owner}); + await I_VestingEscrowWallet.depositTokens(totalNumberOfTokens, {from: token_owner}); + await I_VestingEscrowWallet.addTemplate(templateName, numberOfTokens, duration, frequency, {from: wallet_admin}); + + await catchRevert( + I_VestingEscrowWallet.addScheduleFromTemplateMulti(beneficiaries, templateNames, startTimes, {from: account_beneficiary1}) + ); + }); + + it("Should add schedules from template for 3 beneficiaries", async () => { + currentTime = new BN(await latestTime()); + let templateName = web3.utils.toHex("template-6-01"); + let numberOfTokens = 18000; + let duration = durationUtil.weeks(3); + let frequency = durationUtil.weeks(1); + let templateNames = [templateName, templateName, templateName]; + let startTime = currentTime.add(new BN(durationUtil.seconds(100))); + let startTimes = [startTime, startTime, startTime]; + + let tx = await I_VestingEscrowWallet.addScheduleFromTemplateMulti(beneficiaries, templateNames, startTimes, {from: wallet_admin}); + for (let i = 0; i < beneficiaries.length; i++) { + let log = tx.logs[i]; + let beneficiary = beneficiaries[i]; + checkScheduleLog(log, beneficiary, templateName, startTimes[i]); + + let schedule = await I_VestingEscrowWallet.getSchedule.call(beneficiary, templateName); + checkSchedule(schedule, numberOfTokens, duration, frequency, startTimes[i], CREATED); + } + }); + + it("Should not be able to revoke schedules of the beneficiaries -- fail because of permissions check", async () => { + await catchRevert( + I_VestingEscrowWallet.revokeSchedulesMulti(beneficiaries, {from: account_beneficiary1}) + ); + }); + + it("Should revoke vesting schedule from the 3 beneficiary's addresses", async () => { + const tx = await I_VestingEscrowWallet.revokeSchedulesMulti(beneficiaries, {from: wallet_admin}); + + for (let i = 0; i < beneficiaries.length; i++) { + let log = tx.logs[i]; + let beneficiary = beneficiaries[i]; + assert.equal(log.args._beneficiary, beneficiary); + + let scheduleCount = await I_VestingEscrowWallet.getScheduleCount.call(beneficiary); + assert.equal(scheduleCount, 0); + } + + let unassignedTokens = await I_VestingEscrowWallet.unassignedTokens.call(); + await I_VestingEscrowWallet.sendToTreasury(unassignedTokens, {from: wallet_admin}); + }); + + }); + +}); + +function checkTemplateLog(log, templateName, numberOfTokens, duration, frequency) { + assert.equal(web3.utils.hexToUtf8(log.args._name), web3.utils.hexToUtf8(templateName)); + assert.equal(log.args._numberOfTokens.toString(), numberOfTokens); + assert.equal(log.args._duration.toString(), duration); + assert.equal(log.args._frequency.toString(), frequency); +} + +function checkScheduleLog(log, beneficiary, templateName, startTime) { + assert.equal(log.args._beneficiary, beneficiary); + assert.equal(web3.utils.hexToUtf8(log.args._templateName), web3.utils.hexToUtf8(templateName)); + assert.equal(log.args._startTime.toString(), startTime); +} + +function checkSchedule(schedule, numberOfTokens, duration, frequency, startTime, state) { + assert.equal(schedule[0].toString(), numberOfTokens); + assert.equal(schedule[1].toString(), duration); + assert.equal(schedule[2].toString(), frequency); + assert.equal(schedule[3].toString(), startTime); + assert.equal(schedule[5].toString(), state); +} + +function getTotalNumberOfTokens(schedules) { + let numberOfTokens = new BN(0); + for (let i = 0; i < schedules.length; i++) { + numberOfTokens = numberOfTokens.add(new BN(schedules[i].numberOfTokens)); + } + return numberOfTokens; +}