diff --git a/.travis.yml b/.travis.yml index be99e251f..ca293679d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,10 +6,15 @@ cache: - node_modules matrix: fast_finish: true +before_install: + - echo -ne '\n' | sudo add-apt-repository ppa:ethereum/ethereum + - sudo apt-get -y update + - sudo apt-get -y install solc before_script: - truffle version script: - npm run test + - npm run docs notifications: slack: secure: W4FZSabLrzF74f317hutolEHnlq2GBlQxU6b85L5XymrjgLEhlgE16c5Qz7Emoyt6le6PXL+sfG2ujJc3XYys/6hppgrHSAasuJnKCdQNpmMZ9BNyMs6WGkmB3enIf3K/FLXb26AQdwpQdIXuOeJUTf879u+YoiZV0eZH8d3+fsIOyovq9N6X5pKOpDM9iT8gGB4t7fie7xf51s+iUaHxyO9G7jDginZ4rBXHcU7mxCub9z+Z1H8+kCTnPWaF+KKVEXx4Z0nI3+urboD7E4OIP02LwrThQls2CppA3X0EoesTcdvj/HLErY/JvsXIFiFEEHZzB1Wi+k2TiOeLcYwEuHIVij+HPxxlJNX/j8uy01Uk8s4rd+0EhvfdKHJqUKqxH4YN2npcKfHEss7bU3y7dUinXQfYShW5ZewHdvc7pnnxBTfhvmdi64HdNrXAPq+s1rhciH7MmnU+tsm4lhrpr+FBuHzUMA9fOCr7b0SQytZEgWpiUls88gdbh3yG8TjyZxmZJGx09cwEP0q7VoH0UwFh7mIu5XmYdd5tWUhavTiO7YV8cUPn7MvwMsTltB3YBpF/fB26L7ka8zBhCsjm9prW6SVYU/dyO3m91VeZtO/zJFHRDA6Q58JGVW2rgzO39z193qC1EGRXqTie96VwAAtNg8+hRb+bI/CWDVzSPc= \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 82175d71f..e9f5ed668 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. [__1.5.0__](https://www.npmjs.com/package/polymath-core?activeTab=readme) __15-08-18__ ## Added +* Added withholding tax to ether & erc20 dividends * Generalised MakerDAO oracle to allow different instances referencing different currencies * Added DAI as a fundraising currency to USDTieredSTO * `transferTickerOwnership()` function is introduced in `TickerRegistry` to transfer the ticker ownership after the registeration #191. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..3dadf0761 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,46 @@ +# Contributing Guidelines + +## Do you have a question? + +[Check out our Gitter](https://gitter.im/PolymathNetwork/Lobby) + +See also frequently asked questions tagged with Polymath on Stack Exchange and Reddit. + +# Please Report bugs! + +Do not open an issue on Github if you think your discovered bug could be a security-relevant vulnerability. In the case of the discovery of high security venerabilities, pleasse email us the issue privately at team@polymath.network. + +Otherwise, just create a new issue in our repository and follow the template below: + +[Issue template to make things easier for you](https://github.com/PolymathNetwork/polymath-core/blob/master/.github/ISSUE_TEMPLATE/feature_request.md) + + +# Contribute! + +If you would like to contribute to Polymath-core, please fork it, fix bugs or implement features, and propose a [pull request](https://github.com/PolymathNetwork/polymath-core/blob/master/PULL_REQUEST_TEMPLATE.md) + +Please, refer to the Coding Guide on our [Developer Portal](https://developers.polymath.network/) for more details about hacking on Polymath. + +# Contributing + +When contributing to this repository, please first discuss the change you wish to make via issue, email, or any other method with the owners of this repository before making a change. + +Please note we have a code of conduct, please follow it in all your interactions with the project. + +# Pull Request Process + +[Template](https://github.com/PolymathNetwork/polymath-core/blob/master/PULL_REQUEST_TEMPLATE.md) + +# Code Styleguide + +The polymath-core repo follows the [Solidity style guide](https://solidity.readthedocs.io/en/v0.4.24/style-guide.html) + +# Code of Conduct + +[Community Standards](https://github.com/PolymathNetwork/polymath-core/blob/master/CODE_OF_CONDUCT.md) + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community feel safe and respected. + +# Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. diff --git a/contracts/modules/Checkpoint/DividendCheckpoint.sol b/contracts/modules/Checkpoint/DividendCheckpoint.sol new file mode 100644 index 000000000..e427509ab --- /dev/null +++ b/contracts/modules/Checkpoint/DividendCheckpoint.sol @@ -0,0 +1,214 @@ +pragma solidity ^0.4.24; + +import "./ICheckpoint.sol"; +import "../Module.sol"; +import "../../interfaces/ISecurityToken.sol"; +import "openzeppelin-solidity/contracts/math/SafeMath.sol"; +import "openzeppelin-solidity/contracts/math/Math.sol"; + +/** + * @title Checkpoint module for issuing ether dividends + * @dev abstract contract + */ +contract DividendCheckpoint is ICheckpoint, Module { + using SafeMath for uint256; + + uint256 public EXCLUDED_ADDRESS_LIMIT = 50; + bytes32 public constant DISTRIBUTE = "DISTRIBUTE"; + + struct Dividend { + uint256 checkpointId; + uint256 created; // Time at which the dividend was created + uint256 maturity; // Time after which dividend can be claimed - set to 0 to bypass + uint256 expiry; // Time until which dividend can be claimed - after this time any remaining amount can be withdrawn by issuer - set to very high value to bypass + uint256 amount; // Dividend amount in WEI + uint256 claimedAmount; // Amount of dividend claimed so far + uint256 totalSupply; // Total supply at the associated checkpoint (avoids recalculating this) + bool reclaimed; // True if expiry has passed and issuer has reclaimed remaining dividend + uint256 dividendWithheld; + uint256 dividendWithheldReclaimed; + mapping (address => bool) claimed; // List of addresses which have claimed dividend + mapping (address => bool) dividendExcluded; // List of addresses which cannot claim dividends + } + + // List of all dividends + Dividend[] public dividends; + + // List of addresses which cannot claim dividends + address[] public excluded; + + // 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; + + event SetExcludedAddresses(address[] _excluded, uint256 _timestamp); + + modifier validDividendIndex(uint256 _dividendIndex) { + require(_dividendIndex < dividends.length, "Incorrect dividend index"); + require(now >= dividends[_dividendIndex].maturity, "Dividend maturity is in the future"); + require(now < dividends[_dividendIndex].expiry, "Dividend expiry is in the past"); + require(!dividends[_dividendIndex].reclaimed, "Dividend has been reclaimed by issuer"); + _; + } + + /** + * @notice Init function i.e generalise function to maintain the structure of the module contract + * @return bytes4 + */ + function getInitFunction() public pure returns (bytes4) { + return bytes4(0); + } + + /** + * @notice Function to set withholding tax rates for investors + * @param _investors addresses of investor + * @param _withholding withholding tax for individual investors (multiplied by 10**16) + */ + function setWithholding(address[] _investors, uint256[] _withholding) public onlyOwner { + require(_investors.length == _withholding.length, "Mismatched input lengths"); + for (uint256 i = 0; i < _investors.length; i++) { + require(_withholding[i] <= 10**18); + withholdingTax[_investors[i]] = _withholding[i]; + } + } + + /** + * @notice Function to clear and set list of excluded addresses used for future dividends + * @param _excluded addresses of investor + */ + function setExcluded(address[] _excluded) public onlyOwner { + require(_excluded.length <= EXCLUDED_ADDRESS_LIMIT, "Too many excluded addresses"); + excluded = _excluded; + emit SetExcludedAddresses(excluded, now); + } + + /** + * @notice Function to set withholding tax rates for investors + * @param _investors addresses of investor + * @param _withholding withholding tax for all investors (multiplied by 10**16) + */ + function setWithholdingFixed(address[] _investors, uint256 _withholding) public onlyOwner { + require(_withholding <= 10**18); + for (uint256 i = 0; i < _investors.length; i++) { + withholdingTax[_investors[i]] = _withholding; + } + } + + /** + * @notice Issuer can push dividends to provided addresses + * @param _dividendIndex Dividend to push + * @param _payees Addresses to which to push the dividend + */ + function pushDividendPaymentToAddresses(uint256 _dividendIndex, address[] _payees) public withPerm(DISTRIBUTE) validDividendIndex(_dividendIndex) { + Dividend storage dividend = dividends[_dividendIndex]; + for (uint256 i = 0; i < _payees.length; i++) { + if ((!dividend.claimed[_payees[i]]) && (!dividend.dividendExcluded[_payees[i]])) { + _payDividend(_payees[i], dividend, _dividendIndex); + } + } + } + + /** + * @notice Issuer can push dividends using the investor list from the security token + * @param _dividendIndex Dividend to push + * @param _start Index in investor list at which to start pushing dividends + * @param _iterations Number of addresses to push dividends for + */ + function pushDividendPayment(uint256 _dividendIndex, uint256 _start, uint256 _iterations) public withPerm(DISTRIBUTE) validDividendIndex(_dividendIndex) { + Dividend storage dividend = dividends[_dividendIndex]; + uint256 numberInvestors = ISecurityToken(securityToken).getInvestorsLength(); + for (uint256 i = _start; i < Math.min256(numberInvestors, _start.add(_iterations)); i++) { + address payee = ISecurityToken(securityToken).investors(i); + if ((!dividend.claimed[payee]) && (!dividend.dividendExcluded[payee])) { + _payDividend(payee, dividend, _dividendIndex); + } + } + } + + /** + * @notice Investors can pull their own dividends + * @param _dividendIndex Dividend to pull + */ + function pullDividendPayment(uint256 _dividendIndex) public validDividendIndex(_dividendIndex) + { + Dividend storage dividend = dividends[_dividendIndex]; + require(!dividend.claimed[msg.sender], "Dividend already claimed by msg.sender"); + require(!dividend.dividendExcluded[msg.sender], "msg.sender excluded from Dividend"); + _payDividend(msg.sender, dividend, _dividendIndex); + } + + /** + * @notice Internal function for paying dividends + * @param _payee address of investor + * @param _dividend storage with previously issued dividends + * @param _dividendIndex Dividend to pay + */ + function _payDividend(address _payee, Dividend storage _dividend, uint256 _dividendIndex) internal; + + /** + * @notice Issuer can reclaim remaining unclaimed dividend amounts, for expired dividends + * @param _dividendIndex Dividend to reclaim + */ + function reclaimDividend(uint256 _dividendIndex) external; + + /** + * @notice Calculate amount of dividends claimable + * @param _dividendIndex Dividend to calculate + * @param _payee Affected investor address + * @return unit256 + */ + function calculateDividend(uint256 _dividendIndex, address _payee) public view returns(uint256, uint256) { + require(_dividendIndex < dividends.length, "Incorrect dividend index"); + Dividend storage dividend = dividends[_dividendIndex]; + if (dividend.claimed[_payee] || dividend.dividendExcluded[_payee]) { + return (0, 0); + } + uint256 balance = ISecurityToken(securityToken).balanceOfAt(_payee, dividend.checkpointId); + uint256 claim = balance.mul(dividend.amount).div(dividend.totalSupply); + uint256 withheld = claim.mul(withholdingTax[_payee]).div(uint256(10**18)); + return (claim, withheld); + } + + /** + * @notice Get the index according to the checkpoint id + * @param _checkpointId Checkpoint id to query + * @return uint256[] + */ + function getDividendIndex(uint256 _checkpointId) public view returns(uint256[]) { + uint256 counter = 0; + for(uint256 i = 0; i < dividends.length; i++) { + if (dividends[i].checkpointId == _checkpointId) { + counter++; + } + } + + uint256[] memory index = new uint256[](counter); + counter = 0; + for(uint256 j = 0; j < dividends.length; j++) { + if (dividends[j].checkpointId == _checkpointId) { + index[counter] = j; + counter++; + } + } + return index; + } + + /** + * @notice Allows issuer to withdraw withheld tax + * @param _dividendIndex Dividend to withdraw from + */ + function withdrawWithholding(uint256 _dividendIndex) external; + + /** + * @notice Return the permissions flag that are associated with STO + * @return bytes32 array + */ + function getPermissions() public view returns(bytes32[]) { + bytes32[] memory allPermissions = new bytes32[](1); + allPermissions[0] = DISTRIBUTE; + return allPermissions; + } + +} diff --git a/contracts/modules/Checkpoint/ERC20DividendCheckpoint.sol b/contracts/modules/Checkpoint/ERC20DividendCheckpoint.sol index e3bcdf226..ad61b24a8 100644 --- a/contracts/modules/Checkpoint/ERC20DividendCheckpoint.sol +++ b/contracts/modules/Checkpoint/ERC20DividendCheckpoint.sol @@ -1,47 +1,21 @@ pragma solidity ^0.4.24; -import "./ICheckpoint.sol"; -import "../Module.sol"; -import "../../interfaces/ISecurityToken.sol"; -import "openzeppelin-solidity/contracts/math/SafeMath.sol"; -import "openzeppelin-solidity/contracts/math/Math.sol"; +import "./DividendCheckpoint.sol"; import "../../interfaces/IERC20.sol"; /** * @title Checkpoint module for issuing ERC20 dividends */ -contract ERC20DividendCheckpoint is ICheckpoint, Module { +contract ERC20DividendCheckpoint is DividendCheckpoint { using SafeMath for uint256; - bytes32 public constant DISTRIBUTE = "DISTRIBUTE"; + // Mapping to token address for each dividend + mapping (uint256 => address) public dividendTokens; - struct Dividend { - uint256 checkpointId; - uint256 created; // Time at which the dividend was created - uint256 maturity; // Time after which dividend can be claimed - set to 0 to bypass - uint256 expiry; // Time until which dividend can be claimed - after this time any remaining amount can be withdrawn by issuer - set to very high value to bypass - address token; // Address of ERC20 token that dividend is denominated in - uint256 amount; // Dividend amount in base token amounts - uint256 claimedAmount; // Amount of dividend claimed so far - uint256 totalSupply; // Total supply at the associated checkpoint (avoids recalculating this) - bool reclaimed; - mapping (address => bool) claimed; // List of addresses which have claimed dividend - } - - // List of all dividends - Dividend[] public dividends; - - event ERC20DividendDeposited(address indexed _depositor, uint256 _checkpointId, uint256 _created, uint256 _maturity, uint256 _expiry, address _token, uint256 _amount, uint256 _totalSupply, uint256 _dividendIndex); - event ERC20DividendClaimed(address indexed _payee, uint256 _dividendIndex, address _token, uint256 _amount); - event ERC20DividendReclaimed(address indexed _claimer, uint256 _dividendIndex, address _token, uint256 _claimedAmount); - - modifier validDividendIndex(uint256 _dividendIndex) { - require(_dividendIndex < dividends.length, "Incorrect dividend index"); - require(now >= dividends[_dividendIndex].maturity, "Dividend maturity is in the future"); - require(now < dividends[_dividendIndex].expiry, "Dividend expiry is in the past"); - require(!dividends[_dividendIndex].reclaimed, "Dividend has been reclaimed by issuer"); - _; - } + event ERC20DividendDeposited(address indexed _depositor, uint256 _checkpointId, uint256 _created, uint256 _maturity, uint256 _expiry, address indexed _token, uint256 _amount, uint256 _totalSupply, uint256 _dividendIndex); + event ERC20DividendClaimed(address indexed _payee, uint256 _dividendIndex, address indexed _token, uint256 _amount, uint256 _withheld); + event ERC20DividendReclaimed(address indexed _claimer, uint256 _dividendIndex, address indexed _token, uint256 _claimedAmount); + event ERC20DividendWithholdingWithdrawn(address indexed _claimer, uint256 _dividendIndex, address indexed _token, uint256 _withheldAmount); /** * @notice Constructor @@ -54,11 +28,26 @@ contract ERC20DividendCheckpoint is ICheckpoint, Module { } /** - * @notice Init function i.e generalise function to maintain the structure of the module contract - * @return bytes4 - */ - function getInitFunction() public pure returns (bytes4) { - return bytes4(0); + * @notice Creates a dividend and checkpoint for the dividend + * @param _maturity Time from which dividend can be paid + * @param _expiry Time until dividend can no longer be paid, and can be reclaimed by issuer + * @param _token Address of ERC20 token in which dividend is to be denominated + * @param _amount Amount of specified token for dividend + */ + function createDividend(uint256 _maturity, uint256 _expiry, address _token, uint256 _amount) external onlyOwner { + createDividendWithExclusions(_maturity, _expiry, _token, _amount, excluded); + } + + /** + * @notice Creates a dividend with a provided checkpoint + * @param _maturity Time from which dividend can be paid + * @param _expiry Time until dividend can no longer be paid, and can be reclaimed by issuer + * @param _token Address of ERC20 token in which dividend is to be denominated + * @param _amount Amount of specified token for dividend + * @param _checkpointId Checkpoint id from which to create dividends + */ + function createDividendWithCheckpoint(uint256 _maturity, uint256 _expiry, address _token, uint256 _amount, uint256 _checkpointId) external onlyOwner { + createDividendWithCheckpointAndExclusions(_maturity, _expiry, _token, _amount, _checkpointId, excluded); } /** @@ -67,30 +56,11 @@ contract ERC20DividendCheckpoint is ICheckpoint, Module { * @param _expiry Time until dividend can no longer be paid, and can be reclaimed by issuer * @param _token Address of ERC20 token in which dividend is to be denominated * @param _amount Amount of specified token for dividend + * @param _excluded List of addresses to exclude */ - function createDividend(uint256 _maturity, uint256 _expiry, address _token, uint256 _amount) public onlyOwner { - require(_expiry > _maturity); - require(_expiry > now); - require(_token != address(0)); - require(_amount > 0); - require(IERC20(_token).transferFrom(msg.sender, address(this), _amount), "Unable to transfer tokens for dividend"); - uint256 dividendIndex = dividends.length; + function createDividendWithExclusions(uint256 _maturity, uint256 _expiry, address _token, uint256 _amount, address[] _excluded) public onlyOwner { uint256 checkpointId = ISecurityToken(securityToken).createCheckpoint(); - uint256 currentSupply = ISecurityToken(securityToken).totalSupply(); - dividends.push( - Dividend( - checkpointId, - now, - _maturity, - _expiry, - _token, - _amount, - 0, - currentSupply, - false - ) - ); - emit ERC20DividendDeposited(msg.sender, checkpointId, now, _maturity, _expiry, _token, _amount, currentSupply, dividendIndex); + createDividendWithCheckpointAndExclusions(_maturity, _expiry, _token, _amount, checkpointId, _excluded); } /** @@ -100,70 +70,41 @@ contract ERC20DividendCheckpoint is ICheckpoint, Module { * @param _token Address of ERC20 token in which dividend is to be denominated * @param _amount Amount of specified token for dividend * @param _checkpointId Checkpoint id from which to create dividends + * @param _excluded List of addresses to exclude */ - function createDividendWithCheckpoint(uint256 _maturity, uint256 _expiry, address _token, uint256 _amount, uint256 _checkpointId) payable public onlyOwner { - require(_expiry > _maturity); - require(_expiry > now); - require(_checkpointId <= ISecurityToken(securityToken).currentCheckpointId()); + function createDividendWithCheckpointAndExclusions(uint256 _maturity, uint256 _expiry, address _token, uint256 _amount, uint256 _checkpointId, address[] _excluded) public onlyOwner { + require(_excluded.length <= EXCLUDED_ADDRESS_LIMIT, "Too many addresses excluded"); + require(_expiry > _maturity, "Expiry is before maturity"); + require(_expiry > now, "Expiry is in the past"); + require(_amount > 0, "No dividend sent"); + require(_token != address(0), "0x not valid token"); + require(_checkpointId <= ISecurityToken(securityToken).currentCheckpointId(), "Invalid checkpoint"); + require(IERC20(_token).transferFrom(msg.sender, address(this), _amount), "Unable to transfer tokens for dividend"); uint256 dividendIndex = dividends.length; uint256 currentSupply = ISecurityToken(securityToken).totalSupplyAt(_checkpointId); - require(IERC20(_token).transferFrom(msg.sender, address(this), _amount), "Unable to transfer tokens for dividend"); + uint256 excludedSupply = 0; + for (uint256 i = 0; i < _excluded.length; i++) { + excludedSupply = excludedSupply.add(ISecurityToken(securityToken).balanceOfAt(_excluded[i], _checkpointId)); + } dividends.push( Dividend( _checkpointId, now, _maturity, _expiry, - _token, _amount, 0, - currentSupply, - false + currentSupply.sub(excludedSupply), + false, + 0, + 0 ) ); - emit ERC20DividendDeposited(msg.sender, _checkpointId, now, _maturity, _expiry, _token, _amount, currentSupply, dividendIndex); - } - - /** - * @notice Issuer can push dividends to provided addresses - * @param _dividendIndex Dividend to push - * @param _payees Addresses to which to push the dividend - */ - function pushDividendPaymentToAddresses(uint256 _dividendIndex, address[] _payees) public withPerm(DISTRIBUTE) validDividendIndex(_dividendIndex) { - Dividend storage dividend = dividends[_dividendIndex]; - for (uint256 i = 0; i < _payees.length; i++) { - if (!dividend.claimed[_payees[i]]) { - _payDividend(_payees[i], dividend, _dividendIndex); - } + for (uint256 j = 0; j < _excluded.length; j++) { + dividends[dividends.length - 1].dividendExcluded[_excluded[j]] = true; } - } - - /** - * @notice Issuer can push dividends using the investor list from the security token - * @param _dividendIndex Dividend to push - * @param _start Index in investor list at which to start pushing dividends - * @param _iterations Number of addresses to push dividends for - */ - function pushDividendPayment(uint256 _dividendIndex, uint256 _start, uint256 _iterations) public withPerm(DISTRIBUTE) validDividendIndex(_dividendIndex) { - Dividend storage dividend = dividends[_dividendIndex]; - uint256 numberInvestors = ISecurityToken(securityToken).getInvestorsLength(); - for (uint256 i = _start; i < Math.min256(numberInvestors, _start.add(_iterations)); i++) { - address payee = ISecurityToken(securityToken).investors(i); - if (!dividend.claimed[payee]) { - _payDividend(payee, dividend, _dividendIndex); - } - } - } - - /** - * @notice Investors can pull their own dividends - * @param _dividendIndex Dividend to pull - */ - function pullDividendPayment(uint256 _dividendIndex) public validDividendIndex(_dividendIndex) - { - Dividend storage dividend = dividends[_dividendIndex]; - require(!dividend.claimed[msg.sender], "Dividend already reclaimed"); - _payDividend(msg.sender, dividend, _dividendIndex); + dividendTokens[dividendIndex] = _token; + emit ERC20DividendDeposited(msg.sender, _checkpointId, now, _maturity, _expiry, _token, _amount, currentSupply, dividendIndex); } /** @@ -173,12 +114,15 @@ contract ERC20DividendCheckpoint is ICheckpoint, Module { * @param _dividendIndex Dividend to pay */ function _payDividend(address _payee, Dividend storage _dividend, uint256 _dividendIndex) internal { - uint256 claim = calculateDividend(_dividendIndex, _payee); + (uint256 claim, uint256 withheld) = calculateDividend(_dividendIndex, _payee); _dividend.claimed[_payee] = true; _dividend.claimedAmount = claim.add(_dividend.claimedAmount); - if (claim > 0) { - require(IERC20(_dividend.token).transfer(_payee, claim), "Unable to transfer tokens"); - emit ERC20DividendClaimed(_payee, _dividendIndex, _dividend.token, claim); + uint256 claimAfterWithheld = claim.sub(withheld); + if (claimAfterWithheld > 0) { + require(IERC20(dividendTokens[_dividendIndex]).transfer(_payee, claimAfterWithheld), "Unable to transfer tokens"); + _dividend.dividendWithheld = _dividend.dividendWithheld.add(withheld); + investorWithheld[_payee] = investorWithheld[_payee].add(withheld); + emit ERC20DividendClaimed(_payee, _dividendIndex, dividendTokens[_dividendIndex], claim, withheld); } } @@ -186,64 +130,28 @@ contract ERC20DividendCheckpoint is ICheckpoint, Module { * @notice Issuer can reclaim remaining unclaimed dividend amounts, for expired dividends * @param _dividendIndex Dividend to reclaim */ - function reclaimDividend(uint256 _dividendIndex) public onlyOwner { + function reclaimDividend(uint256 _dividendIndex) external onlyOwner { require(_dividendIndex < dividends.length, "Incorrect dividend index"); require(now >= dividends[_dividendIndex].expiry, "Dividend expiry is in the future"); require(!dividends[_dividendIndex].reclaimed, "Dividend already claimed"); dividends[_dividendIndex].reclaimed = true; Dividend storage dividend = dividends[_dividendIndex]; uint256 remainingAmount = dividend.amount.sub(dividend.claimedAmount); - require(IERC20(dividend.token).transfer(msg.sender, remainingAmount), "Unable to transfer tokens"); - emit ERC20DividendReclaimed(msg.sender, _dividendIndex, dividend.token, remainingAmount); + require(IERC20(dividendTokens[_dividendIndex]).transfer(msg.sender, remainingAmount), "Unable to transfer tokens"); + emit ERC20DividendReclaimed(msg.sender, _dividendIndex, dividendTokens[_dividendIndex], remainingAmount); } /** - * @notice Calculate amount of dividends claimable - * @param _dividendIndex Dividend to calculate - * @param _payee Affected investor address - * @return unit256 + * @notice Allows issuer to withdraw withheld tax + * @param _dividendIndex Dividend to withdraw from */ - function calculateDividend(uint256 _dividendIndex, address _payee) public view returns(uint256) { + function withdrawWithholding(uint256 _dividendIndex) external onlyOwner { + require(_dividendIndex < dividends.length, "Incorrect dividend index"); Dividend storage dividend = dividends[_dividendIndex]; - if (dividend.claimed[_payee]) { - return 0; - } - uint256 balance = ISecurityToken(securityToken).balanceOfAt(_payee, dividends[_dividendIndex].checkpointId); - return balance.mul(dividends[_dividendIndex].amount).div(dividends[_dividendIndex].totalSupply); - } - - /** - * @notice Get the index according to the checkpoint id - * @param _checkpointId Checkpoint id to query - * @return uint256 - */ - function getDividendIndex(uint256 _checkpointId) public view returns(uint256[]) { - uint256 counter = 0; - for(uint256 i = 0; i < dividends.length; i++) { - if (dividends[i].checkpointId == _checkpointId) { - counter++; - } - } - - uint256[] memory index = new uint256[](counter); - counter = 0; - for(uint256 j = 0; j < dividends.length; j++) { - if (dividends[j].checkpointId == _checkpointId) { - index[counter] = j; - counter++; - } - } - return index; - } - - /** - * @notice Return the permissions flag that are associated with STO - * @return bytes32 array - */ - function getPermissions() public view returns(bytes32[]) { - bytes32[] memory allPermissions = new bytes32[](1); - allPermissions[0] = DISTRIBUTE; - return allPermissions; + uint256 remainingWithheld = dividend.dividendWithheld.sub(dividend.dividendWithheldReclaimed); + dividend.dividendWithheldReclaimed = dividend.dividendWithheld; + require(IERC20(dividendTokens[_dividendIndex]).transfer(msg.sender, remainingWithheld), "Unable to transfer tokens"); + emit ERC20DividendWithholdingWithdrawn(msg.sender, _dividendIndex, dividendTokens[_dividendIndex], remainingWithheld); } } diff --git a/contracts/modules/Checkpoint/EtherDividendCheckpoint.sol b/contracts/modules/Checkpoint/EtherDividendCheckpoint.sol index d13994a47..1d17e1abb 100644 --- a/contracts/modules/Checkpoint/EtherDividendCheckpoint.sol +++ b/contracts/modules/Checkpoint/EtherDividendCheckpoint.sol @@ -1,46 +1,18 @@ pragma solidity ^0.4.24; -import "./ICheckpoint.sol"; -import "../Module.sol"; -import "../../interfaces/ISecurityToken.sol"; -import "openzeppelin-solidity/contracts/math/SafeMath.sol"; -import "openzeppelin-solidity/contracts/math/Math.sol"; +import "./DividendCheckpoint.sol"; /** * @title Checkpoint module for issuing ether dividends */ -contract EtherDividendCheckpoint is ICheckpoint, Module { +contract EtherDividendCheckpoint is DividendCheckpoint { using SafeMath for uint256; - bytes32 public constant DISTRIBUTE = "DISTRIBUTE"; - - struct Dividend { - uint256 checkpointId; - uint256 created; // Time at which the dividend was created - uint256 maturity; // Time after which dividend can be claimed - set to 0 to bypass - uint256 expiry; // Time until which dividend can be claimed - after this time any remaining amount can be withdrawn by issuer - set to very high value to bypass - uint256 amount; // Dividend amount in WEI - uint256 claimedAmount; // Amount of dividend claimed so far - uint256 totalSupply; // Total supply at the associated checkpoint (avoids recalculating this) - bool reclaimed; - mapping (address => bool) claimed; // List of addresses which have claimed dividend - } - - // List of all dividends - Dividend[] public dividends; - event EtherDividendDeposited(address indexed _depositor, uint256 _checkpointId, uint256 _created, uint256 _maturity, uint256 _expiry, uint256 _amount, uint256 _totalSupply, uint256 _dividendIndex); - event EtherDividendClaimed(address indexed _payee, uint256 _dividendIndex, uint256 _amount); + event EtherDividendClaimed(address indexed _payee, uint256 _dividendIndex, uint256 _amount, uint256 _withheld); event EtherDividendReclaimed(address indexed _claimer, uint256 _dividendIndex, uint256 _claimedAmount); - event EtherDividendClaimFailed(address indexed _payee, uint256 _dividendIndex, uint256 _amount); - - modifier validDividendIndex(uint256 _dividendIndex) { - require(_dividendIndex < dividends.length, "Incorrect dividend index"); - require(now >= dividends[_dividendIndex].maturity, "Dividend maturity is in the future"); - require(now < dividends[_dividendIndex].expiry, "Dividend expiry is in the past"); - require(!dividends[_dividendIndex].reclaimed, "Dividend has been reclaimed by issuer"); - _; - } + event EtherDividendClaimFailed(address indexed _payee, uint256 _dividendIndex, uint256 _amount, uint256 _withheld); + event EtherDividendWithholdingWithdrawn(address indexed _claimer, uint256 _dividendIndex, uint256 _withheldAmount); /** * @notice Constructor @@ -53,53 +25,54 @@ contract EtherDividendCheckpoint is ICheckpoint, Module { } /** - * @notice Init function i.e generalise function to maintain the structure of the module contract - * @return bytes4 - */ - function getInitFunction() public pure returns (bytes4) { - return bytes4(0); + * @notice Creates a dividend and checkpoint for the dividend, using global list of excluded addresses + * @param _maturity Time from which dividend can be paid + * @param _expiry Time until dividend can no longer be paid, and can be reclaimed by issuer + */ + function createDividend(uint256 _maturity, uint256 _expiry) payable external onlyOwner { + createDividendWithExclusions(_maturity, _expiry, excluded); } /** - * @notice Creates a dividend and checkpoint for the dividend + * @notice Creates a dividend with a provided checkpoint, using global list of excluded addresses * @param _maturity Time from which dividend can be paid * @param _expiry Time until dividend can no longer be paid, and can be reclaimed by issuer + * @param _checkpointId Id of the checkpoint from which to issue dividend */ - function createDividend(uint256 _maturity, uint256 _expiry) payable public onlyOwner { - require(_expiry > _maturity); - require(_expiry > now); - require(msg.value > 0); - uint256 dividendIndex = dividends.length; + function createDividendWithCheckpoint(uint256 _maturity, uint256 _expiry, uint256 _checkpointId) payable external onlyOwner { + createDividendWithCheckpointAndExclusions(_maturity, _expiry, _checkpointId, excluded); + } + + /** + * @notice Creates a dividend and checkpoint for the dividend, specifying explicit excluded addresses + * @param _maturity Time from which dividend can be paid + * @param _expiry Time until dividend can no longer be paid, and can be reclaimed by issuer + * @param _excluded List of addresses to exclude + */ + function createDividendWithExclusions(uint256 _maturity, uint256 _expiry, address[] _excluded) payable public onlyOwner { uint256 checkpointId = ISecurityToken(securityToken).createCheckpoint(); - uint256 currentSupply = ISecurityToken(securityToken).totalSupply(); - dividends.push( - Dividend( - checkpointId, - now, - _maturity, - _expiry, - msg.value, - 0, - currentSupply, - false - ) - ); - emit EtherDividendDeposited(msg.sender, checkpointId, now, _maturity, _expiry, msg.value, currentSupply, dividendIndex); + createDividendWithCheckpointAndExclusions(_maturity, _expiry, checkpointId, _excluded); } /** - * @notice Creates a dividend with a provided checkpoint + * @notice Creates a dividend with a provided checkpoint, specifying explicit excluded addresses * @param _maturity Time from which dividend can be paid * @param _expiry Time until dividend can no longer be paid, and can be reclaimed by issuer * @param _checkpointId Id of the checkpoint from which to issue dividend + * @param _excluded List of addresses to exclude */ - function createDividendWithCheckpoint(uint256 _maturity, uint256 _expiry, uint256 _checkpointId) payable public onlyOwner { - require(_expiry > _maturity); - require(_expiry > now); - require(msg.value > 0); + function createDividendWithCheckpointAndExclusions(uint256 _maturity, uint256 _expiry, uint256 _checkpointId, address[] _excluded) payable public onlyOwner { + require(_excluded.length <= EXCLUDED_ADDRESS_LIMIT, "Too many addresses excluded"); + require(_expiry > _maturity, "Expiry is before maturity"); + require(_expiry > now, "Expiry is in the past"); + require(msg.value > 0, "No dividend sent"); require(_checkpointId <= ISecurityToken(securityToken).currentCheckpointId()); uint256 dividendIndex = dividends.length; uint256 currentSupply = ISecurityToken(securityToken).totalSupplyAt(_checkpointId); + uint256 excludedSupply = 0; + for (uint256 i = 0; i < _excluded.length; i++) { + excludedSupply = excludedSupply.add(ISecurityToken(securityToken).balanceOfAt(_excluded[i], _checkpointId)); + } dividends.push( Dividend( _checkpointId, @@ -108,53 +81,16 @@ contract EtherDividendCheckpoint is ICheckpoint, Module { _expiry, msg.value, 0, - currentSupply, - false + currentSupply.sub(excludedSupply), + false, + 0, + 0 ) ); - emit EtherDividendDeposited(msg.sender, _checkpointId, now, _maturity, _expiry, msg.value, currentSupply, dividendIndex); - } - - /** - * @notice Issuer can push dividends to provided addresses - * @param _dividendIndex Dividend to push - * @param _payees Addresses to which to push the dividend - */ - function pushDividendPaymentToAddresses(uint256 _dividendIndex, address[] _payees) public withPerm(DISTRIBUTE) validDividendIndex(_dividendIndex) { - Dividend storage dividend = dividends[_dividendIndex]; - for (uint256 i = 0; i < _payees.length; i++) { - if (!dividend.claimed[_payees[i]]) { - _payDividend(_payees[i], dividend, _dividendIndex); - } - } - } - - /** - * @notice Issuer can push dividends using the investor list from the security token - * @param _dividendIndex Dividend to push - * @param _start Index in investor list at which to start pushing dividends - * @param _iterations Number of addresses to push dividends for - */ - function pushDividendPayment(uint256 _dividendIndex, uint256 _start, uint256 _iterations) public withPerm(DISTRIBUTE) validDividendIndex(_dividendIndex) { - Dividend storage dividend = dividends[_dividendIndex]; - uint256 numberInvestors = ISecurityToken(securityToken).getInvestorsLength(); - for (uint256 i = _start; i < Math.min256(numberInvestors, _start.add(_iterations)); i++) { - address payee = ISecurityToken(securityToken).investors(i); - if (!dividend.claimed[payee]) { - _payDividend(payee, dividend, _dividendIndex); - } + for (uint256 j = 0; j < _excluded.length; j++) { + dividends[dividends.length - 1].dividendExcluded[_excluded[j]] = true; } - } - - /** - * @notice Investors can pull their own dividends - * @param _dividendIndex Dividend to pull - */ - function pullDividendPayment(uint256 _dividendIndex) public validDividendIndex(_dividendIndex) - { - Dividend storage dividend = dividends[_dividendIndex]; - require(!dividend.claimed[msg.sender], "Dividend already reclaimed"); - _payDividend(msg.sender, dividend, _dividendIndex); + emit EtherDividendDeposited(msg.sender, _checkpointId, now, _maturity, _expiry, msg.value, currentSupply, dividendIndex); } /** @@ -164,15 +100,18 @@ contract EtherDividendCheckpoint is ICheckpoint, Module { * @param _dividendIndex Dividend to pay */ function _payDividend(address _payee, Dividend storage _dividend, uint256 _dividendIndex) internal { - uint256 claim = calculateDividend(_dividendIndex, _payee); - _dividend.claimed[_payee] = true; - _dividend.claimedAmount = claim.add(_dividend.claimedAmount); - if (claim > 0) { - if (_payee.send(claim)) { - emit EtherDividendClaimed(_payee, _dividendIndex, claim); + (uint256 claim, uint256 withheld) = calculateDividend(_dividendIndex, _payee); + _dividend.claimed[_payee] = true; + uint256 claimAfterWithheld = claim.sub(withheld); + if (claimAfterWithheld > 0) { + if (_payee.send(claimAfterWithheld)) { + _dividend.claimedAmount = _dividend.claimedAmount.add(claim); + _dividend.dividendWithheld = _dividend.dividendWithheld.add(withheld); + investorWithheld[_payee] = investorWithheld[_payee].add(withheld); + emit EtherDividendClaimed(_payee, _dividendIndex, claim, withheld); } else { _dividend.claimed[_payee] = false; - emit EtherDividendClaimFailed(_payee, _dividendIndex, claim); + emit EtherDividendClaimFailed(_payee, _dividendIndex, claim, withheld); } } } @@ -181,7 +120,7 @@ contract EtherDividendCheckpoint is ICheckpoint, Module { * @notice Issuer can reclaim remaining unclaimed dividend amounts, for expired dividends * @param _dividendIndex Dividend to reclaim */ - function reclaimDividend(uint256 _dividendIndex) public onlyOwner { + function reclaimDividend(uint256 _dividendIndex) external onlyOwner { require(_dividendIndex < dividends.length, "Incorrect dividend index"); require(now >= dividends[_dividendIndex].expiry, "Dividend expiry is in the future"); require(!dividends[_dividendIndex].reclaimed, "Dividend already claimed"); @@ -193,52 +132,16 @@ contract EtherDividendCheckpoint is ICheckpoint, Module { } /** - * @notice Calculate amount of dividends claimable - * @param _dividendIndex Dividend to calculate - * @param _payee Affected investor address - * @return unit256 + * @notice Allows issuer to withdraw withheld tax + * @param _dividendIndex Dividend to withdraw from */ - function calculateDividend(uint256 _dividendIndex, address _payee) public view returns(uint256) { + function withdrawWithholding(uint256 _dividendIndex) external onlyOwner { + require(_dividendIndex < dividends.length, "Incorrect dividend index"); Dividend storage dividend = dividends[_dividendIndex]; - if (dividend.claimed[_payee]) { - return 0; - } - uint256 balance = ISecurityToken(securityToken).balanceOfAt(_payee, dividend.checkpointId); - return balance.mul(dividend.amount).div(dividend.totalSupply); - } - - /** - * @notice Get the index according to the checkpoint id - * @param _checkpointId Checkpoint id to query - * @return uint256 - */ - function getDividendIndex(uint256 _checkpointId) public view returns(uint256[]) { - uint256 counter = 0; - for(uint256 i = 0; i < dividends.length; i++) { - if (dividends[i].checkpointId == _checkpointId) { - counter++; - } - } - - uint256[] memory index = new uint256[](counter); - counter = 0; - for(uint256 j = 0; j < dividends.length; j++) { - if (dividends[j].checkpointId == _checkpointId) { - index[counter] = j; - counter++; - } - } - return index; - } - - /** - * @notice Return the permissions flag that are associated with STO - * @return bytes32 array - */ - function getPermissions() public view returns(bytes32[]) { - bytes32[] memory allPermissions = new bytes32[](1); - allPermissions[0] = DISTRIBUTE; - return allPermissions; + uint256 remainingWithheld = dividend.dividendWithheld.sub(dividend.dividendWithheldReclaimed); + dividend.dividendWithheldReclaimed = dividend.dividendWithheld; + msg.sender.transfer(remainingWithheld); + emit EtherDividendWithholdingWithdrawn(msg.sender, _dividendIndex, remainingWithheld); } } diff --git a/demo/checkpoint/ethExplorer.js b/demo/checkpoint/ethExplorer.js new file mode 100644 index 000000000..a32bd6b18 --- /dev/null +++ b/demo/checkpoint/ethExplorer.js @@ -0,0 +1,543 @@ +const duration = { + seconds: function (val) { return val; }, + minutes: function (val) { return val * this.seconds(60); }, + hours: function (val) { return val * this.minutes(60); }, + days: function (val) { return val * this.hours(24); }, + weeks: function (val) { return val * this.days(7); }, + years: function (val) { return val * this.days(365); }, + }; +var readlineSync = require('readline-sync'); +var BigNumber = require('bignumber.js') +var chalk = require('chalk'); +var common = require('../common/common_functions'); + +var contracts = require("../helpers/contract_addresses"); +let tickerRegistryAddress = contracts.tickerRegistryAddress(); +let securityTokenRegistryAddress = contracts.securityTokenRegistryAddress(); +let cappedSTOFactoryAddress = contracts.cappedSTOFactoryAddress(); +let etherDividendCheckpointFactoryAddress = contracts.etherDividendCheckpointFactoryAddress(); + +let tickerRegistryABI; +let securityTokenRegistryABI; +let securityTokenABI; +let cappedSTOABI; +let generalTransferManagerABI; +try{ + tickerRegistryABI = JSON.parse(require('fs').readFileSync('./build/contracts/TickerRegistry.json').toString()).abi; + securityTokenRegistryABI = JSON.parse(require('fs').readFileSync('./build/contracts/SecurityTokenRegistry.json').toString()).abi; + securityTokenABI = JSON.parse(require('fs').readFileSync('./build/contracts/SecurityToken.json').toString()).abi; + cappedSTOABI = JSON.parse(require('fs').readFileSync('./build/contracts/CappedSTO.json').toString()).abi; + generalTransferManagerABI = JSON.parse(require('fs').readFileSync('./build/contracts/GeneralTransferManager.json').toString()).abi; + etherDividendCheckpointABI = JSON.parse(require('fs').readFileSync('./build/contracts/EtherDividendCheckpoint.json').toString()).abi; +}catch(err){ + console.log('\x1b[31m%s\x1b[0m',"Couldn't find contracts' artifacts. Make sure you ran truffle compile first"); + return; +} + + +const DEFAULT_GAS_PRICE = 80000000000; +const Web3 = require('web3'); + +if (typeof web3 !== 'undefined') { + web3 = new Web3(web3.currentProvider); +} else { + // set the provider you want from Web3.providers + web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545")); +} + +let tokenSymbol; +let securityToken; + +async function executeApp() { + + accounts = await web3.eth.getAccounts(); + Issuer = accounts[0]; + + setup(); + +}; + +async function setup(){ + try { + tickerRegistry = new web3.eth.Contract(tickerRegistryABI,tickerRegistryAddress); + tickerRegistry.setProvider(web3.currentProvider); + securityTokenRegistry = new web3.eth.Contract(securityTokenRegistryABI,securityTokenRegistryAddress); + securityTokenRegistry.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."); + return; + } + + start_explorer(); + +} + +async function start_explorer(){ + + let tokenDeployed = false; + let tokenDeployedAddress; + if(!tokenSymbol){ + tokenSymbol = readlineSync.question('Enter the token symbol: '); + // Let's check if token has already been deployed, if it has, skip to STO + await securityTokenRegistry.methods.getSecurityTokenAddress(tokenSymbol).call({from: Issuer}, function(error, result){ + if(result != "0x0000000000000000000000000000000000000000"){ + securityToken = new web3.eth.Contract(securityTokenABI,result); + } + }); + } + + let checkpointNum = await securityToken.methods.currentCheckpointId().call({ from: Issuer }); + console.log("Token is at checkpoint:",checkpointNum); + + // Get the GTM + await securityToken.methods.getModule(2, 0).call({ from: Issuer }, function (error, result) { + generalTransferManagerAddress = result[1]; + }); + generalTransferManager = new web3.eth.Contract(generalTransferManagerABI, generalTransferManagerAddress); + generalTransferManager.setProvider(web3.currentProvider); + + await securityToken.methods.getModuleByName(4, web3.utils.toHex("EtherDividendCheckpoint")).call({ from: Issuer }, function (error, result) { + etherDividendCheckpointAddress = result[1]; + console.log("Dividends module address is:",etherDividendCheckpointAddress); + if(etherDividendCheckpointAddress != "0x0000000000000000000000000000000000000000"){ + etherDividendCheckpoint = new web3.eth.Contract(etherDividendCheckpointABI, etherDividendCheckpointAddress); + etherDividendCheckpoint.setProvider(web3.currentProvider); + } + }); + + let options = ['Mint tokens', 'Transfer tokens', 'Explore account at checkpoint', + 'Explore total supply at checkpoint', 'Create checkpoint', 'Issue Dividends', + 'Issue Dividends at a checkpoint', 'Tax Withholding', 'Push dividends to account', + 'Pull dividends to account', 'Explore ETH balance', 'Reclaimed dividends after expiry', 'Exit']; + let index = readlineSync.keyInSelect(options, 'What do you want to do?', {cancel: false}); + console.log("Selected:",options[index]); + switch(index){ + case 0: + let _to = readlineSync.question('Enter beneficiary of minting: '); + let _amount = readlineSync.question('Enter amount of tokens to mint: '); + await mintTokens(_to,_amount); + break; + case 1: + let _to2 = readlineSync.question('Enter beneficiary of tranfer: '); + let _amount2 = readlineSync.question('Enter amount of tokens to transfer: '); + await transferTokens(_to2,_amount2); + break; + case 2: + let _address = readlineSync.question('Enter address to explore: '); + let _checkpoint = readlineSync.question('At checkpoint: '); + await exploreAddress(_address,_checkpoint); + break; + case 3: + let _checkpoint2 = readlineSync.question('Explore total supply at checkpoint: '); + await exploreTotalSupply(_checkpoint2); + break; + case 4: + //Create new checkpoint + await securityToken.methods.createCheckpoint().send({ from: Issuer}); + break; + case 5: + //Create dividends + let ethDividend = readlineSync.question('How much eth would you like to distribute to token holders?: '); + await createDividends(ethDividend); + break; + case 6: + //Create dividends + let _ethDividend = readlineSync.question('How much eth would you like to distribute to token holders?: '); + let _checkpointId = readlineSync.question(`Enter the checkpoint on which you want to distribute dividend: `); + let currentCheckpointId = await securityToken.methods.currentCheckpointId().call(); + if (currentCheckpointId >= _checkpointId) { + await createDividendWithCheckpoint(_ethDividend, _checkpointId); + } else { + console.log(`Future checkpoint are not allowed to create the dividends`); + } + break; + case 7: + await withholdingTax(); + break; + case 8: + //Create dividends + let _checkpoint3 = readlineSync.question('Distribute dividends at checkpoint: '); + let _address2 = readlineSync.question('Enter address to push dividends to (ex- add1,add2,add3,...): '); + await pushDividends(_checkpoint3,_address2); + break; + case 9: + let _checkpoint7 = readlineSync.question('Distribute dividends at checkpoint: '); + await pullDividends(_checkpoint7); + break; + case 10: + //explore eth balance + let _checkpoint4 = readlineSync.question('Enter checkpoint to explore: '); + let _address3 = readlineSync.question('Enter address to explore: '); + let _dividendIndex = await etherDividendCheckpoint.methods.getDividendIndex(_checkpoint4).call(); + if (_dividendIndex.length == 1) { + let div = await etherDividendCheckpoint.methods.dividends(_dividendIndex[0]).call(); + let res = await etherDividendCheckpoint.methods.calculateDividend(_dividendIndex[0],_address3).call({ from: Issuer}); + let claim = new BigNumber(res[0]); + let withheld = new BigNumber(res[1]); + let percent = withheld.dividedBy(claim).times(100); + console.log(` + ETH Balance: ${web3.utils.fromWei((await web3.eth.getBalance(_address3)).toString(),"ether")} ETH + Dividend total size: ${web3.utils.fromWei((div.amount).toString(),"ether")} ETH + Dividends owed to investor at checkpoint ${_checkpoint4}: ${web3.utils.fromWei((claim).toString(),"ether")} ETH + Dividends withheld at checkpoint ${_checkpoint4}: ${web3.utils.fromWei((withheld).toString(),"ether")} ETH + Tax withholding percentage: ${percent}% + `) + } else { + console.log("Sorry Future checkpoints are not allowed"); + } + break; + case 11: + let _checkpoint5 = readlineSync.question('Enter the checkpoint to explore: '); + await reclaimedDividend(_checkpoint5); + break; + case 12: + process.exit(); + } + + //Restart + start_explorer(); + +} + +async function createDividends(ethDividend){ + // Get the Dividends module + await securityToken.methods.getModuleByName(4, web3.utils.toHex("EtherDividendCheckpoint")).call({ from: Issuer }, function (error, result) { + etherDividendCheckpointAddress = result[1]; + }); + if(etherDividendCheckpointAddress != "0x0000000000000000000000000000000000000000"){ + etherDividendCheckpoint = new web3.eth.Contract(etherDividendCheckpointABI, etherDividendCheckpointAddress); + etherDividendCheckpoint.setProvider(web3.currentProvider); + }else{ + let addModuleAction = securityToken.methods.addModule(etherDividendCheckpointFactoryAddress, web3.utils.fromAscii('', 16), 0, 0); + let GAS = await common.estimateGas(addModuleAction, Issuer, 1.2); + await addModuleAction.send({ from: Issuer, gas: GAS }) + .on('transactionHash', function(hash){ + console.log(` + Your transaction is being processed. Please wait... + TxHash: ${hash}\n` + ); + }) + .on('receipt', function(receipt){ + console.log(` + Congratulations! The transaction was successfully completed. + Module deployed at address: ${receipt.events.LogModuleAdded.returnValues._module} + Review it on Etherscan. + TxHash: ${receipt.transactionHash}\n` + ); + + etherDividendCheckpoint = new web3.eth.Contract(etherDividendCheckpointABI, receipt.events.LogModuleAdded.returnValues._module); + etherDividendCheckpoint.setProvider(web3.currentProvider); + }) + .on('error', console.error); + } + + let time = (await web3.eth.getBlock('latest')).timestamp; + let expiryTime = readlineSync.question('Enter the dividend expiry time (Unix Epoch time)\n(10 minutes from now = '+(time+duration.minutes(10))+' ): '); + let blacklist = readlineSync.question('Enter an address to blacklist from dividend distribution: '); + if(expiryTime == "") expiryTime = time+duration.minutes(10); + //Send eth dividends + let createDividendAction = etherDividendCheckpoint.methods.createDividend(time, expiryTime, [blacklist]); + GAS = await common.estimateGas(createDividendAction, Issuer, 1.2, web3.utils.toWei(ethDividend,"ether")); + await createDividendAction.send({ from: Issuer, value: web3.utils.toWei(ethDividend,"ether"), gas: GAS }) + .on('transactionHash', function(hash){ + console.log(` + Your transaction is being processed. Please wait... + TxHash: ${hash}\n` + ); + }) + .on('receipt', function(receipt){ + console.log(` + ${receipt.events} + TxHash: ${receipt.transactionHash}\n` + ); + }) +} + +async function withholdingTax() { + let GAS + let options = ['Set a % to withhold from the dividends sent to an address', 'Withdraw all withheld dividends', 'Return to main menu']; + let index = readlineSync.keyInSelect(options, 'What do you want to do?', {cancel: false}); + console.log("Selected:",options[index]); + switch (index) { + case 0: + let addressT = readlineSync.question('Enter the address of the investor for which to withhold dividends: '); + let percentT = readlineSync.question('Enter the percentage of dividends to withhold (number between 0-100): '); + percentT = web3.utils.toWei((percentT * 10).toString(), 'milli'); + GAS = Math.round(1.2 * (await etherDividendCheckpoint.methods.setWithholdingFixed([addressT], percentT).estimateGas({from: Issuer}))); + console.log(chalk.black.bgYellowBright(`---- Transaction executed: setWithholdingFixed - Gas limit provided: ${GAS} ----`)); + await etherDividendCheckpoint.methods.setWithholdingFixed([addressT], percentT).send({from: Issuer, gas: GAS, gasPrice: DEFAULT_GAS_PRICE }) + .on('receipt', function(receipt){ + console.log(chalk.green(`\nSuccessfully set tax withholding of ${web3.utils.fromWei(percentT, 'milli')/10}% for ${addressT}.`)); + }); + break; + case 1: + // Withdraw + let checkpointId = readlineSync.question('Enter the checkpoint at which to withdraw: '); + let dividendIndex = await etherDividendCheckpoint.methods.getDividendIndex(checkpointId).call(); + if (dividendIndex.length == 1) { + GAS = Math.round(1.2 * (await etherDividendCheckpoint.methods.withdrawWithholding(dividendIndex[0]).estimateGas({from: Issuer}))); + console.log(chalk.black.bgYellowBright(`---- Transaction executed: withdrawWithholding - Gas limit provided: ${GAS} ----`)); + await etherDividendCheckpoint.methods.withdrawWithholding(dividendIndex[0]).send({from: Issuer, gas: GAS, gasPrice: DEFAULT_GAS_PRICE }) + .on('receipt', function(receipt){ + let val = receipt.events.EtherDividendWithholdingWithdrawn.returnValues._withheldAmount; + console.log(chalk.green(`\nSuccessfully withdrew ${val} ETH from checkpoint ${checkpointId} and dividend ${dividendIndex[0]} tax withholding to ${Issuer}.`)); + }); + }else{ + console.log(`\nCheckpoint doesn't exist`); + } + break; + case 2: + return; + break; + } +} + +async function createDividendWithCheckpoint(ethDividend, _checkpointId) { + + // Get the Dividends module + await securityToken.methods.getModuleByName(4, web3.utils.toHex("EtherDividendCheckpoint")).call({ from: Issuer }, function (error, result) { + etherDividendCheckpointAddress = result[1]; + }); + if(etherDividendCheckpointAddress != "0x0000000000000000000000000000000000000000"){ + etherDividendCheckpoint = new web3.eth.Contract(etherDividendCheckpointABI, etherDividendCheckpointAddress); + etherDividendCheckpoint.setProvider(web3.currentProvider); + }else{ + let addModuleAction = securityToken.methods.addModule(etherDividendCheckpointFactoryAddress, web3.utils.fromAscii('', 16), 0, 0); + let GAS = await common.estimateGas(addModuleAction, Issuer, 1.2); + await addModuleAction.send({ from: Issuer, gas: GAS }) + .on('transactionHash', function(hash){ + console.log(` + Your transaction is being processed. Please wait... + TxHash: ${hash}\n` + ); + }) + .on('receipt', function(receipt){ + console.log(` + Congratulations! The transaction was successfully completed. + Module deployed at address: ${receipt.events.LogModuleAdded.returnValues._module} + Review it on Etherscan. + TxHash: ${receipt.transactionHash}\n` + ); + + etherDividendCheckpoint = new web3.eth.Contract(etherDividendCheckpointABI, receipt.events.LogModuleAdded.returnValues._module); + etherDividendCheckpoint.setProvider(web3.currentProvider); + }) + .on('error', console.error); + } + + let time = (await web3.eth.getBlock('latest')).timestamp; + let expiryTime = readlineSync.question('Enter the dividend expiry time (Unix Epoch time)\n(10 minutes from now = '+(time+duration.minutes(10))+' ): '); + if(expiryTime == "") expiryTime = time+duration.minutes(10); + let _dividendStatus = await etherDividendCheckpoint.methods.getDividendIndex(_checkpointId).call(); + if (_dividendStatus.length != 1) { + //Send eth dividends + let createDividendWithCheckpointAction = etherDividendCheckpoint.methods.createDividendWithCheckpoint(time, expiryTime, _checkpointId, []); + let GAS = await common.estimateGas(createDividendWithCheckpointAction, Issuer, 1.2, web3.utils.toWei(ethDividend,"ether")); + await createDividendWithCheckpointAction.send({ from: Issuer, value: web3.utils.toWei(ethDividend,"ether"), gas: GAS }) + .on('transactionHash', function(hash){ + console.log(` + Your transaction is being processed. Please wait... + TxHash: ${hash}\n` + ); + }) + .on('receipt', function(receipt){ + console.log(` + Congratulations! Dividend is created successfully. + CheckpointId: ${receipt.events.EtherDividendDeposited.returnValues._checkpointId} + TxHash: ${receipt.transactionHash}\n` + ); + }) + .on('error', console.error); + } else { + console.log(chalk.blue(`\nDividends are already distributed at checkpoint '${_checkpointId}'. Not allowed to re-create\n`)); + } +} + +async function pushDividends(checkpoint, account){ + let accs = account.split(','); + let dividend = await etherDividendCheckpoint.methods.getDividendIndex(checkpoint).call(); + if(dividend.length == 1) { + let pushDividendPaymentToAddressesAction = etherDividendCheckpoint.methods.pushDividendPaymentToAddresses(dividend[0], accs); + let GAS = await common.estimateGas(pushDividendPaymentToAddressesAction, Issuer, 1.2); + await pushDividendPaymentToAddressesAction.send({ from: Issuer, gas: GAS }) + .on('transactionHash', function(hash){ + console.log(` + Your transaction is being processed. Please wait... + TxHash: ${hash}\n` + ); + }) + .on('receipt', function(receipt){ + console.log(` + Congratulations! Dividends are pushed successfully + TxHash: ${receipt.transactionHash}\n` + ); + }) + } else { + console.log(`Checkpoint is not yet created. Please enter the pre-created checkpoint`); + } + + +} + +async function pullDividends(checkpointId) { + let dividend = await etherDividendCheckpoint.methods.getDividendIndex(checkpointId).call(); + if(dividend.length == 1) { + try { + let pullDividendPaymentAction = etherDividendCheckpoint.methods.pullDividendPayment(dividend[0]); + let GAS = await common.estimateGas(pullDividendPaymentAction, Issuer, 1.2); + await pullDividendPaymentAction.send({ from: Issuer, gas: GAS }) + .on('transactionHash', function(hash){ + console.log(` + Your transaction is being processed. Please wait... + TxHash: ${hash}\n` + ); + }) + .on('receipt', function(receipt){ + console.log(` + Amount: ${web3.utils.fromWei(receipt.events.EtherDividendClaimed.returnValues._amount, "ether")} ETH + Payee: ${receipt.events.EtherDividendClaimed.returnValues._payee} + TxHash: ${receipt.transactionHash}\n` + ); + }) + .on('error', console.error); + } catch(error) { + console.log(error.message); + } + } else { + console.log(`Checkpoint is not yet created. Please enter the pre-created checkpoint`); + } +} + +async function exploreAddress(address, checkpoint){ + let balance = await securityToken.methods.balanceOf(address).call({from: Issuer}); + balance = web3.utils.fromWei(balance,"ether"); + console.log("Balance of",address,"is:",balance,"(Using balanceOf)"); + + let balanceAt = await securityToken.methods.balanceOfAt(address,checkpoint).call({from: Issuer}); + balanceAt = web3.utils.fromWei(balanceAt,"ether"); + console.log("Balance of",address,"is:",balanceAt,"(Using balanceOfAt - checkpoint",checkpoint,")"); +} + +async function exploreTotalSupply(checkpoint){ + let totalSupply = await securityToken.methods.totalSupply().call({from: Issuer}); + totalSupply = web3.utils.fromWei(totalSupply,"ether"); + console.log("TotalSupply is:",totalSupply,"(Using totalSupply)"); + + let totalSupplyAt = await securityToken.methods.totalSupplyAt(checkpoint).call({from: Issuer}); + totalSupplyAt = web3.utils.fromWei(totalSupplyAt,"ether"); + console.log("totalSupply is:",totalSupplyAt,"(Using totalSupplyAt - checkpoint",checkpoint,")"); +} + + +async function transferTokens(address, amount){ + + let modifyWhitelistAction = generalTransferManager.methods.modifyWhitelist(address, Math.floor(Date.now()/1000), Math.floor(Date.now()/1000), Math.floor(Date.now()/1000 + 31536000), true); + let GAS = await common.estimateGas(modifyWhitelistAction, Issuer, 1.2); + let whitelistTransaction = await modifyWhitelistAction.send({ from: Issuer, gas: GAS}); + + try{ + let transferAction = securityToken.methods.transfer(address,web3.utils.toWei(amount,"ether")); + GAS = await common.estimateGas(transferAction, Issuer, 1.2); + await transferAction.send({ from: Issuer, gas: GAS}) + .on('transactionHash', function(hash){ + console.log(` + Your transaction is being processed. Please wait... + TxHash: ${hash}\n` + ); + }) + .on('receipt', function(receipt){ + console.log(` + Congratulations! The transaction was successfully completed. + + Account ${receipt.events.Transfer.returnValues.from} + transfered ${web3.utils.fromWei(receipt.events.Transfer.returnValues.value,"ether")} tokens + to account ${receipt.events.Transfer.returnValues.to} + + Review it on Etherscan. + TxHash: ${receipt.transactionHash}\n` + ); + }); + + }catch (err){ + console.log(err); + console.log("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.") + return; + } +} + +async function mintTokens(address, amount){ + let isSTOAttached; + let modifyWhitelistAction = generalTransferManager.methods.modifyWhitelist(address,Math.floor(Date.now()/1000),Math.floor(Date.now()/1000),Math.floor(Date.now()/1000 + 31536000),true); + let GAS = await common.estimateGas(modifyWhitelistAction, Issuer, 1.2); + let whitelistTransaction = await modifyWhitelistAction.send({ from: Issuer, gas: GAS}); + let _flag = await securityToken.methods.finishedIssuerMinting().call(); + await securityToken.methods.getModule(3, 0).call({from: Issuer}, function(error, result) { + isSTOAttached = result[1] == "0x0000000000000000000000000000000000000000"? false : true; + }); + if (isSTOAttached || _flag) { + console.log("\n"); + console.log("***************************") + console.log("Minting is Finished"); + console.log("***************************\n") + return; + } + try{ + let mintAction = securityToken.methods.mint(address,web3.utils.toWei(amount,"ether")); + GAS = await common.estimateGas(mintAction, Issuer, 1.2); + await mintAction.send({ from: Issuer, gas: GAS}) + .on('transactionHash', function(hash){ + console.log(` + Your transaction is being processed. Please wait... + TxHash: ${hash}\n` + ); + }) + .on('receipt', function(receipt){ + console.log(` + Congratulations! The transaction was successfully completed. + + Minted ${web3.utils.fromWei(receipt.events.Transfer.returnValues.value,"ether")} tokens + to account ${receipt.events.Transfer.returnValues.to} + + Review it on Etherscan. + TxHash: ${receipt.transactionHash}\n` + ); + }); + + }catch (err){ + console.log(err); + console.log("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.") + return; + } +} + +async function reclaimedDividend(checkpointId) { + let dividendIndex = await etherDividendCheckpoint.methods.getDividendIndex(checkpointId).call(); + if (dividendIndex.length == 1) { + let reclaimDividendAction = etherDividendCheckpoint.methods.reclaimDividend(dividendIndex[0]); + let GAS = await common.estimateGas(reclaimDividendAction, Issuer, 1.2); + await reclaimDividendAction.send({from: Issuer, gas: GAS}) + .on("transactionHash", function(hash) { + console.log(` + Your transaction is being processed. Please wait... + TxHash: ${hash}\n` + ); + }) + .on('receipt', function(receipt){ + console.log(` + Congratulations! The transaction was successfully completed. + + Claimed Amount ${web3.utils.fromWei(receipt.events.EtherDividendReclaimed.returnValues._claimedAmount,"ether")} ETH + to account ${receipt.events.EtherDividendReclaimed.returnValues._claimer} + + Review it on Etherscan. + TxHash: ${receipt.transactionHash}\n` + ); + }) + .on('error', console.error); + }else{ + console.log(`\nCheckpoint doesn't exist`); + } +} + +executeApp(); diff --git a/package-lock.json b/package-lock.json index 99315dfe7..fc73460ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9463,9 +9463,9 @@ "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=" }, "truffle": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/truffle/-/truffle-4.1.11.tgz", - "integrity": "sha512-VNhc6jexZeM92sNJJr4U8ln3uJ/mJEQO/0y9ZLYc4pccyIskPtl+3r4mzymgGM/Mq5v6MpoQVD6NZgHUVKX+Dw==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/truffle/-/truffle-4.1.13.tgz", + "integrity": "sha1-vydYaYi0/4RWPt+/MrR5QUCKdq0=", "dev": true, "requires": { "mocha": "4.1.0", diff --git a/package.json b/package.json index c1259e7ec..3e06d880f 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,7 @@ "solidity-coverage": "^0.5.10", "solidity-docgen": "^0.1.0", "solium": "^1.1.6", - "truffle": "^4.1.11", + "truffle": "^4.1.13", "truffle-wallet-provider": "0.0.5" } } diff --git a/scripts/docs.sh b/scripts/docs.sh index 6cb78ded3..64194ea95 100755 --- a/scripts/docs.sh +++ b/scripts/docs.sh @@ -8,6 +8,7 @@ DIRECTORY=polymath-developer-portal WEBSITE_DIRECTORY=versioned_docs CORE_ROUTE=$PWD + # functions that used to create the documentation create_docs() { @@ -15,9 +16,9 @@ create_docs() { if [ "$(git branch | grep -w $latestTag)" == "" ]; then # Check whether the branch is already present or not - if [ "$(git branch -r | grep "origin/$latestTag" | wc -l)" -eq 1 ]; + if [ "$(git branch -r | grep "origin/$latestTag" | wc -l)" -ge 1 ]; then - echo "$latesTag Branch is already present on remote" + echo "$latestTag Branch is already present on remote" exit 0 fi # Checkout and create the $latestTag branch @@ -29,19 +30,21 @@ create_docs() { echo "Creating the new docs for the version "$latestTag"" cd $WEBSITE_DIRECTORY - - # Creating the new directory with name $latestTag - mkdir $latestTag fi echo "Generating the API documentation in branch $latestTag" # Command to generate the documentation using the solidity-docgen - #npm install > /dev/null 2>&1 + migrate=$(SOLC_ARGS="openzeppelin-solidity="$CORE_ROUTE"/node_modules/openzeppelin-solidity" \ -solidity-docgen $CORE_ROUTE $CORE_ROUTE/contracts $CORE_ROUTE/polymath-developer-portal/) +solidity-docgen -x $CORE_ROUTE/contracts/external,$CORE_ROUTE/contracts/mocks $CORE_ROUTE $CORE_ROUTE/contracts $CORE_ROUTE/polymath-developer-portal/) + echo "Successfully docs are generated..." - echo "Transferring the API DOCS to $latestTag directory" - mv ../../docs/api_* $latestTag + + echo "Installing npm dependencies..." + yarn install > /dev/null 2>&1 + + echo "Gererate versioning docs..." + yarn run version $versionNo # Commit the changes echo "Commiting the new changes..." @@ -98,8 +101,9 @@ else echo "There is no version specific folders" create_docs else - echo "$(basename "$dir")" - if [ "$(basename "$dir")" == "$latestTag" ]; then + reponame=$(echo $(basename "$dir") | cut -d '-' -f2) + echo $reponame + if [ "$reponame" == "$versionNo" ]; then reject_docs fi fi diff --git a/test/e_erc20_dividends.js b/test/e_erc20_dividends.js index 47e3334c6..4e4812419 100644 --- a/test/e_erc20_dividends.js +++ b/test/e_erc20_dividends.js @@ -190,8 +190,8 @@ contract('ERC20DividendCheckpoint', accounts => { I_SecurityTokenRegistryProxy = await SecurityTokenRegistryProxy.new({from: account_polymath}); let bytesProxy = encodeProxyCall([I_PolymathRegistry.address, I_STFactory.address, initRegFee, initRegFee, I_PolyToken.address, account_polymath]); await I_SecurityTokenRegistryProxy.upgradeToAndCall("1.0.0", I_SecurityTokenRegistry.address, bytesProxy, {from: account_polymath}); - I_STRProxied = await SecurityTokenRegistry.at(I_SecurityTokenRegistryProxy.address); - + I_STRProxied = await SecurityTokenRegistry.at(I_SecurityTokenRegistryProxy.address); + // Step 10: Deploy the FeatureRegistry I_FeatureRegistry = await FeatureRegistry.new( @@ -264,7 +264,7 @@ contract('ERC20DividendCheckpoint', accounts => { }); - it("Should successfully attach the ERC20DividendCheckpoint with the security token", async () => { + it("Should successfully attach the ERC20DividendCheckpoint with the security token - fail insufficient payment", async () => { let errorThrown = false; try { const tx = await I_SecurityToken.addModule(P_ERC20DividendCheckpointFactory.address, "", web3.utils.toWei("500", "ether"), 0, { from: token_owner }); @@ -276,7 +276,7 @@ contract('ERC20DividendCheckpoint', accounts => { assert.ok(errorThrown, message); }); - it("Should successfully attach the ERC20DividendCheckpoint with the security token", async () => { + it("Should successfully attach the ERC20DividendCheckpoint with the security token with budget", async () => { let snapId = await takeSnapshot() await I_PolyToken.getTokens(web3.utils.toWei("500", "ether"), token_owner); await I_PolyToken.transfer(I_SecurityToken.address, web3.utils.toWei("500", "ether"), {from: token_owner}); @@ -360,7 +360,7 @@ contract('ERC20DividendCheckpoint', accounts => { ); }); - it("Should fail in creating the dividend", async() => { + it("Should fail in creating the dividend - incorrect allowance", async() => { let errorThrown = false; let maturity = latestTime(); let expiry = latestTime() + duration.days(10); @@ -375,7 +375,7 @@ contract('ERC20DividendCheckpoint', accounts => { assert.ok(errorThrown, message); }); - it("Should fail in creating the dividend", async() => { + it("Should fail in creating the dividend - maturity > expiry", async() => { let errorThrown = false; let maturity = latestTime(); let expiry = latestTime() - duration.days(10); @@ -390,7 +390,7 @@ contract('ERC20DividendCheckpoint', accounts => { assert.ok(errorThrown, message); }); - it("Should fail in creating the dividend", async() => { + it("Should fail in creating the dividend - now > expiry", async() => { let errorThrown = false; let maturity = latestTime() - duration.days(2); let expiry = latestTime() - duration.days(1); @@ -404,7 +404,7 @@ contract('ERC20DividendCheckpoint', accounts => { assert.ok(errorThrown, message); }); - it("Should fail in creating the dividend", async() => { + it("Should fail in creating the dividend - bad token", async() => { let errorThrown = false; let maturity = latestTime(); let expiry = latestTime() + duration.days(10); @@ -418,7 +418,7 @@ contract('ERC20DividendCheckpoint', accounts => { assert.ok(errorThrown, message); }); - it("Should fail in creating the dividend", async() => { + it("Should fail in creating the dividend - amount is 0", async() => { let errorThrown = false; let maturity = latestTime(); let expiry = latestTime() + duration.days(10); @@ -446,7 +446,7 @@ contract('ERC20DividendCheckpoint', accounts => { assert.equal(await I_SecurityToken.balanceOf(account_investor2), web3.utils.toWei('3', 'ether')); }); - it("Issuer pushes dividends iterating over account holders - dividends proportional to checkpoint", async() => { + it("Issuer pushes dividends iterating over account holders - dividends proportional to checkpoint - fails maturity in the future", async() => { let errorThrown = false; try { await I_ERC20DividendCheckpoint.pushDividendPayment(0, 0, 10, {from: token_owner}); @@ -458,7 +458,7 @@ contract('ERC20DividendCheckpoint', accounts => { assert.ok(errorThrown, message); }); - it("Issuer pushes dividends iterating over account holders - dividends proportional to checkpoint", async() => { + it("Issuer pushes dividends iterating over account holders - dividends proportional to checkpoint - fails not owner", async() => { let errorThrown = false; // Increase time by 2 day await increaseTime(duration.days(2)); @@ -472,7 +472,7 @@ contract('ERC20DividendCheckpoint', accounts => { assert.ok(errorThrown, message); }); - it("Issuer pushes dividends iterating over account holders - dividends proportional to checkpoint", async() => { + it("Issuer pushes dividends iterating over account holders - dividends proportional to checkpoint - fails wrong index", async() => { let errorThrown = false; try { await I_ERC20DividendCheckpoint.pushDividendPayment(2, 0, 10, {from: token_owner}); @@ -485,7 +485,6 @@ contract('ERC20DividendCheckpoint', accounts => { }); it("Issuer pushes dividends iterating over account holders - dividends proportional to checkpoint", async() => { - let _dev = await I_ERC20DividendCheckpoint.dividends.call(0); let investor1Balance = BigNumber(await I_PolyToken.balanceOf(account_investor1)); let investor2Balance = BigNumber(await I_PolyToken.balanceOf(account_investor2)); await I_ERC20DividendCheckpoint.pushDividendPayment(0, 0, 10, {from: token_owner, gas: 5000000}); @@ -494,7 +493,7 @@ contract('ERC20DividendCheckpoint', accounts => { assert.equal(investor1BalanceAfter.sub(investor1Balance).toNumber(), web3.utils.toWei('0.5', 'ether')); assert.equal(investor2BalanceAfter.sub(investor2Balance).toNumber(), web3.utils.toWei('1', 'ether')); //Check fully claimed - assert.equal((await I_ERC20DividendCheckpoint.dividends(0))[6].toNumber(), web3.utils.toWei('1.5', 'ether')); + assert.equal((await I_ERC20DividendCheckpoint.dividends(0))[5].toNumber(), web3.utils.toWei('1.5', 'ether')); }); it("Buy some tokens for account_temp (1 ETH)", async() => { @@ -531,7 +530,7 @@ contract('ERC20DividendCheckpoint', accounts => { assert.equal(tx.logs[0].args._checkpointId.toNumber(), 2, "Dividend should be created at checkpoint 1"); }); - it("Issuer pushes dividends iterating over account holders - dividends proportional to checkpoint", async() => { + it("Issuer pushes dividends iterating over account holders - dividends proportional to checkpoint - fails past expiry", async() => { let errorThrown = false; await increaseTime(duration.days(12)); try { @@ -544,7 +543,7 @@ contract('ERC20DividendCheckpoint', accounts => { assert.ok(errorThrown, message); }); - it("Issuer pushes dividends iterating over account holders - dividends proportional to checkpoint", async() => { + it("Issuer pushes dividends iterating over account holders - dividends proportional to checkpoin - fails already reclaimed", async() => { let errorThrown = false; let tx = await I_ERC20DividendCheckpoint.reclaimDividend(1, {from: token_owner, gas: 500000}); assert.equal((tx.logs[0].args._claimedAmount).toNumber(), web3.utils.toWei("1.5", "ether")); @@ -583,16 +582,20 @@ contract('ERC20DividendCheckpoint', accounts => { ); }); + it("Exclude account_temp using global exclusion list", async() => { + await I_ERC20DividendCheckpoint.setExcluded([account_temp], {from: token_owner}); + }); + it("Create another new dividend", async() => { let maturity = latestTime(); let expiry = latestTime() + duration.days(10); await I_PolyToken.getTokens(web3.utils.toWei('11', 'ether'), token_owner); await I_PolyToken.approve(I_ERC20DividendCheckpoint.address, web3.utils.toWei('11', 'ether'), {from: token_owner}); - let tx = await I_ERC20DividendCheckpoint.createDividend(maturity, expiry, I_PolyToken.address, web3.utils.toWei('11', 'ether'), {from: token_owner}); + let tx = await I_ERC20DividendCheckpoint.createDividend(maturity, expiry, I_PolyToken.address, web3.utils.toWei('10', 'ether'), {from: token_owner}); assert.equal(tx.logs[0].args._checkpointId.toNumber(), 3, "Dividend should be created at checkpoint 2"); }); - it("should investor 3 claims dividend", async() => { + it("should investor 3 claims dividend - fail bad index", async() => { let errorThrown = false; let investor1Balance = BigNumber(await I_PolyToken.balanceOf(account_investor1)); let investor2Balance = BigNumber(await I_PolyToken.balanceOf(account_investor2)); @@ -608,6 +611,7 @@ contract('ERC20DividendCheckpoint', accounts => { }); it("should investor 3 claims dividend", async() => { + console.log((await I_ERC20DividendCheckpoint.dividends(2))[5].toNumber()); let investor1Balance = BigNumber(await I_PolyToken.balanceOf(account_investor1)); let investor2Balance = BigNumber(await I_PolyToken.balanceOf(account_investor2)); let investor3Balance = BigNumber(await I_PolyToken.balanceOf(account_investor3)); @@ -620,7 +624,7 @@ contract('ERC20DividendCheckpoint', accounts => { assert.equal(investor3BalanceAfter1.sub(investor3Balance).toNumber(), web3.utils.toWei('7', 'ether')); }); - it("should investor 3 claims dividend", async() => { + it("should investor 3 claims dividend - fails already claimed", async() => { let errorThrown = false; try { await I_ERC20DividendCheckpoint.pullDividendPayment(2, {from: account_investor3, gasPrice: 0}); @@ -633,32 +637,45 @@ contract('ERC20DividendCheckpoint', accounts => { }); it("should issuer pushes remain", async() => { + console.log((await I_ERC20DividendCheckpoint.dividends(2))[5].toNumber()); let investor1BalanceAfter1 = BigNumber(await I_PolyToken.balanceOf(account_investor1)); let investor2BalanceAfter1 = BigNumber(await I_PolyToken.balanceOf(account_investor2)); let investor3BalanceAfter1 = BigNumber(await I_PolyToken.balanceOf(account_investor3)); + let investorTempBalanceAfter1 = BigNumber(await I_PolyToken.balanceOf(account_temp)); await I_ERC20DividendCheckpoint.pushDividendPayment(2, 0, 10, {from: token_owner}); let investor1BalanceAfter2 = BigNumber(await I_PolyToken.balanceOf(account_investor1)); let investor2BalanceAfter2 = BigNumber(await I_PolyToken.balanceOf(account_investor2)); let investor3BalanceAfter2 = BigNumber(await I_PolyToken.balanceOf(account_investor3)); + let investorTempBalanceAfter2 = BigNumber(await I_PolyToken.balanceOf(account_temp)); assert.equal(investor1BalanceAfter2.sub(investor1BalanceAfter1).toNumber(), 0); assert.equal(investor2BalanceAfter2.sub(investor2BalanceAfter1).toNumber(), web3.utils.toWei('3', 'ether')); assert.equal(investor3BalanceAfter2.sub(investor3BalanceAfter1).toNumber(), 0); + assert.equal(investorTempBalanceAfter2.sub(investorTempBalanceAfter1).toNumber(), 0); //Check fully claimed - assert.equal((await I_ERC20DividendCheckpoint.dividends(2))[6].toNumber(), web3.utils.toWei('11', 'ether')); + assert.equal((await I_ERC20DividendCheckpoint.dividends(2))[5].toNumber(), web3.utils.toWei('10', 'ether')); + }); + + + it("Delete global exclusion list", async() => { + await I_ERC20DividendCheckpoint.setExcluded([], {from: token_owner}); }); + it("Investor 2 transfers 1 ETH of his token balance to investor 1", async() => { await I_SecurityToken.transfer(account_investor1, web3.utils.toWei('1', 'ether'), {from: account_investor2}); assert.equal(await I_SecurityToken.balanceOf(account_investor1), web3.utils.toWei('1', 'ether')); assert.equal(await I_SecurityToken.balanceOf(account_investor2), web3.utils.toWei('2', 'ether')); assert.equal(await I_SecurityToken.balanceOf(account_investor3), web3.utils.toWei('7', 'ether')); + assert.equal(await I_SecurityToken.balanceOf(account_temp), web3.utils.toWei('1', 'ether')); }); - it("Create another new dividend with explicit", async() => { + it("Create another new dividend with explicit checkpoint - fails bad allowance", async() => { let errorThrown = false; let maturity = latestTime(); let expiry = latestTime() + duration.days(2); let tx = await I_SecurityToken.createCheckpoint({from: token_owner}); + console.log(JSON.stringify(tx.logs[0].args)); + console.log((await I_SecurityToken.currentCheckpointId()).toNumber()); await I_PolyToken.getTokens(web3.utils.toWei('20', 'ether'), token_owner); try { tx = await I_ERC20DividendCheckpoint.createDividendWithCheckpoint(maturity, expiry, I_PolyToken.address, web3.utils.toWei('20', 'ether'), 4, {from: token_owner}); @@ -670,7 +687,9 @@ contract('ERC20DividendCheckpoint', accounts => { assert.ok(errorThrown, message); }); - it("Create another new dividend with explicit", async() => { + it("Create another new dividend with explicit - fails maturity > expiry", async() => { + console.log((await I_SecurityToken.currentCheckpointId()).toNumber()); + let errorThrown = false; let maturity = latestTime(); let expiry = latestTime() - duration.days(10); @@ -685,7 +704,9 @@ contract('ERC20DividendCheckpoint', accounts => { assert.ok(errorThrown, message); }); - it("Create another new dividend with explicit", async() => { + it("Create another new dividend with explicit - fails now > expiry", async() => { + console.log((await I_SecurityToken.currentCheckpointId()).toNumber()); + let errorThrown = false; let maturity = latestTime() - duration.days(5); let expiry = latestTime() - duration.days(2); @@ -699,7 +720,7 @@ contract('ERC20DividendCheckpoint', accounts => { assert.ok(errorThrown, message); }); - it("Create another new dividend with explicit", async() => { + it("Create another new dividend with explicit - fails bad checkpoint", async() => { let errorThrown = false; let maturity = latestTime(); let expiry = latestTime() + duration.days(2); @@ -713,17 +734,20 @@ contract('ERC20DividendCheckpoint', accounts => { assert.ok(errorThrown, message); }); - it("Create another new dividend with explicit", async() => { + it("Set withholding tax of 20% on account_temp and 10% on investor2", async() => { + await I_ERC20DividendCheckpoint.setWithholding([account_temp, account_investor2], [BigNumber(20*10**16), BigNumber(10*10**16)], {from: token_owner}); + }); + + it("Create another new dividend with explicit checkpoint and exclusion", async() => { let maturity = latestTime(); let expiry = latestTime() + duration.days(10); - let tx = await I_SecurityToken.createCheckpoint({from: token_owner}); await I_PolyToken.getTokens(web3.utils.toWei('11', 'ether'), token_owner); await I_PolyToken.approve(I_ERC20DividendCheckpoint.address, web3.utils.toWei('11', 'ether'), {from: token_owner}); - tx = await I_ERC20DividendCheckpoint.createDividendWithCheckpoint(maturity, expiry, I_PolyToken.address, web3.utils.toWei('11', 'ether'), 4, {from: token_owner}); + let tx = await I_ERC20DividendCheckpoint.createDividendWithCheckpointAndExclusions(maturity, expiry, I_PolyToken.address, web3.utils.toWei('10', 'ether'), 4, [account_investor1], {from: token_owner}); assert.equal(tx.logs[0].args._checkpointId.toNumber(), 4, "Dividend should be created at checkpoint 3"); }); - it("Investor 2 claims dividend, issuer pushes investor 1", async() => { + it("Investor 2 claims dividend, issuer pushes investor 1 - fails not owner", async() => { let errorThrown = false; let investor1Balance = BigNumber(await I_PolyToken.balanceOf(account_investor1)); let investor2Balance = BigNumber(await I_PolyToken.balanceOf(account_investor2)); @@ -738,7 +762,7 @@ contract('ERC20DividendCheckpoint', accounts => { assert.ok(errorThrown, message); }); - it("Investor 2 claims dividend, issuer pushes investor 1", async() => { + it("Investor 2 claims dividend, issuer pushes investor 1 - fails bad index", async() => { let errorThrown = false; let investor1Balance = BigNumber(await I_PolyToken.balanceOf(account_investor1)); let investor2Balance = BigNumber(await I_PolyToken.balanceOf(account_investor2)); @@ -758,10 +782,14 @@ contract('ERC20DividendCheckpoint', accounts => { let dividendAmount2 = await I_ERC20DividendCheckpoint.calculateDividend.call(3, account_investor2); let dividendAmount3 = await I_ERC20DividendCheckpoint.calculateDividend.call(3, account_investor3); let dividendAmount_temp = await I_ERC20DividendCheckpoint.calculateDividend.call(3, account_temp); - assert.equal(dividendAmount1.toNumber(), web3.utils.toWei("1", "ether")); - assert.equal(dividendAmount2.toNumber(), web3.utils.toWei("2", "ether")); - assert.equal(dividendAmount3.toNumber(), web3.utils.toWei("7", "ether")); - assert.equal(dividendAmount_temp.toNumber(), web3.utils.toWei("1", "ether")); + assert.equal(dividendAmount1[0].toNumber(), web3.utils.toWei("0", "ether")); + assert.equal(dividendAmount2[0].toNumber(), web3.utils.toWei("2", "ether")); + assert.equal(dividendAmount3[0].toNumber(), web3.utils.toWei("7", "ether")); + assert.equal(dividendAmount_temp[0].toNumber(), web3.utils.toWei("1", "ether")); + assert.equal(dividendAmount1[1].toNumber(), web3.utils.toWei("0", "ether")); + assert.equal(dividendAmount2[1].toNumber(), web3.utils.toWei("0.2", "ether")); + assert.equal(dividendAmount3[1].toNumber(), web3.utils.toWei("0", "ether")); + assert.equal(dividendAmount_temp[1].toNumber(), web3.utils.toWei("0.2", "ether")); }); it("Investor 2 claims dividend", async() => { @@ -775,12 +803,12 @@ contract('ERC20DividendCheckpoint', accounts => { let investor3BalanceAfter1 = BigNumber(await I_PolyToken.balanceOf(account_investor3)); let tempBalanceAfter1 = BigNumber(await web3.eth.getBalance(account_temp)); assert.equal(investor1BalanceAfter1.sub(investor1Balance).toNumber(), 0); - assert.equal(investor2BalanceAfter1.sub(investor2Balance).toNumber(), web3.utils.toWei('2', 'ether')); + assert.equal(investor2BalanceAfter1.sub(investor2Balance).toNumber(), web3.utils.toWei('1.8', 'ether')); assert.equal(investor3BalanceAfter1.sub(investor3Balance).toNumber(), 0); assert.equal(tempBalanceAfter1.sub(tempBalance).toNumber(), 0); }); - it("Should issuer pushes investor 1 and temp investor", async() => { + it("Should issuer pushes temp investor - investor1 excluded", async() => { let investor1BalanceAfter1 = BigNumber(await I_PolyToken.balanceOf(account_investor1)); let investor2BalanceAfter1 = BigNumber(await I_PolyToken.balanceOf(account_investor2)); let investor3BalanceAfter1 = BigNumber(await I_PolyToken.balanceOf(account_investor3)); @@ -790,20 +818,27 @@ contract('ERC20DividendCheckpoint', accounts => { let investor2BalanceAfter2 = BigNumber(await I_PolyToken.balanceOf(account_investor2)); let investor3BalanceAfter2 = BigNumber(await I_PolyToken.balanceOf(account_investor3)); let tempBalanceAfter2 = BigNumber(await I_PolyToken.balanceOf(account_temp)); - assert.equal(investor1BalanceAfter2.sub(investor1BalanceAfter1).toNumber(), web3.utils.toWei('1', 'ether')); + assert.equal(investor1BalanceAfter2.sub(investor1BalanceAfter1).toNumber(), 0); assert.equal(investor2BalanceAfter2.sub(investor2BalanceAfter1).toNumber(), 0); assert.equal(investor3BalanceAfter2.sub(investor3BalanceAfter1).toNumber(), 0); - assert.equal(tempBalanceAfter2.sub(tempBalanceAfter1).toNumber(), web3.utils.toWei('1', 'ether')); + assert.equal(tempBalanceAfter2.sub(tempBalanceAfter1).toNumber(), web3.utils.toWei('0.8', 'ether')); //Check fully claimed - assert.equal((await I_ERC20DividendCheckpoint.dividends(3))[6].toNumber(), web3.utils.toWei('4', 'ether')); + assert.equal((await I_ERC20DividendCheckpoint.dividends(3))[5].toNumber(), web3.utils.toWei('3', 'ether')); }); it("should calculate dividend after the push dividend payment", async() => { let dividendAmount1 = await I_ERC20DividendCheckpoint.calculateDividend.call(3, account_investor1); let dividendAmount2 = await I_ERC20DividendCheckpoint.calculateDividend.call(3, account_investor2); - assert.equal(dividendAmount1.toNumber(), 0); - assert.equal(dividendAmount2.toNumber(), 0); - }); + assert.equal(dividendAmount1[0].toNumber(), 0); + assert.equal(dividendAmount2[0].toNumber(), 0); + }); + + it("Issuer reclaims withholding tax", async() => { + let issuerBalance = BigNumber(await I_PolyToken.balanceOf(token_owner)); + await I_ERC20DividendCheckpoint.withdrawWithholding(3, {from: token_owner, gasPrice: 0}); + let issuerBalanceAfter = BigNumber(await I_PolyToken.balanceOf(token_owner)); + assert.equal(issuerBalanceAfter.sub(issuerBalance).toNumber(), web3.utils.toWei('0.4', 'ether')) + }); it("Issuer unable to reclaim dividend (expiry not passed)", async() => { let errorThrown = false; diff --git a/test/f_ether_dividends.js b/test/f_ether_dividends.js index 93aeb8a8f..4d58e26c8 100644 --- a/test/f_ether_dividends.js +++ b/test/f_ether_dividends.js @@ -189,7 +189,7 @@ contract('EtherDividendCheckpoint', accounts => { I_SecurityTokenRegistryProxy = await SecurityTokenRegistryProxy.new({from: account_polymath}); let bytesProxy = encodeProxyCall([I_PolymathRegistry.address, I_STFactory.address, initRegFee, initRegFee, I_PolyToken.address, account_polymath]); await I_SecurityTokenRegistryProxy.upgradeToAndCall("1.0.0", I_SecurityTokenRegistry.address, bytesProxy, {from: account_polymath}); - I_STRProxied = await SecurityTokenRegistry.at(I_SecurityTokenRegistryProxy.address); + I_STRProxied = await SecurityTokenRegistry.at(I_SecurityTokenRegistryProxy.address); // Step 10: Deploy the FeatureRegistry @@ -402,6 +402,10 @@ contract('EtherDividendCheckpoint', accounts => { assert.ok(errorThrown, message); }); + it("Set withholding tax of 20% on investor 2", async() => { + await I_EtherDividendCheckpoint.setWithholdingFixed([account_investor2], BigNumber(20*10**16), {from: token_owner}); + }); + it("Create new dividend", async() => { let maturity = latestTime() + duration.days(1); let expiry = latestTime() + duration.days(10); @@ -460,11 +464,25 @@ contract('EtherDividendCheckpoint', accounts => { let investor1BalanceAfter = BigNumber(await web3.eth.getBalance(account_investor1)); let investor2BalanceAfter = BigNumber(await web3.eth.getBalance(account_investor2)); assert.equal(investor1BalanceAfter.sub(investor1Balance).toNumber(), web3.utils.toWei('0.5', 'ether')); - assert.equal(investor2BalanceAfter.sub(investor2Balance).toNumber(), web3.utils.toWei('1', 'ether')); + assert.equal(investor2BalanceAfter.sub(investor2Balance).toNumber(), web3.utils.toWei('0.8', 'ether')); //Check fully claimed assert.equal((await I_EtherDividendCheckpoint.dividends(0))[5].toNumber(), web3.utils.toWei('1.5', 'ether')); }); + it("Issuer reclaims withholding tax", async() => { + let issuerBalance = BigNumber(await web3.eth.getBalance(token_owner)); + await I_EtherDividendCheckpoint.withdrawWithholding(0, {from: token_owner, gasPrice: 0}); + let issuerBalanceAfter = BigNumber(await web3.eth.getBalance(token_owner)); + assert.equal(issuerBalanceAfter.sub(issuerBalance).toNumber(), web3.utils.toWei('0.2', 'ether')) + }); + + it("No more withholding tax to withdraw", async() => { + let issuerBalance = BigNumber(await web3.eth.getBalance(token_owner)); + await I_EtherDividendCheckpoint.withdrawWithholding(0, {from: token_owner, gasPrice: 0}); + let issuerBalanceAfter = BigNumber(await web3.eth.getBalance(token_owner)); + assert.equal(issuerBalanceAfter.sub(issuerBalance).toNumber(), web3.utils.toWei('0', 'ether')) + }); + it("Buy some tokens for account_temp (1 ETH)", async() => { // Add the Investor in to the whitelist @@ -494,10 +512,10 @@ contract('EtherDividendCheckpoint', accounts => { let maturity = latestTime() + duration.days(1); let expiry = latestTime() + duration.days(10); let tx = await I_EtherDividendCheckpoint.createDividend(maturity, expiry, {from: token_owner, value: web3.utils.toWei('1.5', 'ether')}); - assert.equal(tx.logs[0].args._checkpointId.toNumber(), 2, "Dividend should be created at checkpoint 1"); + assert.equal(tx.logs[0].args._checkpointId.toNumber(), 2, "Dividend should be created at checkpoint 2"); }); - it("Issuer pushes dividends iterating over account holders - dividends proportional to checkpoint", async() => { + it("Issuer pushes dividends fails due to passed expiry", async() => { let errorThrown = false; await increaseTime(duration.days(12)); try { @@ -510,7 +528,7 @@ contract('EtherDividendCheckpoint', accounts => { assert.ok(errorThrown, message); }); - it("Issuer pushes dividends iterating over account holders - dividends proportional to checkpoint", async() => { + it("Issuer reclaims dividend", async() => { let errorThrown = false; let tx = await I_EtherDividendCheckpoint.reclaimDividend(1, {from: token_owner, gas: 500000}); assert.equal((tx.logs[0].args._claimedAmount).toNumber(), web3.utils.toWei("1.5", "ether")); @@ -524,6 +542,13 @@ contract('EtherDividendCheckpoint', accounts => { assert.ok(errorThrown, message); }); + it("Still no more withholding tax to withdraw", async() => { + let issuerBalance = BigNumber(await web3.eth.getBalance(token_owner)); + await I_EtherDividendCheckpoint.withdrawWithholding(0, {from: token_owner, gasPrice: 0}); + let issuerBalanceAfter = BigNumber(await web3.eth.getBalance(token_owner)); + assert.equal(issuerBalanceAfter.sub(issuerBalance).toNumber(), web3.utils.toWei('0', 'ether')) + }); + it("Buy some tokens for account_investor3 (7 ETH)", async() => { // Add the Investor in to the whitelist @@ -553,10 +578,10 @@ contract('EtherDividendCheckpoint', accounts => { let maturity = latestTime(); let expiry = latestTime() + duration.days(10); let tx = await I_EtherDividendCheckpoint.createDividend(maturity, expiry, {from: token_owner, value: web3.utils.toWei('11', 'ether')}); - assert.equal(tx.logs[0].args._checkpointId.toNumber(), 3, "Dividend should be created at checkpoint 2"); + assert.equal(tx.logs[0].args._checkpointId.toNumber(), 3, "Dividend should be created at checkpoint 3"); }); - it("should investor 3 claims dividend", async() => { + it("should investor 3 claims dividend - fails bad index", async() => { let errorThrown = false; let investor1Balance = BigNumber(await I_PolyToken.balanceOf(account_investor1)); let investor2Balance = BigNumber(await I_PolyToken.balanceOf(account_investor2)); @@ -584,6 +609,13 @@ contract('EtherDividendCheckpoint', accounts => { assert.equal(investor3BalanceAfter1.sub(investor3Balance).toNumber(), web3.utils.toWei('7', 'ether')); }); + it("Still no more withholding tax to withdraw", async() => { + let issuerBalance = BigNumber(await web3.eth.getBalance(token_owner)); + await I_EtherDividendCheckpoint.withdrawWithholding(0, {from: token_owner, gasPrice: 0}); + let issuerBalanceAfter = BigNumber(await web3.eth.getBalance(token_owner)); + assert.equal(issuerBalanceAfter.sub(issuerBalance).toNumber(), web3.utils.toWei('0', 'ether')) + }); + it("should investor 3 claims dividend", async() => { let errorThrown = false; try { @@ -605,12 +637,19 @@ contract('EtherDividendCheckpoint', accounts => { let investor2BalanceAfter2 = BigNumber(await web3.eth.getBalance(account_investor2)); let investor3BalanceAfter2 = BigNumber(await web3.eth.getBalance(account_investor3)); assert.equal(investor1BalanceAfter2.sub(investor1BalanceAfter1).toNumber(), 0); - assert.equal(investor2BalanceAfter2.sub(investor2BalanceAfter1).toNumber(), web3.utils.toWei('3', 'ether')); + assert.equal(investor2BalanceAfter2.sub(investor2BalanceAfter1).toNumber(), web3.utils.toWei('2.4', 'ether')); assert.equal(investor3BalanceAfter2.sub(investor3BalanceAfter1).toNumber(), 0); //Check fully claimed assert.equal((await I_EtherDividendCheckpoint.dividends(2))[5].toNumber(), web3.utils.toWei('11', 'ether')); }); + it("Issuer withdraws new withholding tax", async() => { + let issuerBalance = BigNumber(await web3.eth.getBalance(token_owner)); + await I_EtherDividendCheckpoint.withdrawWithholding(2, {from: token_owner, gasPrice: 0}); + let issuerBalanceAfter = BigNumber(await web3.eth.getBalance(token_owner)); + assert.equal(issuerBalanceAfter.sub(issuerBalance).toNumber(), web3.utils.toWei('0.6', 'ether')) + }); + it("Investor 2 transfers 1 ETH of his token balance to investor 1", async() => { await I_SecurityToken.transfer(account_investor1, web3.utils.toWei('1', 'ether'), {from: account_investor2}); assert.equal(await I_SecurityToken.balanceOf(account_investor1), web3.utils.toWei('1', 'ether')); @@ -618,7 +657,7 @@ contract('EtherDividendCheckpoint', accounts => { assert.equal(await I_SecurityToken.balanceOf(account_investor3), web3.utils.toWei('7', 'ether')); }); - it("Create another new dividend with explicit", async() => { + it("Create another new dividend with no value - fails", async() => { let errorThrown = false; let maturity = latestTime(); let expiry = latestTime() + duration.days(2); @@ -647,7 +686,7 @@ contract('EtherDividendCheckpoint', accounts => { assert.ok(errorThrown, message); }); - it("Create another new dividend with explicit", async() => { + it("Create another new dividend with bad expirty - fails", async() => { let errorThrown = false; let maturity = latestTime() - duration.days(5); let expiry = latestTime() - duration.days(2); @@ -661,7 +700,7 @@ contract('EtherDividendCheckpoint', accounts => { assert.ok(errorThrown, message); }); - it("Create another new dividend with explicit", async() => { + it("Create another new dividend with bad checkpoint in the future - fails", async() => { let errorThrown = false; let maturity = latestTime(); let expiry = latestTime() + duration.days(2); @@ -675,21 +714,21 @@ contract('EtherDividendCheckpoint', accounts => { assert.ok(errorThrown, message); }); - it("Create another new dividend with explicit", async() => { + it("Create another new dividend with explicit checkpoint and excluding account_investor1", async() => { let maturity = latestTime(); let expiry = latestTime() + duration.days(10); let tx = await I_SecurityToken.createCheckpoint({from: token_owner}); - tx = await I_EtherDividendCheckpoint.createDividendWithCheckpoint(maturity, expiry, 4, {from: token_owner, value: web3.utils.toWei('11', 'ether')}); - assert.equal(tx.logs[0].args._checkpointId.toNumber(), 4, "Dividend should be created at checkpoint 3"); + tx = await I_EtherDividendCheckpoint.createDividendWithCheckpointAndExclusions(maturity, expiry, 4, [account_investor1], {from: token_owner, value: web3.utils.toWei('10', 'ether')}); + assert.equal(tx.logs[0].args._checkpointId.toNumber(), 4, "Dividend should be created at checkpoint 4"); }); - it("Investor 2 claims dividend, issuer pushes investor 1", async() => { + it("Non-owner pushes investor 1 - fails", async() => { let errorThrown = false; let investor1Balance = BigNumber(await I_PolyToken.balanceOf(account_investor1)); let investor2Balance = BigNumber(await I_PolyToken.balanceOf(account_investor2)); let investor3Balance = BigNumber(await I_PolyToken.balanceOf(account_investor3)); try { - await I_EtherDividendCheckpoint.pushDividendPaymentToAddresses(4, [account_investor2, account_investor1],{from: account_investor2, gasPrice: 0}); + await I_EtherDividendCheckpoint.pushDividendPaymentToAddresses(3, [account_investor2, account_investor1],{from: account_investor2, gasPrice: 0}); } catch(error) { console.log(` tx -> failed because not called by the owner`.grey); ensureException(error); @@ -698,7 +737,7 @@ contract('EtherDividendCheckpoint', accounts => { assert.ok(errorThrown, message); }); - it("Investor 2 claims dividend, issuer pushes investor 1", async() => { + it("issuer pushes investor 1 with bad dividend index - fails", async() => { let errorThrown = false; let investor1Balance = BigNumber(await I_PolyToken.balanceOf(account_investor1)); let investor2Balance = BigNumber(await I_PolyToken.balanceOf(account_investor2)); @@ -718,13 +757,17 @@ contract('EtherDividendCheckpoint', accounts => { let dividendAmount2 = await I_EtherDividendCheckpoint.calculateDividend.call(3, account_investor2); let dividendAmount3 = await I_EtherDividendCheckpoint.calculateDividend.call(3, account_investor3); let dividendAmount_temp = await I_EtherDividendCheckpoint.calculateDividend.call(3, account_temp); - assert.equal(dividendAmount1.toNumber(), web3.utils.toWei("1", "ether")); - assert.equal(dividendAmount2.toNumber(), web3.utils.toWei("2", "ether")); - assert.equal(dividendAmount3.toNumber(), web3.utils.toWei("7", "ether")); - assert.equal(dividendAmount_temp.toNumber(), web3.utils.toWei("1", "ether")); + //1 has 1/11th, 2 has 2/11th, 3 has 7/11th, temp has 1/11th, but 1 is excluded + assert.equal(dividendAmount1[0].toNumber(), web3.utils.toWei("0", "ether")); + assert.equal(dividendAmount1[1].toNumber(), web3.utils.toWei("0", "ether")); + assert.equal(dividendAmount2[0].toNumber(), web3.utils.toWei("2", "ether")); + assert.equal(dividendAmount2[1].toNumber(), web3.utils.toWei("0.4", "ether")); + assert.equal(dividendAmount3[0].toNumber(), web3.utils.toWei("7", "ether")); + assert.equal(dividendAmount3[1].toNumber(), web3.utils.toWei("0", "ether")); + assert.equal(dividendAmount_temp[0].toNumber(), web3.utils.toWei("1", "ether")); + assert.equal(dividendAmount_temp[1].toNumber(), web3.utils.toWei("0", "ether")); }); - it("Investor 2 claims dividend", async() => { let investor1Balance = BigNumber(await web3.eth.getBalance(account_investor1)); let investor2Balance = BigNumber(await web3.eth.getBalance(account_investor2)); @@ -736,13 +779,11 @@ contract('EtherDividendCheckpoint', accounts => { let investor3BalanceAfter1 = BigNumber(await web3.eth.getBalance(account_investor3)); let tempBalanceAfter1 = BigNumber(await web3.eth.getBalance(account_temp)); assert.equal(investor1BalanceAfter1.sub(investor1Balance).toNumber(), 0); - assert.equal(investor2BalanceAfter1.sub(investor2Balance).toNumber(), web3.utils.toWei('2', 'ether')); + assert.equal(investor2BalanceAfter1.sub(investor2Balance).toNumber(), web3.utils.toWei('1.6', 'ether')); assert.equal(investor3BalanceAfter1.sub(investor3Balance).toNumber(), 0); assert.equal(tempBalanceAfter1.sub(tempBalance).toNumber(), 0); - }); - it("Should issuer pushes investor 1 and temp investor", async() => { let investor1BalanceAfter1 = BigNumber(await web3.eth.getBalance(account_investor1)); let investor2BalanceAfter1 = BigNumber(await web3.eth.getBalance(account_investor2)); @@ -753,19 +794,19 @@ contract('EtherDividendCheckpoint', accounts => { let investor2BalanceAfter2 = BigNumber(await web3.eth.getBalance(account_investor2)); let investor3BalanceAfter2 = BigNumber(await web3.eth.getBalance(account_investor3)); let tempBalanceAfter2 = BigNumber(await web3.eth.getBalance(account_temp)); - assert.equal(investor1BalanceAfter2.sub(investor1BalanceAfter1).toNumber(), web3.utils.toWei('1', 'ether')); + assert.equal(investor1BalanceAfter2.sub(investor1BalanceAfter1).toNumber(), 0); assert.equal(investor2BalanceAfter2.sub(investor2BalanceAfter1).toNumber(), 0); assert.equal(investor3BalanceAfter2.sub(investor3BalanceAfter1).toNumber(), 0); assert.equal(tempBalanceAfter2.sub(tempBalanceAfter1).toNumber(), web3.utils.toWei('1', 'ether')); //Check fully claimed - assert.equal((await I_EtherDividendCheckpoint.dividends(3))[5].toNumber(), web3.utils.toWei('4', 'ether')); + assert.equal((await I_EtherDividendCheckpoint.dividends(3))[5].toNumber(), web3.utils.toWei('3', 'ether')); }); it("should calculate dividend after the push dividend payment", async() => { let dividendAmount1 = await I_EtherDividendCheckpoint.calculateDividend.call(3, account_investor1); let dividendAmount2 = await I_EtherDividendCheckpoint.calculateDividend.call(3, account_investor2); - assert.equal(dividendAmount1.toNumber(), 0); - assert.equal(dividendAmount2.toNumber(), 0); + assert.equal(dividendAmount1[0].toNumber(), 0); + assert.equal(dividendAmount2[0].toNumber(), 0); }); it("Issuer unable to reclaim dividend (expiry not passed)", async() => { @@ -822,7 +863,61 @@ contract('EtherDividendCheckpoint', accounts => { ensureException(error); } assert.ok(errorThrown, message); + }); + + it("Assign token balance to an address that can't receive funds", async() => { + let tx = await I_GeneralTransferManager.modifyWhitelist( + I_PolyToken.address, + latestTime(), + latestTime(), + latestTime() + duration.days(10), + true, + { + from: account_issuer, + gas: 500000 + }); + // Jump time + await increaseTime(5000); + // Mint some tokens + await I_SecurityToken.mint(I_PolyToken.address, web3.utils.toWei('1', 'ether'), { from: token_owner }); + assert.equal(await I_SecurityToken.balanceOf(account_investor1), web3.utils.toWei('1', 'ether')); + assert.equal(await I_SecurityToken.balanceOf(account_investor2), web3.utils.toWei('2', 'ether')); + assert.equal(await I_SecurityToken.balanceOf(account_investor3), web3.utils.toWei('7', 'ether')); + assert.equal(await I_SecurityToken.balanceOf(account_temp), web3.utils.toWei('1', 'ether')); + assert.equal(await I_SecurityToken.balanceOf(I_PolyToken.address), web3.utils.toWei('1', 'ether')); + }); + + it("Create another new dividend", async() => { + let maturity = latestTime(); + let expiry = latestTime() + duration.days(10); + let tx = await I_EtherDividendCheckpoint.createDividendWithExclusions(maturity, expiry, [], {from: token_owner, value: web3.utils.toWei('12', 'ether')}); + assert.equal(tx.logs[0].args._checkpointId.toNumber(), 6, "Dividend should be created at checkpoint 6"); + }); + + it("Should issuer pushes all dividends", async() => { + let investor1BalanceBefore = BigNumber(await web3.eth.getBalance(account_investor1)); + let investor2BalanceBefore = BigNumber(await web3.eth.getBalance(account_investor2)); + let investor3BalanceBefore = BigNumber(await web3.eth.getBalance(account_investor3)); + let tempBalanceBefore = BigNumber(await web3.eth.getBalance(account_temp)); + let tokenBalanceBefore = BigNumber(await web3.eth.getBalance(I_PolyToken.address)); + + await I_EtherDividendCheckpoint.pushDividendPayment(4, 0, 10, {from: token_owner}); + + let investor1BalanceAfter = BigNumber(await web3.eth.getBalance(account_investor1)); + let investor2BalanceAfter = BigNumber(await web3.eth.getBalance(account_investor2)); + let investor3BalanceAfter = BigNumber(await web3.eth.getBalance(account_investor3)); + let tempBalanceAfter = BigNumber(await web3.eth.getBalance(account_temp)); + let tokenBalanceAfter = BigNumber(await web3.eth.getBalance(I_PolyToken.address)); + + assert.equal(investor1BalanceAfter.sub(investor1BalanceBefore).toNumber(), web3.utils.toWei('1', 'ether')); + assert.equal(investor2BalanceAfter.sub(investor2BalanceBefore).toNumber(), web3.utils.toWei('1.6', 'ether')); + assert.equal(investor3BalanceAfter.sub(investor3BalanceBefore).toNumber(), web3.utils.toWei('7', 'ether')); + assert.equal(tempBalanceAfter.sub(tempBalanceBefore).toNumber(), web3.utils.toWei('1', 'ether')); + assert.equal(tokenBalanceAfter.sub(tokenBalanceBefore).toNumber(), web3.utils.toWei('0', 'ether')); + + //Check partially claimed + assert.equal((await I_EtherDividendCheckpoint.dividends(4))[5].toNumber(), web3.utils.toWei('11', 'ether')); }); it("Should give the right dividend index", async() => {