From cbd2f5724d5d4460a787f86745b0be455aae1fcf Mon Sep 17 00:00:00 2001 From: Adam Dossa Date: Fri, 14 Dec 2018 18:49:03 +0000 Subject: [PATCH 01/12] Add ability to see excluded addresses --- .../modules/Checkpoint/DividendCheckpoint.sol | 9 ++++++ .../Checkpoint/DividendCheckpointStorage.sol | 1 + .../Checkpoint/ERC20DividendCheckpoint.sol | 1 + .../Checkpoint/EtherDividendCheckpoint.sol | 3 +- test/e_erc20_dividends.js | 28 ++++++++++++++++++- 5 files changed, 40 insertions(+), 2 deletions(-) diff --git a/contracts/modules/Checkpoint/DividendCheckpoint.sol b/contracts/modules/Checkpoint/DividendCheckpoint.sol index 3a345af25..9d7858ceb 100644 --- a/contracts/modules/Checkpoint/DividendCheckpoint.sol +++ b/contracts/modules/Checkpoint/DividendCheckpoint.sol @@ -226,6 +226,15 @@ contract DividendCheckpoint is DividendCheckpointStorage, ICheckpoint, Module { */ function withdrawWithholding(uint256 _dividendIndex) external; + /** + * @notice Retrieves list of excluded addresses for a dividend + * @param _dividendIndex Dividend to withdraw from + */ + function getExcluded(uint256 _dividendIndex) external view returns (address[]) { + require(_dividendIndex < dividends.length, "Invalid dividend"); + return dividends[_dividendIndex].excluded; + } + /** * @notice Return the permissions flag that are associated with this module * @return bytes32 array diff --git a/contracts/modules/Checkpoint/DividendCheckpointStorage.sol b/contracts/modules/Checkpoint/DividendCheckpointStorage.sol index 4cf1f2838..f56a35bc9 100644 --- a/contracts/modules/Checkpoint/DividendCheckpointStorage.sol +++ b/contracts/modules/Checkpoint/DividendCheckpointStorage.sol @@ -25,6 +25,7 @@ contract DividendCheckpointStorage { uint256 dividendWithheldReclaimed; mapping (address => bool) claimed; // List of addresses which have claimed dividend mapping (address => bool) dividendExcluded; // List of addresses which cannot claim dividends + address[] excluded; bytes32 name; // Name/title - used for identification } diff --git a/contracts/modules/Checkpoint/ERC20DividendCheckpoint.sol b/contracts/modules/Checkpoint/ERC20DividendCheckpoint.sol index b779088f7..54d130940 100644 --- a/contracts/modules/Checkpoint/ERC20DividendCheckpoint.sol +++ b/contracts/modules/Checkpoint/ERC20DividendCheckpoint.sol @@ -192,6 +192,7 @@ contract ERC20DividendCheckpoint is ERC20DividendCheckpointStorage, DividendChec false, 0, 0, + _excluded, _name ) ); diff --git a/contracts/modules/Checkpoint/EtherDividendCheckpoint.sol b/contracts/modules/Checkpoint/EtherDividendCheckpoint.sol index f5a916818..d58886549 100644 --- a/contracts/modules/Checkpoint/EtherDividendCheckpoint.sol +++ b/contracts/modules/Checkpoint/EtherDividendCheckpoint.sol @@ -8,7 +8,7 @@ import "../../interfaces/IOwnable.sol"; */ contract EtherDividendCheckpoint is DividendCheckpoint { using SafeMath for uint256; - + event EtherDividendDeposited( address indexed _depositor, uint256 _checkpointId, @@ -147,6 +147,7 @@ contract EtherDividendCheckpoint is DividendCheckpoint { false, 0, 0, + _excluded, _name ) ); diff --git a/test/e_erc20_dividends.js b/test/e_erc20_dividends.js index 1b2bd7482..1dcbb396e 100644 --- a/test/e_erc20_dividends.js +++ b/test/e_erc20_dividends.js @@ -412,7 +412,10 @@ contract("ERC20DividendCheckpoint", accounts => { dividendName, { from: token_owner } ); + console.log("Gas used w/ no exclusions: " + tx.receipt.gasUsed); + let excluded = await I_ERC20DividendCheckpoint.getExcluded.call(tx.logs[0].args._dividendIndex); assert.equal(tx.logs[0].args._checkpointId.toNumber(), 2, "Dividend should be created at checkpoint 1"); + assert.equal(excluded.length, 0, "Dividend has no exclusions"); }); it("Issuer pushes dividends iterating over account holders - dividends proportional to checkpoint - fails past expiry", async () => { @@ -493,6 +496,17 @@ contract("ERC20DividendCheckpoint", accounts => { await catchRevert(I_ERC20DividendCheckpoint.setDefaultExcluded(addresses, { from: token_owner })); }); + it("Set EXCLUDED_ADDRESS_LIMIT number of addresses to excluded", async () => { + let limit = await I_ERC20DividendCheckpoint.EXCLUDED_ADDRESS_LIMIT(); + limit = limit.toNumber(); + let addresses = []; + addresses.push(account_temp); + while (--limit) addresses.push(limit); + await I_ERC20DividendCheckpoint.setDefaultExcluded(addresses, { from: token_owner }); + let excluded = await I_ERC20DividendCheckpoint.getDefaultExcluded(); + assert.equal(excluded[0], account_temp); + }); + it("Create another new dividend", async () => { let maturity = latestTime(); let expiry = latestTime() + duration.days(10); @@ -506,7 +520,10 @@ contract("ERC20DividendCheckpoint", accounts => { dividendName, { from: token_owner } ); + console.log("Gas used w/ 50 exclusions - default: " + tx.receipt.gasUsed); + let excluded = await I_ERC20DividendCheckpoint.getExcluded.call(tx.logs[0].args._dividendIndex); assert.equal(tx.logs[0].args._checkpointId.toNumber(), 3, "Dividend should be created at checkpoint 2"); + assert.equal(excluded.length, 50, "Dividend "); }); it("should investor 3 claims dividend - fail bad index", async () => { @@ -1062,7 +1079,13 @@ contract("ERC20DividendCheckpoint", accounts => { it("should allow manager with permission to create dividend with exclusion", async () => { let maturity = latestTime() + duration.days(1); let expiry = latestTime() + duration.days(10); - let exclusions = [1]; + + let limit = await I_ERC20DividendCheckpoint.EXCLUDED_ADDRESS_LIMIT(); + limit = limit.toNumber(); + let exclusions = []; + exclusions.push(account_temp); + while (--limit) exclusions.push(limit); + let tx = await I_ERC20DividendCheckpoint.createDividendWithExclusions( maturity, expiry, @@ -1073,6 +1096,9 @@ contract("ERC20DividendCheckpoint", accounts => { { from: account_manager } ); assert.equal(tx.logs[0].args._checkpointId.toNumber(), 9); + console.log("Gas used w/ 50 exclusions - non-default: " + tx.receipt.gasUsed); + let excluded = await I_ERC20DividendCheckpoint.getExcluded.call(tx.logs[0].args._dividendIndex); + assert.equal(excluded.length, 50, "Dividend exclusions should match"); }); it("should allow manager with permission to create dividend with checkpoint and exclusion", async () => { From c0f5594630f74dbb292d63f1d925df8d750d4fff Mon Sep 17 00:00:00 2001 From: Adam Dossa Date: Sun, 16 Dec 2018 20:23:46 +0000 Subject: [PATCH 02/12] Updates --- .../modules/Checkpoint/DividendCheckpoint.sol | 18 ++++++++++++--- .../Checkpoint/DividendCheckpointStorage.sol | 1 - .../Checkpoint/ERC20DividendCheckpoint.sol | 1 - .../Checkpoint/EtherDividendCheckpoint.sol | 1 - test/e_erc20_dividends.js | 22 ++++++++++--------- 5 files changed, 27 insertions(+), 16 deletions(-) diff --git a/contracts/modules/Checkpoint/DividendCheckpoint.sol b/contracts/modules/Checkpoint/DividendCheckpoint.sol index 9d7858ceb..708b2351c 100644 --- a/contracts/modules/Checkpoint/DividendCheckpoint.sol +++ b/contracts/modules/Checkpoint/DividendCheckpoint.sol @@ -227,12 +227,24 @@ contract DividendCheckpoint is DividendCheckpointStorage, ICheckpoint, Module { function withdrawWithholding(uint256 _dividendIndex) external; /** - * @notice Retrieves list of excluded addresses for a dividend + * @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 */ - function getExcluded(uint256 _dividendIndex) external view returns (address[]) { + function getDividendInfo(uint256 _dividendIndex) external view returns (address[], bool[], bool[]) { require(_dividendIndex < dividends.length, "Invalid dividend"); - return dividends[_dividendIndex].excluded; + //Get list of Investors + uint256 checkpointId = dividends[_dividendIndex].checkpointId; + address[] memory investors = ISecurityToken(securityToken).getInvestorsAt(checkpointId); + bool[] memory resultClaimed = new bool[](investors.length); + bool[] memory resultExcluded = new bool[](investors.length); + for (uint256 i; i < investors.length; i++) { + resultClaimed[i] = dividends[_dividendIndex].claimed[investors[i]]; + resultExcluded[i] = dividends[_dividendIndex].dividendExcluded[investors[i]]; + } + return (investors, resultClaimed, resultExcluded); } /** diff --git a/contracts/modules/Checkpoint/DividendCheckpointStorage.sol b/contracts/modules/Checkpoint/DividendCheckpointStorage.sol index f56a35bc9..4cf1f2838 100644 --- a/contracts/modules/Checkpoint/DividendCheckpointStorage.sol +++ b/contracts/modules/Checkpoint/DividendCheckpointStorage.sol @@ -25,7 +25,6 @@ contract DividendCheckpointStorage { uint256 dividendWithheldReclaimed; mapping (address => bool) claimed; // List of addresses which have claimed dividend mapping (address => bool) dividendExcluded; // List of addresses which cannot claim dividends - address[] excluded; bytes32 name; // Name/title - used for identification } diff --git a/contracts/modules/Checkpoint/ERC20DividendCheckpoint.sol b/contracts/modules/Checkpoint/ERC20DividendCheckpoint.sol index 54d130940..b779088f7 100644 --- a/contracts/modules/Checkpoint/ERC20DividendCheckpoint.sol +++ b/contracts/modules/Checkpoint/ERC20DividendCheckpoint.sol @@ -192,7 +192,6 @@ contract ERC20DividendCheckpoint is ERC20DividendCheckpointStorage, DividendChec false, 0, 0, - _excluded, _name ) ); diff --git a/contracts/modules/Checkpoint/EtherDividendCheckpoint.sol b/contracts/modules/Checkpoint/EtherDividendCheckpoint.sol index d58886549..e1cb3174c 100644 --- a/contracts/modules/Checkpoint/EtherDividendCheckpoint.sol +++ b/contracts/modules/Checkpoint/EtherDividendCheckpoint.sol @@ -147,7 +147,6 @@ contract EtherDividendCheckpoint is DividendCheckpoint { false, 0, 0, - _excluded, _name ) ); diff --git a/test/e_erc20_dividends.js b/test/e_erc20_dividends.js index 1dcbb396e..416078971 100644 --- a/test/e_erc20_dividends.js +++ b/test/e_erc20_dividends.js @@ -413,9 +413,8 @@ contract("ERC20DividendCheckpoint", accounts => { { from: token_owner } ); console.log("Gas used w/ no exclusions: " + tx.receipt.gasUsed); - let excluded = await I_ERC20DividendCheckpoint.getExcluded.call(tx.logs[0].args._dividendIndex); assert.equal(tx.logs[0].args._checkpointId.toNumber(), 2, "Dividend should be created at checkpoint 1"); - assert.equal(excluded.length, 0, "Dividend has no exclusions"); + console.log(await I_ERC20DividendCheckpoint.getDividendInfo.call(tx.logs[0].args._dividendIndex)); }); it("Issuer pushes dividends iterating over account holders - dividends proportional to checkpoint - fails past expiry", async () => { @@ -500,11 +499,9 @@ contract("ERC20DividendCheckpoint", accounts => { let limit = await I_ERC20DividendCheckpoint.EXCLUDED_ADDRESS_LIMIT(); limit = limit.toNumber(); let addresses = []; - addresses.push(account_temp); while (--limit) addresses.push(limit); await I_ERC20DividendCheckpoint.setDefaultExcluded(addresses, { from: token_owner }); let excluded = await I_ERC20DividendCheckpoint.getDefaultExcluded(); - assert.equal(excluded[0], account_temp); }); it("Create another new dividend", async () => { @@ -520,10 +517,9 @@ contract("ERC20DividendCheckpoint", accounts => { dividendName, { from: token_owner } ); - console.log("Gas used w/ 50 exclusions - default: " + tx.receipt.gasUsed); - let excluded = await I_ERC20DividendCheckpoint.getExcluded.call(tx.logs[0].args._dividendIndex); + 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(excluded.length, 50, "Dividend "); + console.log(await I_ERC20DividendCheckpoint.getDividendInfo.call(tx.logs[0].args._dividendIndex)); }); it("should investor 3 claims dividend - fail bad index", async () => { @@ -539,9 +535,14 @@ contract("ERC20DividendCheckpoint", accounts => { let investor1BalanceAfter1 = new BigNumber(await I_PolyToken.balanceOf(account_investor1)); let investor2BalanceAfter1 = new BigNumber(await I_PolyToken.balanceOf(account_investor2)); let investor3BalanceAfter1 = new BigNumber(await I_PolyToken.balanceOf(account_investor3)); + console.log(await I_ERC20DividendCheckpoint.dividends(2)); assert.equal(investor1BalanceAfter1.sub(investor1Balance).toNumber(), 0); assert.equal(investor2BalanceAfter1.sub(investor2Balance).toNumber(), 0); assert.equal(investor3BalanceAfter1.sub(investor3Balance).toNumber(), web3.utils.toWei("7", "ether")); + let info = await I_ERC20DividendCheckpoint.getDividendInfo.call(2); + console.log(info); + // assert.equal(info[0][2], account_temp, "account_temp is excluded"); + // assert.equal(info[2][2], true, "account_temp is excluded"); }); it("should investor 3 claims dividend - fails already claimed", async () => { @@ -1096,9 +1097,10 @@ contract("ERC20DividendCheckpoint", accounts => { { from: account_manager } ); assert.equal(tx.logs[0].args._checkpointId.toNumber(), 9); - console.log("Gas used w/ 50 exclusions - non-default: " + tx.receipt.gasUsed); - let excluded = await I_ERC20DividendCheckpoint.getExcluded.call(tx.logs[0].args._dividendIndex); - assert.equal(excluded.length, 50, "Dividend exclusions should match"); + console.log("Gas used w/ max exclusions - non-default: " + tx.receipt.gasUsed); + let info = await I_ERC20DividendCheckpoint.getDividendInfo.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 () => { From 5574125aa37735e2e6cf49b23f8ce89e392aafe6 Mon Sep 17 00:00:00 2001 From: Adam Dossa Date: Sun, 16 Dec 2018 20:57:32 +0000 Subject: [PATCH 03/12] Fixes --- .../modules/Checkpoint/DividendCheckpoint.sol | 20 ++++++++++++++++++ test/e_erc20_dividends.js | 21 ++++++------------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/contracts/modules/Checkpoint/DividendCheckpoint.sol b/contracts/modules/Checkpoint/DividendCheckpoint.sol index 708b2351c..497f6a5bc 100644 --- a/contracts/modules/Checkpoint/DividendCheckpoint.sol +++ b/contracts/modules/Checkpoint/DividendCheckpoint.sol @@ -247,6 +247,26 @@ contract DividendCheckpoint is DividendCheckpointStorage, ICheckpoint, Module { return (investors, resultClaimed, resultExcluded); } + /** + * @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 diff --git a/test/e_erc20_dividends.js b/test/e_erc20_dividends.js index 416078971..b0256eec0 100644 --- a/test/e_erc20_dividends.js +++ b/test/e_erc20_dividends.js @@ -414,7 +414,6 @@ contract("ERC20DividendCheckpoint", accounts => { ); 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"); - console.log(await I_ERC20DividendCheckpoint.getDividendInfo.call(tx.logs[0].args._dividendIndex)); }); it("Issuer pushes dividends iterating over account holders - dividends proportional to checkpoint - fails past expiry", async () => { @@ -492,18 +491,10 @@ contract("ERC20DividendCheckpoint", accounts => { let addresses = []; addresses.push(account_temp); while (limit--) addresses.push(limit); + console.log(addresses.length); await catchRevert(I_ERC20DividendCheckpoint.setDefaultExcluded(addresses, { from: token_owner })); }); - it("Set EXCLUDED_ADDRESS_LIMIT number of addresses to excluded", async () => { - let limit = await I_ERC20DividendCheckpoint.EXCLUDED_ADDRESS_LIMIT(); - limit = limit.toNumber(); - let addresses = []; - while (--limit) addresses.push(limit); - await I_ERC20DividendCheckpoint.setDefaultExcluded(addresses, { from: token_owner }); - let excluded = await I_ERC20DividendCheckpoint.getDefaultExcluded(); - }); - it("Create another new dividend", async () => { let maturity = latestTime(); let expiry = latestTime() + duration.days(10); @@ -519,7 +510,7 @@ contract("ERC20DividendCheckpoint", accounts => { ); 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"); - console.log(await I_ERC20DividendCheckpoint.getDividendInfo.call(tx.logs[0].args._dividendIndex)); + 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 () => { @@ -535,14 +526,14 @@ contract("ERC20DividendCheckpoint", accounts => { let investor1BalanceAfter1 = new BigNumber(await I_PolyToken.balanceOf(account_investor1)); let investor2BalanceAfter1 = new BigNumber(await I_PolyToken.balanceOf(account_investor2)); let investor3BalanceAfter1 = new BigNumber(await I_PolyToken.balanceOf(account_investor3)); - console.log(await I_ERC20DividendCheckpoint.dividends(2)); assert.equal(investor1BalanceAfter1.sub(investor1Balance).toNumber(), 0); assert.equal(investor2BalanceAfter1.sub(investor2Balance).toNumber(), 0); assert.equal(investor3BalanceAfter1.sub(investor3Balance).toNumber(), web3.utils.toWei("7", "ether")); let info = await I_ERC20DividendCheckpoint.getDividendInfo.call(2); - console.log(info); - // assert.equal(info[0][2], account_temp, "account_temp is excluded"); - // assert.equal(info[2][2], true, "account_temp is excluded"); + assert.equal(info[0][1], account_temp, "account_temp is excluded"); + assert.equal(info[2][1], true, "account_temp is excluded"); + assert.equal(info[0][2], account_investor3, "account_investor3 is claimed"); + assert.equal(info[1][2], true, "account_temp is claimed"); }); it("should investor 3 claims dividend - fails already claimed", async () => { From b01b826c06675f73278761d07cb788da5fdd9105 Mon Sep 17 00:00:00 2001 From: Adam Dossa Date: Sun, 16 Dec 2018 21:13:22 +0000 Subject: [PATCH 04/12] Bump excluded limit to 150 --- contracts/modules/Checkpoint/DividendCheckpointStorage.sol | 2 +- test/e_erc20_dividends.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/modules/Checkpoint/DividendCheckpointStorage.sol b/contracts/modules/Checkpoint/DividendCheckpointStorage.sol index 4cf1f2838..fd95e7e00 100644 --- a/contracts/modules/Checkpoint/DividendCheckpointStorage.sol +++ b/contracts/modules/Checkpoint/DividendCheckpointStorage.sol @@ -6,7 +6,7 @@ pragma solidity ^0.4.24; */ contract DividendCheckpointStorage { - uint256 public EXCLUDED_ADDRESS_LIMIT = 50; + uint256 public EXCLUDED_ADDRESS_LIMIT = 150; bytes32 public constant DISTRIBUTE = "DISTRIBUTE"; bytes32 public constant MANAGE = "MANAGE"; bytes32 public constant CHECKPOINT = "CHECKPOINT"; diff --git a/test/e_erc20_dividends.js b/test/e_erc20_dividends.js index b0256eec0..e77ff69e6 100644 --- a/test/e_erc20_dividends.js +++ b/test/e_erc20_dividends.js @@ -1077,7 +1077,7 @@ contract("ERC20DividendCheckpoint", accounts => { let exclusions = []; exclusions.push(account_temp); while (--limit) exclusions.push(limit); - + console.log(exclusions.length); let tx = await I_ERC20DividendCheckpoint.createDividendWithExclusions( maturity, expiry, From d5058bf4cd1c132ef03b2a6ab14c3a34256ed079 Mon Sep 17 00:00:00 2001 From: Adam Dossa Date: Mon, 17 Dec 2018 13:45:16 +0000 Subject: [PATCH 05/12] Use investor list from checkpoint --- contracts/modules/Checkpoint/DividendCheckpoint.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/modules/Checkpoint/DividendCheckpoint.sol b/contracts/modules/Checkpoint/DividendCheckpoint.sol index 497f6a5bc..daa5f1e50 100644 --- a/contracts/modules/Checkpoint/DividendCheckpoint.sol +++ b/contracts/modules/Checkpoint/DividendCheckpoint.sol @@ -142,7 +142,8 @@ contract DividendCheckpoint is DividendCheckpointStorage, ICheckpoint, Module { validDividendIndex(_dividendIndex) { Dividend storage dividend = dividends[_dividendIndex]; - address[] memory investors = ISecurityToken(securityToken).getInvestors(); + uint256 checkpointId = dividend.checkpointId; + address[] memory investors = ISecurityToken(securityToken).getInvestorsAt(checkpointId); uint256 numberInvestors = Math.min256(investors.length, _start.add(_iterations)); for (uint256 i = _start; i < numberInvestors; i++) { address payee = investors[i]; From 65b2b49fd9bf8a48526b82efb3b8e78abf51ed5a Mon Sep 17 00:00:00 2001 From: Adam Dossa Date: Wed, 19 Dec 2018 22:32:22 +0000 Subject: [PATCH 06/12] Track withheld at dividend granularity --- .../modules/Checkpoint/DividendCheckpoint.sol | 41 ++++++++++++++++++- .../Checkpoint/DividendCheckpointStorage.sol | 8 ++-- .../Checkpoint/ERC20DividendCheckpoint.sol | 10 +++-- .../Checkpoint/EtherDividendCheckpoint.sol | 10 +++-- test/e_erc20_dividends.js | 30 +++++++++++--- 5 files changed, 79 insertions(+), 20 deletions(-) diff --git a/contracts/modules/Checkpoint/DividendCheckpoint.sol b/contracts/modules/Checkpoint/DividendCheckpoint.sol index daa5f1e50..6f1887693 100644 --- a/contracts/modules/Checkpoint/DividendCheckpoint.sol +++ b/contracts/modules/Checkpoint/DividendCheckpoint.sol @@ -227,25 +227,62 @@ contract DividendCheckpoint is DividendCheckpointStorage, ICheckpoint, Module { */ 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] = dividends[i].created; + maturitys[i] = dividends[i].maturity; + expirys[i] = dividends[i].expiry; + amounts[i] = dividends[i].amount; + claimedAmounts[i] = dividends[i].claimedAmount; + names[i] = dividends[i].name; + } + return (createds, maturitys, expirys, amounts, claimedAmounts, names); + } + /** * @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 */ - function getDividendInfo(uint256 _dividendIndex) external view returns (address[], bool[], bool[]) { + function getDividendProgress(uint256 _dividendIndex) external view returns (address[], bool[], bool[], uint256[]) { require(_dividendIndex < dividends.length, "Invalid dividend"); //Get list of Investors uint256 checkpointId = dividends[_dividendIndex].checkpointId; address[] memory investors = ISecurityToken(securityToken).getInvestorsAt(checkpointId); bool[] memory resultClaimed = new bool[](investors.length); bool[] memory resultExcluded = new bool[](investors.length); + uint256[] memory resultWithheld = new uint256[](investors.length); for (uint256 i; i < investors.length; i++) { resultClaimed[i] = dividends[_dividendIndex].claimed[investors[i]]; resultExcluded[i] = dividends[_dividendIndex].dividendExcluded[investors[i]]; + resultWithheld[i] = dividends[_dividendIndex].withheld[investors[i]]; } - return (investors, resultClaimed, resultExcluded); + return (investors, resultClaimed, resultExcluded, resultWithheld); } /** diff --git a/contracts/modules/Checkpoint/DividendCheckpointStorage.sol b/contracts/modules/Checkpoint/DividendCheckpointStorage.sol index fd95e7e00..85e7e789a 100644 --- a/contracts/modules/Checkpoint/DividendCheckpointStorage.sol +++ b/contracts/modules/Checkpoint/DividendCheckpointStorage.sol @@ -21,10 +21,11 @@ contract DividendCheckpointStorage { 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; + 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 } @@ -37,7 +38,4 @@ contract DividendCheckpointStorage { // Mapping from address to withholding tax as a percentage * 10**16 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 b779088f7..174498537 100644 --- a/contracts/modules/Checkpoint/ERC20DividendCheckpoint.sol +++ b/contracts/modules/Checkpoint/ERC20DividendCheckpoint.sol @@ -241,8 +241,10 @@ contract ERC20DividendCheckpoint is ERC20DividendCheckpointStorage, DividendChec 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); + if (withheld > 0) { + _dividend.totalWithheld = _dividend.totalWithheld.add(withheld); + _dividend.withheld[_payee] = withheld; + } emit ERC20DividendClaimed(_payee, _dividendIndex, dividendTokens[_dividendIndex], claim, withheld); } } @@ -271,8 +273,8 @@ contract ERC20DividendCheckpoint is ERC20DividendCheckpointStorage, DividendChec 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; + uint256 remainingWithheld = dividend.totalWithheld.sub(dividend.totalWithheldWithdrawn); + dividend.totalWithheldWithdrawn = dividend.totalWithheld; address owner = IOwnable(securityToken).owner(); require(IERC20(dividendTokens[_dividendIndex]).transfer(owner, remainingWithheld), "transfer failed"); emit ERC20DividendWithholdingWithdrawn(owner, _dividendIndex, dividendTokens[_dividendIndex], remainingWithheld); diff --git a/contracts/modules/Checkpoint/EtherDividendCheckpoint.sol b/contracts/modules/Checkpoint/EtherDividendCheckpoint.sol index e1cb3174c..3397f208b 100644 --- a/contracts/modules/Checkpoint/EtherDividendCheckpoint.sol +++ b/contracts/modules/Checkpoint/EtherDividendCheckpoint.sol @@ -176,8 +176,10 @@ contract EtherDividendCheckpoint is DividendCheckpoint { /*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); + if (withheld > 0) { + _dividend.totalWithheld = _dividend.totalWithheld.add(withheld); + _dividend.withheld[_payee] = withheld; + } emit EtherDividendClaimed(_payee, _dividendIndex, claim, withheld); } else { _dividend.claimed[_payee] = false; @@ -210,8 +212,8 @@ contract EtherDividendCheckpoint is DividendCheckpoint { 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; + uint256 remainingWithheld = dividend.totalWithheld.sub(dividend.totalWithheldWithdrawn); + dividend.totalWithheldWithdrawn = dividend.totalWithheld; address owner = IOwnable(securityToken).owner(); owner.transfer(remainingWithheld); emit EtherDividendWithholdingWithdrawn(owner, _dividendIndex, remainingWithheld); diff --git a/test/e_erc20_dividends.js b/test/e_erc20_dividends.js index e77ff69e6..192e13598 100644 --- a/test/e_erc20_dividends.js +++ b/test/e_erc20_dividends.js @@ -330,6 +330,12 @@ contract("ERC20DividendCheckpoint", 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].toNumber(), web3.utils.toWei("1.5", "ether"), "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 () => { @@ -529,11 +535,16 @@ contract("ERC20DividendCheckpoint", accounts => { assert.equal(investor1BalanceAfter1.sub(investor1Balance).toNumber(), 0); assert.equal(investor2BalanceAfter1.sub(investor2Balance).toNumber(), 0); assert.equal(investor3BalanceAfter1.sub(investor3Balance).toNumber(), web3.utils.toWei("7", "ether")); - let info = await I_ERC20DividendCheckpoint.getDividendInfo.call(2); - assert.equal(info[0][1], account_temp, "account_temp is excluded"); + 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[0][2], account_investor3, "account_investor3 is claimed"); - assert.equal(info[1][2], true, "account_temp is claimed"); + 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 () => { @@ -832,6 +843,15 @@ contract("ERC20DividendCheckpoint", accounts => { }); it("Issuer reclaims withholding tax", async () => { + let info = await I_ERC20DividendCheckpoint.getDividendProgress.call(3); + 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].toNumber(), 0, "withheld match"); + assert.equal(info[3][1].toNumber(), web3.utils.toWei("0.2", "ether"), "withheld match"); + assert.equal(info[3][2].toNumber(), web3.utils.toWei("0.2", "ether"), "withheld match"); + assert.equal(info[3][3].toNumber(), 0, "withheld match"); let issuerBalance = new BigNumber(await I_PolyToken.balanceOf(token_owner)); await I_ERC20DividendCheckpoint.withdrawWithholding(3, { from: token_owner, gasPrice: 0 }); let issuerBalanceAfter = new BigNumber(await I_PolyToken.balanceOf(token_owner)); @@ -1089,7 +1109,7 @@ contract("ERC20DividendCheckpoint", accounts => { ); 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.getDividendInfo.call(tx.logs[0].args._dividendIndex); + 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"); }); From 213c4981bc64bdbddde29616b72e49e694e93b55 Mon Sep 17 00:00:00 2001 From: Adam Dossa Date: Thu, 20 Dec 2018 18:09:37 +0000 Subject: [PATCH 07/12] Add balances & claim amounts to getters --- .../modules/Checkpoint/DividendCheckpoint.sol | 62 ++++++++++++++----- test/e_erc20_dividends.js | 1 + 2 files changed, 46 insertions(+), 17 deletions(-) diff --git a/contracts/modules/Checkpoint/DividendCheckpoint.sol b/contracts/modules/Checkpoint/DividendCheckpoint.sol index 6f1887693..929bf977c 100644 --- a/contracts/modules/Checkpoint/DividendCheckpoint.sol +++ b/contracts/modules/Checkpoint/DividendCheckpoint.sol @@ -251,14 +251,33 @@ contract DividendCheckpoint is DividendCheckpointStorage, ICheckpoint, Module { claimedAmounts = new uint256[](dividends.length); names = new bytes32[](dividends.length); for (uint256 i = 0; i < dividends.length; i++) { - createds[i] = dividends[i].created; - maturitys[i] = dividends[i].maturity; - expirys[i] = dividends[i].expiry; - amounts[i] = dividends[i].amount; - claimedAmounts[i] = dividends[i].claimedAmount; - names[i] = dividends[i].name; + (createds[i], maturitys[i], expirys[i], amounts[i], claimedAmounts[i], names[i]) = getDividendData(i); } - return (createds, maturitys, expirys, amounts, claimedAmounts, names); + } + + /** + * @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; } /** @@ -268,21 +287,30 @@ contract DividendCheckpoint is DividendCheckpointStorage, ICheckpoint, Module { * @return bool[] whether investor has claimed * @return bool[] whether investor is excluded * @return uint256[] amount of withheld tax + * @return uint256[] investor balance + * @return uint256[] amount to be claimed including withheld tax */ - function getDividendProgress(uint256 _dividendIndex) external view returns (address[], bool[], bool[], uint256[]) { + function getDividendProgress(uint256 _dividendIndex) external view returns (address[] memory investors, bool[] memory resultClaimed, bool[] memory resultExcluded, uint256[] memory resultWithheld, uint256[] memory resultBalance, uint256[] memory resultAmount) { require(_dividendIndex < dividends.length, "Invalid dividend"); //Get list of Investors - uint256 checkpointId = dividends[_dividendIndex].checkpointId; - address[] memory investors = ISecurityToken(securityToken).getInvestorsAt(checkpointId); - bool[] memory resultClaimed = new bool[](investors.length); - bool[] memory resultExcluded = new bool[](investors.length); - uint256[] memory resultWithheld = new uint256[](investors.length); + 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); + resultBalance = new uint256[](investors.length); + resultAmount = new uint256[](investors.length); for (uint256 i; i < investors.length; i++) { - resultClaimed[i] = dividends[_dividendIndex].claimed[investors[i]]; - resultExcluded[i] = dividends[_dividendIndex].dividendExcluded[investors[i]]; - resultWithheld[i] = dividends[_dividendIndex].withheld[investors[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]) { + resultWithheld[i] = dividend.withheld[investors[i]]; + resultAmount[i] = resultBalance[i].mul(dividend.amount).div(dividend.totalSupply); + } } - return (investors, resultClaimed, resultExcluded, resultWithheld); + /* return (investors, resultClaimed, resultExcluded, resultWithheld, resultBalance, resultAmount); */ } /** diff --git a/test/e_erc20_dividends.js b/test/e_erc20_dividends.js index 192e13598..3ec86dadc 100644 --- a/test/e_erc20_dividends.js +++ b/test/e_erc20_dividends.js @@ -844,6 +844,7 @@ contract("ERC20DividendCheckpoint", accounts => { it("Issuer reclaims withholding tax", async () => { let info = await I_ERC20DividendCheckpoint.getDividendProgress.call(3); + console.log(info); 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"); From a1ed6323619f7291d35daf9b04dfef89bed2abc1 Mon Sep 17 00:00:00 2001 From: Victor Date: Thu, 20 Dec 2018 16:37:50 -0300 Subject: [PATCH 08/12] CLI - Flow change and updates for dividends_manager --- CLI/commands/dividends_manager.js | 1158 ++++++++++------- ...xclusions_data.csv => exclusions_data.csv} | 0 CLI/data/Checkpoint/tax_withholding_data.csv | 10 + 3 files changed, 709 insertions(+), 459 deletions(-) rename CLI/data/Checkpoint/{dividendsExclusions_data.csv => exclusions_data.csv} (100%) create mode 100644 CLI/data/Checkpoint/tax_withholding_data.csv diff --git a/CLI/commands/dividends_manager.js b/CLI/commands/dividends_manager.js index 19ef24910..f021d6e2c 100644 --- a/CLI/commands/dividends_manager.js +++ b/CLI/commands/dividends_manager.js @@ -1,487 +1,601 @@ -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 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 currentCheckpoint = await securityToken.methods.currentCheckpointId().call(); + if (currentCheckpoint > 0) { + console.log(`\nCurrent checkpoint: ${currentCheckpoint}`); } -} - -async function start_explorer() { - console.log('\n\x1b[34m%s\x1b[0m', "Dividends Manager - Main Menu"); - if (!tokenSymbol) - tokenSymbol = readlineSync.question('Enter the token symbol: '); - - 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); + let options = ['Create checkpoint', 'Explore address balances']; - // 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}`) - } + 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; + } - let currentCheckpoint = await securityToken.methods.currentCheckpointId().call(); - console.log(chalk.yellow(`\nToken is at checkpoint: ${currentCheckpoint}`)); + await executeApp(); +} - let options = ['Mint tokens', 'Transfer tokens', 'Create checkpoint', 'Set default exclusions for dividends', 'Tax holding settings', 'Create dividends'] +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!`)); +} - if (currentCheckpoint > 0) { - options.push('Explore account at checkpoint', 'Explore total supply at checkpoint') - } +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); + } - // Only show dividend options if divididenModule is already attached - if (await isDividendsModuleAttached()) { - options.push('Push dividends to accounts', - `Explore ${dividendsType} balance`, 'Reclaim expired dividends') - } + 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 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; - } - } + 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}`); } - //Restart - await start_explorer(); } -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); - - 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.")); - } +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; } + + await dividendsManager(); } -async function transferTokens(address, amount) { - await whitelistAddress(address); +async function dividendsManager() { + console.log(chalk.blue(`Dividends module at ${currentDividendsModule.options.address}`), '\n'); - 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 currentDividends = await getDividends(); + let defaultExcluded = await currentDividendsModule.methods.getDefaultExcluded().call(); + + console.log(`- Current dividends: ${currentDividends.length}`); + console.log(`- Default exclusions: ${defaultExcluded.length}`); + + let options = ['Create checkpoint']; + 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'); -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)`); + 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 'Create checkpoint': + await createCheckpointFromDividendModule(); + break; + 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; + } - 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})`); + await dividendsManager(); } -async function exploreTotalSupply(checkpoint) { - let totalSupply = await securityToken.methods.totalSupply().call(); - totalSupply = web3.utils.fromWei(totalSupply); - console.log(`TotalSupply is: ${totalSupply} (Using totalSupply)`); - - let totalSupplyAt = await securityToken.methods.totalSupplyAt(checkpoint).call(); - totalSupplyAt = web3.utils.fromWei(totalSupplyAt); - console.log(`TotalSupply is: ${totalSupplyAt} (Using totalSupplyAt - checkpoint ${checkpoint})`); +async function createCheckpointFromDividendModule() { + let createCheckpointAction = securityToken.methods.createCheckpoint(); + await common.sendTransaction(createCheckpointAction); + console.log(chalk.green(`Checkpoint have been created successfully!`)); } 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.`)); + 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 balanceArray = progress[4]; + let amountArray = progress[5]; + + 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(`- Withheld: ${web3.utils.fromWei(dividend.totalWithheld)} ${dividendTokenSymbol}`); + console.log(`- Withheld claimed: ${web3.utils.fromWei(dividend.totalWithheldWithdrawn)} ${dividendTokenSymbol}`); + console.log(`- Total investors: ${investorArray.length}`); + console.log(` Have already claimed: ${claimedArray.filter(c => c).length}`); + console.log(` Excluded: ${excludedArray.filter(e => e).length} `); + // ------------------ + + + 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 'Explore account': + await exploreAccount(dividendIndex, dividendTokenAddress, dividendTokenSymbol); break; - case 'Return to main menu': + 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(); - 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 }); + await manageExistingDividend(dividendIndex); +} - 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 { + 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(dividendAmountBN) }); + let event = common.getEventFromLogs(currentDividendsModule._jsonInterface, receipt.logs, 'EtherDividendDeposited'); + console.log(` +Dividend ${ event._dividendIndex} deposited` + ); } - 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) { - console.log(` - Failed to claim ${web3.utils.fromWei(event._amount)} ${dividendsType} - to account ${event._payee}` - ); - } +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' + ]); + } + console.log(); + console.log(table(dataTable)); +} + +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 amount = !excluded ? web3.utils.fromWei(_amountArray[i]) : 0; + let withheld = !excluded ? web3.utils.fromWei(_withheldArray[i]) : 'NA'; + let withheldPercentage = !excluded ? web3.utils.toBN(_withheldArray[i]).div(web3.utils.toBN(_amountArray[i])).muln(100) : 'NA'; + let received = !excluded ? web3.utils.fromWei(web3.utils.toBN(_amountArray[i]).sub(web3.utils.toBN(_withheldArray[i]))) : 0; + let withdrawn = _claimedArray[i] ? 'YES' : 'NO'; + 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(chalk.yellow(`-----------------------------------------------------------------------------------------------------------------------------------------------------------`)); + console.log(); +} - 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 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 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'; +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); + + 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} `); + } } + 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.`)); +} + +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)} ${dividendsType} - to account ${event._claimer}` +Reclaimed amount ${ web3.utils.fromWei(event._claimedAmount)} ${dividendTokenSymbol} +to account ${ event._claimer} ` ); } -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}.`)); +async function addDividendsModule() { + 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 bytes = web3.utils.fromAscii('', 16); + + 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(chalk.green(`Module deployed at address: ${event._module} `)); + } } // 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 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); } } -async function isDividendsModuleAttached() { - let dividendsModuleName; - if (dividendsType == 'POLY') { - dividendsModuleName = 'ERC20DividendCheckpoint'; - } else if (dividendsType == 'ETH') { - dividendsModuleName = 'EtherDividendCheckpoint'; +function logPushResults(receipt) { + let successEventName; + if (dividendsType == 'ERC20') { + successEventName = 'ERC20DividendClaimed'; } - - 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(); + 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')); } - currentDividendsModule = new web3.eth.Contract(dividendsModuleABI, dividendsModuleAddress); - currentDividendsModule.setProvider(web3.currentProvider); } - - return (typeof currentDividendsModule !== 'undefined'); -} - -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 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 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); + 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) { - 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); + 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() { @@ -498,91 +612,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; +} +function isExpiredDividend(dividend) { 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); - } - } + return now > dividend.expiry; +} - if (dividends.length > 0) { - let options = dividends.map(function (d) { - return `${web3.utils.toAscii(d.name)} - 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}` - }); +function hasRemaining(dividend) { + return Number(new web3.utils.BN(dividend.amount).sub(new web3.utils.BN(dividend.claimedAmount))).toFixed(10) > 0; +} - 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`)) +function hasRemainingWithheld(dividend) { + return Number(new web3.utils.BN(dividend.dividendWithheld).sub(new web3.utils.BN(dividend.dividendWithheldReclaimed))).toFixed(10) > 0; +} + +async function selectDividend(dividends) { + let result = null; + let options = dividends.map(function (d) { + return `${d.name} + Amount: ${ web3.utils.fromWei(d.amount)} ${dividendsType} + Status: ${ isExpiredDividend(d) ? 'Expired' : hasRemaining(d) ? 'In progress' : 'Completed'} + Token: ${ d.tokenSymbol} + Created: ${ moment.unix(d.created).format('MMMM Do YYYY, HH:mm:ss')} + Expiry: ${ moment.unix(d.expiry).format('MMMM Do YYYY, HH:mm:ss')} ` + }); + + 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(`${__dirname}/../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/data/Checkpoint/dividendsExclusions_data.csv b/CLI/data/Checkpoint/exclusions_data.csv similarity index 100% rename from CLI/data/Checkpoint/dividendsExclusions_data.csv rename to CLI/data/Checkpoint/exclusions_data.csv 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 From e5fab1f14b8e4ed799aede84463afd98fbf6c426 Mon Sep 17 00:00:00 2001 From: Adam Dossa Date: Fri, 21 Dec 2018 10:01:35 +0000 Subject: [PATCH 09/12] Add more test cases --- .../modules/Checkpoint/DividendCheckpoint.sol | 9 ++++++++- test/e_erc20_dividends.js | 18 +++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/contracts/modules/Checkpoint/DividendCheckpoint.sol b/contracts/modules/Checkpoint/DividendCheckpoint.sol index 929bf977c..4aa939c20 100644 --- a/contracts/modules/Checkpoint/DividendCheckpoint.sol +++ b/contracts/modules/Checkpoint/DividendCheckpoint.sol @@ -290,7 +290,14 @@ contract DividendCheckpoint is DividendCheckpointStorage, ICheckpoint, Module { * @return uint256[] investor balance * @return uint256[] amount to be claimed including withheld tax */ - function getDividendProgress(uint256 _dividendIndex) external view returns (address[] memory investors, bool[] memory resultClaimed, bool[] memory resultExcluded, uint256[] memory resultWithheld, uint256[] memory resultBalance, uint256[] memory resultAmount) { + function getDividendProgress(uint256 _dividendIndex) external view returns ( + address[] memory investors, + bool[] memory resultClaimed, + bool[] memory resultExcluded, + uint256[] memory resultWithheld, + uint256[] memory resultBalance, + uint256[] memory resultAmount) + { require(_dividendIndex < dividends.length, "Invalid dividend"); //Get list of Investors Dividend storage dividend = dividends[_dividendIndex]; diff --git a/test/e_erc20_dividends.js b/test/e_erc20_dividends.js index 3ec86dadc..bc52e3886 100644 --- a/test/e_erc20_dividends.js +++ b/test/e_erc20_dividends.js @@ -719,7 +719,7 @@ contract("ERC20DividendCheckpoint", 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 () => { @@ -853,6 +853,22 @@ contract("ERC20DividendCheckpoint", accounts => { assert.equal(info[3][1].toNumber(), web3.utils.toWei("0.2", "ether"), "withheld match"); assert.equal(info[3][2].toNumber(), web3.utils.toWei("0.2", "ether"), "withheld match"); assert.equal(info[3][3].toNumber(), 0, "withheld match"); + + console.log(info[4][0].toNumber()); + console.log(info[4][1].toNumber()); + console.log(info[4][2].toNumber()); + console.log(info[4][3].toNumber()); + + assert.equal(info[4][0].toNumber(), (await I_SecurityToken.balanceOfAt(account_investor1, 4)).toNumber(), "balance match"); + assert.equal(info[4][1].toNumber(), (await I_SecurityToken.balanceOfAt(account_investor2, 4)).toNumber(), "balance match"); + assert.equal(info[4][2].toNumber(), (await I_SecurityToken.balanceOfAt(account_temp, 4)).toNumber(), "balance match"); + assert.equal(info[4][3].toNumber(), (await I_SecurityToken.balanceOfAt(account_investor3, 4)).toNumber(), "balance match"); + + assert.equal(info[5][0].toNumber(), 0, "excluded"); + assert.equal(info[5][1].toNumber(), web3.utils.toWei("2", "ether"), "claim match"); + assert.equal(info[5][2].toNumber(), web3.utils.toWei("1", "ether"), "claim match"); + assert.equal(info[5][3].toNumber(), web3.utils.toWei("7", "ether"), "claim match"); + let issuerBalance = new BigNumber(await I_PolyToken.balanceOf(token_owner)); await I_ERC20DividendCheckpoint.withdrawWithholding(3, { from: token_owner, gasPrice: 0 }); let issuerBalanceAfter = new BigNumber(await I_PolyToken.balanceOf(token_owner)); From 2ba0db0c83431134ba6d0c64da71f08f6a240458 Mon Sep 17 00:00:00 2001 From: Victor Date: Fri, 21 Dec 2018 08:58:18 -0300 Subject: [PATCH 10/12] CLI Minor fixes --- CLI/commands/dividends_manager.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/CLI/commands/dividends_manager.js b/CLI/commands/dividends_manager.js index f021d6e2c..f65074f88 100644 --- a/CLI/commands/dividends_manager.js +++ b/CLI/commands/dividends_manager.js @@ -338,12 +338,12 @@ async function createDividends() { token = new web3.eth.Contract(abis.erc20(), dividendToken); try { dividendSymbol = await token.methods.symbol().call(); - } catch { + } catch (err) { console.log(chalk.red(`${dividendToken} is not a valid ERC20 token address!!`)); } } while (dividendSymbol === 'ETH'); } - let dividendAmount = readlineSync.question(`How much ${dividendSymbol} would you like to distribute to token holders ? `); + 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))); @@ -356,7 +356,7 @@ async function createDividends() { 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 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') { @@ -425,7 +425,7 @@ function showReport(_name, _tokenSymbol, _amount, _witthheld, _claimed, _investo 'Investor', 'Amount sent', 'Taxes withheld (%)', - `Taxes withheld(${_tokenSymbol})`, + `Taxes withheld (${_tokenSymbol})`, 'Amount received', 'Withdrawn' ]]; @@ -434,7 +434,7 @@ function showReport(_name, _tokenSymbol, _amount, _witthheld, _claimed, _investo let excluded = _excludedArray[i]; let amount = !excluded ? web3.utils.fromWei(_amountArray[i]) : 0; let withheld = !excluded ? web3.utils.fromWei(_withheldArray[i]) : 'NA'; - let withheldPercentage = !excluded ? web3.utils.toBN(_withheldArray[i]).div(web3.utils.toBN(_amountArray[i])).muln(100) : 'NA'; + let withheldPercentage = !excluded ? (withheld !== '0' ? parseFloat(withheld) / parseFloat(amount) * 100 : 0) : 'NA'; let received = !excluded ? web3.utils.fromWei(web3.utils.toBN(_amountArray[i]).sub(web3.utils.toBN(_withheldArray[i]))) : 0; let withdrawn = _claimedArray[i] ? 'YES' : 'NO'; dataTable.push([ @@ -473,7 +473,7 @@ async function pushDividends(dividendIndex, checkpointId) { } 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 ? `); + 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); @@ -539,7 +539,7 @@ async function addDividendsModule() { })); 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 ? `)) { + if (index != -1 && readlineSync.keyInYNStrict(`Are you sure you want to add ${options[index]} module? `)) { let bytes = web3.utils.fromAscii('', 16); let selectedDividendFactoryAddress = await contracts.getModuleFactoryAddressByName(securityToken.options.address, gbl.constants.MODULES_TYPES.DIVIDENDS, options[index]); From 9aea06ba5df032425e6d75e34ba415dd5eb9e4bd Mon Sep 17 00:00:00 2001 From: Adam Dossa Date: Fri, 21 Dec 2018 17:13:13 +0000 Subject: [PATCH 11/12] Add getCheckpointData --- .../modules/Checkpoint/DividendCheckpoint.sol | 19 ++++++++++++++++- test/e_erc20_dividends.js | 21 +++++++++++++------ 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/contracts/modules/Checkpoint/DividendCheckpoint.sol b/contracts/modules/Checkpoint/DividendCheckpoint.sol index 4aa939c20..8f8c75e2d 100644 --- a/contracts/modules/Checkpoint/DividendCheckpoint.sol +++ b/contracts/modules/Checkpoint/DividendCheckpoint.sol @@ -317,7 +317,24 @@ contract DividendCheckpoint is DividendCheckpointStorage, ICheckpoint, Module { resultAmount[i] = resultBalance[i].mul(dividend.amount).div(dividend.totalSupply); } } - /* return (investors, resultClaimed, resultExcluded, resultWithheld, resultBalance, resultAmount); */ + } + + /** + * @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]]; + } } /** diff --git a/test/e_erc20_dividends.js b/test/e_erc20_dividends.js index bc52e3886..a13047fc0 100644 --- a/test/e_erc20_dividends.js +++ b/test/e_erc20_dividends.js @@ -854,11 +854,6 @@ contract("ERC20DividendCheckpoint", accounts => { assert.equal(info[3][2].toNumber(), web3.utils.toWei("0.2", "ether"), "withheld match"); assert.equal(info[3][3].toNumber(), 0, "withheld match"); - console.log(info[4][0].toNumber()); - console.log(info[4][1].toNumber()); - console.log(info[4][2].toNumber()); - console.log(info[4][3].toNumber()); - assert.equal(info[4][0].toNumber(), (await I_SecurityToken.balanceOfAt(account_investor1, 4)).toNumber(), "balance match"); assert.equal(info[4][1].toNumber(), (await I_SecurityToken.balanceOfAt(account_investor2, 4)).toNumber(), "balance match"); assert.equal(info[4][2].toNumber(), (await I_SecurityToken.balanceOfAt(account_temp, 4)).toNumber(), "balance match"); @@ -1102,7 +1097,21 @@ contract("ERC20DividendCheckpoint", 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].toNumber(), (await I_SecurityToken.balanceOfAt.call(account_investor1, checkpointID)).toNumber(), "balance match"); + assert.equal(info[1][1].toNumber(), (await I_SecurityToken.balanceOfAt.call(account_investor2, checkpointID)).toNumber(), "balance match"); + assert.equal(info[1][2].toNumber(), (await I_SecurityToken.balanceOfAt.call(account_temp, checkpointID)).toNumber(), "balance match"); + assert.equal(info[1][3].toNumber(), (await I_SecurityToken.balanceOfAt.call(account_investor3, checkpointID)).toNumber(), "balance match"); + assert.equal(info[2][0].toNumber(), 0, "withholding match"); + assert.equal(info[2][1].toNumber(), BigNumber(10 * 10 ** 16).toNumber(), "withholding match"); + assert.equal(info[2][2].toNumber(), BigNumber(20 * 10 ** 16).toNumber(), "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 () => { From fef44d0ad18f9592bc8a3076f193ec44cca801d6 Mon Sep 17 00:00:00 2001 From: Victor Date: Fri, 21 Dec 2018 16:25:49 -0300 Subject: [PATCH 12/12] CLI updates --- CLI/commands/dividends_manager.js | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/CLI/commands/dividends_manager.js b/CLI/commands/dividends_manager.js index f65074f88..15b624953 100644 --- a/CLI/commands/dividends_manager.js +++ b/CLI/commands/dividends_manager.js @@ -39,7 +39,6 @@ async function executeApp() { } let options = ['Create checkpoint', 'Explore address balances']; - if (nonArchivedModules.length > 0) { options.push('Config existing modules'); } @@ -126,11 +125,15 @@ async function dividendsManager() { let currentDividends = await getDividends(); let defaultExcluded = await currentDividendsModule.methods.getDefaultExcluded().call(); + let currentCheckpointId = await securityToken.methods.currentCheckpointId().call(); console.log(`- Current dividends: ${currentDividends.length}`); console.log(`- Default exclusions: ${defaultExcluded.length}`); let options = ['Create checkpoint']; + if (currentCheckpointId > 0) { + options.push('Explore checkpoint'); + } if (defaultExcluded.length > 0) { options.push('Show current default exclusions'); } @@ -150,6 +153,8 @@ async function dividendsManager() { case 'Create checkpoint': await createCheckpointFromDividendModule(); break; + case 'Explore checkpoint': + await exploreCheckpoint(); case 'Show current default exclusions': showExcluded(defaultExcluded); break; @@ -181,6 +186,22 @@ async function createCheckpointFromDividendModule() { console.log(chalk.green(`Checkpoint have been created successfully!`)); } +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() { 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?`)) { @@ -432,11 +453,11 @@ function showReport(_name, _tokenSymbol, _amount, _witthheld, _claimed, _investo for (let i = 0; i < _investorArray.length; i++) { let investor = _investorArray[i]; let excluded = _excludedArray[i]; - let amount = !excluded ? web3.utils.fromWei(_amountArray[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(web3.utils.toBN(_amountArray[i]).sub(web3.utils.toBN(_withheldArray[i]))) : 0; let withdrawn = _claimedArray[i] ? 'YES' : 'NO'; + let amount = !excluded ? web3.utils.fromWei(_amountArray[i]) : 0; + let withheld = (!excluded && _claimedArray[i]) ? web3.utils.fromWei(_withheldArray[i]) : 'NA'; + let withheldPercentage = (!excluded && _claimedArray[i]) ? (withheld !== '0' ? parseFloat(withheld) / parseFloat(amount) * 100 : 0) : 'NA'; + let received = (!excluded && _claimedArray[i]) ? web3.utils.fromWei(web3.utils.toBN(_amountArray[i]).sub(web3.utils.toBN(_withheldArray[i]))) : 0; dataTable.push([ investor, amount,