diff --git a/contracts/modules/Wallet/IWallet.sol b/contracts/modules/Wallet/IWallet.sol new file mode 100644 index 000000000..ea5c918d8 --- /dev/null +++ b/contracts/modules/Wallet/IWallet.sol @@ -0,0 +1,19 @@ +pragma solidity ^0.4.24; + +import "../../Pausable.sol"; +import "../Module.sol"; + +/** + * @title Interface to be implemented by all Wallet modules + * @dev abstract contract + */ +contract IWallet is Module, Pausable { + + function unpause() public onlyOwner { + super._unpause(); + } + + function pause() public onlyOwner { + super._pause(); + } +} diff --git a/contracts/modules/Wallet/VestingEscrowWallet.sol b/contracts/modules/Wallet/VestingEscrowWallet.sol new file mode 100644 index 000000000..17f6dbb08 --- /dev/null +++ b/contracts/modules/Wallet/VestingEscrowWallet.sol @@ -0,0 +1,558 @@ +pragma solidity ^0.4.24; + +import "openzeppelin-solidity/contracts/math/SafeMath.sol"; +import "./VestingEscrowWalletStorage.sol"; +import "./IWallet.sol"; + +/** + * @title Wallet for core vesting escrow functionality + */ +contract VestingEscrowWallet is VestingEscrowWalletStorage, IWallet { + using SafeMath for uint256; + + bytes32 public constant ADMIN = "ADMIN"; + + // States used to represent the status of the schedule + enum State {CREATED, STARTED, COMPLETED} + + // Emit when new schedule is added + event AddSchedule( + address indexed _beneficiary, + bytes32 _templateName, + uint256 _startTime + ); + // Emit when schedule is modified + event ModifySchedule( + address indexed _beneficiary, + bytes32 _templateName, + uint256 _startTime + ); + // Emit when all schedules are revoked for user + event RevokeAllSchedules(address indexed _beneficiary); + // Emit when schedule is revoked + event RevokeSchedule(address indexed _beneficiary, bytes32 _templateName); + // Emit when tokes are deposited to wallet + event DepositTokens(uint256 _numberOfTokens, address _sender); + // Emit when all unassigned tokens are sent to treasury + event SendToTreasury(uint256 _numberOfTokens, address _sender); + // Emit when is sent tokes to user + event SendTokens(address indexed _beneficiary, uint256 _numberOfTokens); + // Emit when template is added + event AddTemplate(bytes32 _name, uint256 _numberOfTokens, uint256 _duration, uint256 _frequency); + // Emit when template is removed + event RemoveTemplate(bytes32 _name); + // Emit when the treasury wallet gets changed + event TreasuryWalletChanged(address _newWallet, address _oldWallet); + + /** + * @notice Constructor + * @param _securityToken Address of the security token + * @param _polyAddress Address of the polytoken + */ + constructor (address _securityToken, address _polyAddress) + public + Module(_securityToken, _polyAddress) + { + } + + /** + * @notice This function returns the signature of the configure function + */ + function getInitFunction() public pure returns (bytes4) { + return bytes4(keccak256("configure(address)")); + } + + /** + * @notice Used to initialize the treasury wallet address + * @param _treasuryWallet Address of the treasury wallet + */ + function configure(address _treasuryWallet) public onlyFactory { + require(_treasuryWallet != address(0), "Invalid address"); + treasuryWallet = _treasuryWallet; + } + + /** + * @notice Used to change the treasury wallet address + * @param _newTreasuryWallet Address of the treasury wallet + */ + function changeTreasuryWallet(address _newTreasuryWallet) public onlyOwner { + require(_newTreasuryWallet != address(0)); + emit TreasuryWalletChanged(_newTreasuryWallet, treasuryWallet); + treasuryWallet = _newTreasuryWallet; + } + + /** + * @notice Used to deposit tokens from treasury wallet to the vesting escrow wallet + * @param _numberOfTokens Number of tokens that should be deposited + */ + function depositTokens(uint256 _numberOfTokens) external withPerm(ADMIN) { + _depositTokens(_numberOfTokens); + } + + function _depositTokens(uint256 _numberOfTokens) internal { + require(_numberOfTokens > 0, "Should be > 0"); + require( + ISecurityToken(securityToken).transferFrom(msg.sender, address(this), _numberOfTokens), + "Failed transferFrom due to insufficent Allowance provided" + ); + unassignedTokens = unassignedTokens.add(_numberOfTokens); + emit DepositTokens(_numberOfTokens, msg.sender); + } + + /** + * @notice Sends unassigned tokens to the treasury wallet + * @param _amount Amount of tokens that should be send to the treasury wallet + */ + function sendToTreasury(uint256 _amount) external withPerm(ADMIN) { + require(_amount > 0, "Amount cannot be zero"); + require(_amount <= unassignedTokens, "Amount is greater than unassigned tokens"); + uint256 amount = unassignedTokens; + unassignedTokens = 0; + require(ISecurityToken(securityToken).transfer(treasuryWallet, amount), "Transfer failed"); + emit SendToTreasury(amount, msg.sender); + } + + /** + * @notice Pushes available tokens to the beneficiary's address + * @param _beneficiary Address of the beneficiary who will receive tokens + */ + function pushAvailableTokens(address _beneficiary) public withPerm(ADMIN) { + _sendTokens(_beneficiary); + } + + /** + * @notice Used to withdraw available tokens by beneficiary + */ + function pullAvailableTokens() external { + _sendTokens(msg.sender); + } + + /** + * @notice Adds template that can be used for creating schedule + * @param _name Name of the template will be created + * @param _numberOfTokens Number of tokens that should be assigned to schedule + * @param _duration Duration of the vesting schedule + * @param _frequency Frequency of the vesting schedule + */ + function addTemplate(bytes32 _name, uint256 _numberOfTokens, uint256 _duration, uint256 _frequency) external withPerm(ADMIN) { + _addTemplate(_name, _numberOfTokens, _duration, _frequency); + } + + function _addTemplate(bytes32 _name, uint256 _numberOfTokens, uint256 _duration, uint256 _frequency) internal { + require(_name != bytes32(0), "Invalid name"); + require(!_isTemplateExists(_name), "Already exists"); + _validateTemplate(_numberOfTokens, _duration, _frequency); + templateNames.push(_name); + templates[_name] = Template(_numberOfTokens, _duration, _frequency, templateNames.length - 1); + emit AddTemplate(_name, _numberOfTokens, _duration, _frequency); + } + + /** + * @notice Removes template with a given name + * @param _name Name of the template that will be removed + */ + function removeTemplate(bytes32 _name) external withPerm(ADMIN) { + require(_isTemplateExists(_name), "Template not found"); + require(templateToUsers[_name].length == 0, "Template is used"); + uint256 index = templates[_name].index; + if (index != templateNames.length - 1) { + templateNames[index] = templateNames[templateNames.length - 1]; + templates[templateNames[index]].index = index; + } + templateNames.length--; + // delete template data + delete templates[_name]; + emit RemoveTemplate(_name); + } + + /** + * @notice Returns count of the templates those can be used for creating schedule + * @return Count of the templates + */ + function getTemplateCount() external view returns(uint256) { + return templateNames.length; + } + + /** + * @notice Gets the list of the template names those can be used for creating schedule + * @return bytes32 Array of all template names were created + */ + function getAllTemplateNames() external view returns(bytes32[]) { + return templateNames; + } + + /** + * @notice Adds vesting schedules for each of the beneficiary's address + * @param _beneficiary Address of the beneficiary for whom it is scheduled + * @param _templateName Name of the template that will be created + * @param _numberOfTokens Total number of tokens for created schedule + * @param _duration Duration of the created vesting schedule + * @param _frequency Frequency of the created vesting schedule + * @param _startTime Start time of the created vesting schedule + */ + function addSchedule( + address _beneficiary, + bytes32 _templateName, + uint256 _numberOfTokens, + uint256 _duration, + uint256 _frequency, + uint256 _startTime + ) + external + withPerm(ADMIN) + { + _addSchedule(_beneficiary, _templateName, _numberOfTokens, _duration, _frequency, _startTime); + } + + function _addSchedule( + address _beneficiary, + bytes32 _templateName, + uint256 _numberOfTokens, + uint256 _duration, + uint256 _frequency, + uint256 _startTime + ) + internal + { + _addTemplate(_templateName, _numberOfTokens, _duration, _frequency); + _addScheduleFromTemplate(_beneficiary, _templateName, _startTime); + } + + /** + * @notice Adds vesting schedules from template for the beneficiary + * @param _beneficiary Address of the beneficiary for whom it is scheduled + * @param _templateName Name of the exists template + * @param _startTime Start time of the created vesting schedule + */ + function addScheduleFromTemplate(address _beneficiary, bytes32 _templateName, uint256 _startTime) external withPerm(ADMIN) { + _addScheduleFromTemplate(_beneficiary, _templateName, _startTime); + } + + function _addScheduleFromTemplate(address _beneficiary, bytes32 _templateName, uint256 _startTime) internal { + require(_beneficiary != address(0), "Invalid address"); + require(_isTemplateExists(_templateName), "Template not found"); + uint256 index = userToTemplateIndex[_beneficiary][_templateName]; + require( + schedules[_beneficiary].length == 0 || + schedules[_beneficiary][index].templateName != _templateName, + "Already added" + ); + require(_startTime >= now, "Date in the past"); + uint256 numberOfTokens = templates[_templateName].numberOfTokens; + if (numberOfTokens > unassignedTokens) { + _depositTokens(numberOfTokens.sub(unassignedTokens)); + } + unassignedTokens = unassignedTokens.sub(numberOfTokens); + if (!beneficiaryAdded[_beneficiary]) { + beneficiaries.push(_beneficiary); + beneficiaryAdded[_beneficiary] = true; + } + schedules[_beneficiary].push(Schedule(_templateName, 0, _startTime)); + userToTemplates[_beneficiary].push(_templateName); + userToTemplateIndex[_beneficiary][_templateName] = schedules[_beneficiary].length - 1; + templateToUsers[_templateName].push(_beneficiary); + templateToUserIndex[_templateName][_beneficiary] = templateToUsers[_templateName].length - 1; + emit AddSchedule(_beneficiary, _templateName, _startTime); + } + + /** + * @notice Modifies vesting schedules for each of the beneficiary + * @param _beneficiary Address of the beneficiary for whom it is modified + * @param _templateName Name of the template was used for schedule creation + * @param _startTime Start time of the created vesting schedule + */ + function modifySchedule(address _beneficiary, bytes32 _templateName, uint256 _startTime) public withPerm(ADMIN) { + _modifySchedule(_beneficiary, _templateName, _startTime); + } + + function _modifySchedule(address _beneficiary, bytes32 _templateName, uint256 _startTime) internal { + _checkSchedule(_beneficiary, _templateName); + require(_startTime > now, "Date in the past"); + uint256 index = userToTemplateIndex[_beneficiary][_templateName]; + Schedule storage schedule = schedules[_beneficiary][index]; + /*solium-disable-next-line security/no-block-members*/ + require(now < schedule.startTime, "Schedule started"); + schedule.startTime = _startTime; + emit ModifySchedule(_beneficiary, _templateName, _startTime); + } + + /** + * @notice Revokes vesting schedule with given template name for given beneficiary + * @param _beneficiary Address of the beneficiary for whom it is revoked + * @param _templateName Name of the template was used for schedule creation + */ + function revokeSchedule(address _beneficiary, bytes32 _templateName) external withPerm(ADMIN) { + _checkSchedule(_beneficiary, _templateName); + uint256 index = userToTemplateIndex[_beneficiary][_templateName]; + _sendTokensPerSchedule(_beneficiary, index); + uint256 releasedTokens = _getReleasedTokens(_beneficiary, index); + unassignedTokens = unassignedTokens.add(templates[_templateName].numberOfTokens.sub(releasedTokens)); + _deleteUserToTemplates(_beneficiary, _templateName); + _deleteTemplateToUsers(_beneficiary, _templateName); + emit RevokeSchedule(_beneficiary, _templateName); + } + + function _deleteUserToTemplates(address _beneficiary, bytes32 _templateName) internal { + uint256 index = userToTemplateIndex[_beneficiary][_templateName]; + Schedule[] storage userSchedules = schedules[_beneficiary]; + if (index != userSchedules.length - 1) { + userSchedules[index] = userSchedules[userSchedules.length - 1]; + userToTemplates[_beneficiary][index] = userToTemplates[_beneficiary][userToTemplates[_beneficiary].length - 1]; + userToTemplateIndex[_beneficiary][userSchedules[index].templateName] = index; + } + userSchedules.length--; + userToTemplates[_beneficiary].length--; + delete userToTemplateIndex[_beneficiary][_templateName]; + } + + function _deleteTemplateToUsers(address _beneficiary, bytes32 _templateName) internal { + uint256 templateIndex = templateToUserIndex[_templateName][_beneficiary]; + if (templateIndex != templateToUsers[_templateName].length - 1) { + templateToUsers[_templateName][templateIndex] = templateToUsers[_templateName][templateToUsers[_templateName].length - 1]; + templateToUserIndex[_templateName][templateToUsers[_templateName][templateIndex]] = templateIndex; + } + templateToUsers[_templateName].length--; + delete templateToUserIndex[_templateName][_beneficiary]; + } + + /** + * @notice Revokes all vesting schedules for given beneficiary's address + * @param _beneficiary Address of the beneficiary for whom all schedules will be revoked + */ + function revokeAllSchedules(address _beneficiary) public withPerm(ADMIN) { + _revokeAllSchedules(_beneficiary); + } + + function _revokeAllSchedules(address _beneficiary) internal { + require(_beneficiary != address(0), "Invalid address"); + _sendTokens(_beneficiary); + Schedule[] storage userSchedules = schedules[_beneficiary]; + for (uint256 i = 0; i < userSchedules.length; i++) { + uint256 releasedTokens = _getReleasedTokens(_beneficiary, i); + Template memory template = templates[userSchedules[i].templateName]; + unassignedTokens = unassignedTokens.add(template.numberOfTokens.sub(releasedTokens)); + delete userToTemplateIndex[_beneficiary][userSchedules[i].templateName]; + _deleteTemplateToUsers(_beneficiary, userSchedules[i].templateName); + } + delete schedules[_beneficiary]; + delete userToTemplates[_beneficiary]; + emit RevokeAllSchedules(_beneficiary); + } + + /** + * @notice Returns beneficiary's schedule created using template name + * @param _beneficiary Address of the beneficiary who will receive tokens + * @param _templateName Name of the template was used for schedule creation + * @return beneficiary's schedule data (numberOfTokens, duration, frequency, startTime, claimedTokens, State) + */ + function getSchedule(address _beneficiary, bytes32 _templateName) external view returns(uint256, uint256, uint256, uint256, uint256, State) { + _checkSchedule(_beneficiary, _templateName); + uint256 index = userToTemplateIndex[_beneficiary][_templateName]; + Schedule memory schedule = schedules[_beneficiary][index]; + return ( + templates[schedule.templateName].numberOfTokens, + templates[schedule.templateName].duration, + templates[schedule.templateName].frequency, + schedule.startTime, + schedule.claimedTokens, + _getScheduleState(_beneficiary, _templateName) + ); + } + + function _getScheduleState(address _beneficiary, bytes32 _templateName) internal view returns(State) { + _checkSchedule(_beneficiary, _templateName); + uint256 index = userToTemplateIndex[_beneficiary][_templateName]; + Schedule memory schedule = schedules[_beneficiary][index]; + if (now < schedule.startTime) { + return State.CREATED; + } else if (now > schedule.startTime && now < schedule.startTime.add(templates[_templateName].duration)) { + return State.STARTED; + } else { + return State.COMPLETED; + } + } + + /** + * @notice Returns list of the template names for given beneficiary's address + * @param _beneficiary Address of the beneficiary + * @return List of the template names that were used for schedule creation + */ + function getTemplateNames(address _beneficiary) external view returns(bytes32[]) { + require(_beneficiary != address(0), "Invalid address"); + return userToTemplates[_beneficiary]; + } + + /** + * @notice Returns count of the schedules were created for given beneficiary + * @param _beneficiary Address of the beneficiary + * @return Count of beneficiary's schedules + */ + function getScheduleCount(address _beneficiary) external view returns(uint256) { + require(_beneficiary != address(0), "Invalid address"); + return schedules[_beneficiary].length; + } + + function _getAvailableTokens(address _beneficiary, uint256 _index) internal view returns(uint256) { + Schedule memory schedule = schedules[_beneficiary][_index]; + uint256 releasedTokens = _getReleasedTokens(_beneficiary, _index); + return releasedTokens.sub(schedule.claimedTokens); + } + + function _getReleasedTokens(address _beneficiary, uint256 _index) internal view returns(uint256) { + Schedule memory schedule = schedules[_beneficiary][_index]; + Template memory template = templates[schedule.templateName]; + /*solium-disable-next-line security/no-block-members*/ + if (now > schedule.startTime) { + uint256 periodCount = template.duration.div(template.frequency); + /*solium-disable-next-line security/no-block-members*/ + uint256 periodNumber = (now.sub(schedule.startTime)).div(template.frequency); + if (periodNumber > periodCount) { + periodNumber = periodCount; + } + return template.numberOfTokens.mul(periodNumber).div(periodCount); + } else { + return 0; + } + } + + /** + * @notice Used to bulk send available tokens for each of the beneficiaries + * @param _fromIndex Start index of array of beneficiary's addresses + * @param _toIndex End index of array of beneficiary's addresses + */ + function pushAvailableTokensMulti(uint256 _fromIndex, uint256 _toIndex) external withPerm(ADMIN) { + require(_toIndex <= beneficiaries.length - 1, "Array out of bound"); + for (uint256 i = _fromIndex; i <= _toIndex; i++) { + if (schedules[beneficiaries[i]].length !=0) + pushAvailableTokens(beneficiaries[i]); + } + } + + /** + * @notice Used to bulk add vesting schedules for each of beneficiary + * @param _beneficiaries Array of the beneficiary's addresses + * @param _templateNames Array of the template names + * @param _numberOfTokens Array of number of tokens should be assigned to schedules + * @param _durations Array of the vesting duration + * @param _frequencies Array of the vesting frequency + * @param _startTimes Array of the vesting start time + */ + function addScheduleMulti( + address[] _beneficiaries, + bytes32[] _templateNames, + uint256[] _numberOfTokens, + uint256[] _durations, + uint256[] _frequencies, + uint256[] _startTimes + ) + public + withPerm(ADMIN) + { + require( + _beneficiaries.length == _templateNames.length && /*solium-disable-line operator-whitespace*/ + _beneficiaries.length == _numberOfTokens.length && /*solium-disable-line operator-whitespace*/ + _beneficiaries.length == _durations.length && /*solium-disable-line operator-whitespace*/ + _beneficiaries.length == _frequencies.length && /*solium-disable-line operator-whitespace*/ + _beneficiaries.length == _startTimes.length, + "Arrays sizes mismatch" + ); + for (uint256 i = 0; i < _beneficiaries.length; i++) { + _addSchedule(_beneficiaries[i], _templateNames[i], _numberOfTokens[i], _durations[i], _frequencies[i], _startTimes[i]); + } + } + + /** + * @notice Used to bulk add vesting schedules from template for each of the beneficiary + * @param _beneficiaries Array of beneficiary's addresses + * @param _templateNames Array of the template names were used for schedule creation + * @param _startTimes Array of the vesting start time + */ + function addScheduleFromTemplateMulti(address[] _beneficiaries, bytes32[] _templateNames, uint256[] _startTimes) external withPerm(ADMIN) { + require(_beneficiaries.length == _templateNames.length && _beneficiaries.length == _startTimes.length, "Arrays sizes mismatch"); + for (uint256 i = 0; i < _beneficiaries.length; i++) { + _addScheduleFromTemplate(_beneficiaries[i], _templateNames[i], _startTimes[i]); + } + } + + /** + * @notice Used to bulk revoke vesting schedules for each of the beneficiaries + * @param _beneficiaries Array of the beneficiary's addresses + */ + function revokeSchedulesMulti(address[] _beneficiaries) external withPerm(ADMIN) { + for (uint256 i = 0; i < _beneficiaries.length; i++) { + _revokeAllSchedules(_beneficiaries[i]); + } + } + + /** + * @notice Used to bulk modify vesting schedules for each of the beneficiaries + * @param _beneficiaries Array of the beneficiary's addresses + * @param _templateNames Array of the template names + * @param _startTimes Array of the vesting start time + */ + function modifyScheduleMulti( + address[] _beneficiaries, + bytes32[] _templateNames, + uint256[] _startTimes + ) + public + withPerm(ADMIN) + { + require( + _beneficiaries.length == _templateNames.length && /*solium-disable-line operator-whitespace*/ + _beneficiaries.length == _startTimes.length, + "Arrays sizes mismatch" + ); + for (uint256 i = 0; i < _beneficiaries.length; i++) { + _modifySchedule(_beneficiaries[i], _templateNames[i], _startTimes[i]); + } + } + + function _checkSchedule(address _beneficiary, bytes32 _templateName) internal view { + require(_beneficiary != address(0), "Invalid address"); + uint256 index = userToTemplateIndex[_beneficiary][_templateName]; + require( + index < schedules[_beneficiary].length && + schedules[_beneficiary][index].templateName == _templateName, + "Schedule not found" + ); + } + + function _isTemplateExists(bytes32 _name) internal view returns(bool) { + return templates[_name].numberOfTokens > 0; + } + + function _validateTemplate(uint256 _numberOfTokens, uint256 _duration, uint256 _frequency) internal view { + require(_numberOfTokens > 0, "Zero amount"); + require(_duration % _frequency == 0, "Invalid frequency"); + uint256 periodCount = _duration.div(_frequency); + require(_numberOfTokens % periodCount == 0); + uint256 amountPerPeriod = _numberOfTokens.div(periodCount); + require(amountPerPeriod % ISecurityToken(securityToken).granularity() == 0, "Invalid granularity"); + } + + function _sendTokens(address _beneficiary) internal { + for (uint256 i = 0; i < schedules[_beneficiary].length; i++) { + _sendTokensPerSchedule(_beneficiary, i); + } + } + + function _sendTokensPerSchedule(address _beneficiary, uint256 _index) internal { + uint256 amount = _getAvailableTokens(_beneficiary, _index); + if (amount > 0) { + schedules[_beneficiary][_index].claimedTokens = schedules[_beneficiary][_index].claimedTokens.add(amount); + require(ISecurityToken(securityToken).transfer(_beneficiary, amount), "Transfer failed"); + emit SendTokens(_beneficiary, amount); + } + } + + /** + * @notice Return the permissions flag that are associated with VestingEscrowWallet + */ + function getPermissions() public view returns(bytes32[]) { + bytes32[] memory allPermissions = new bytes32[](1); + allPermissions[0] = ADMIN; + return allPermissions; + } + +} diff --git a/contracts/modules/Wallet/VestingEscrowWalletFactory.sol b/contracts/modules/Wallet/VestingEscrowWalletFactory.sol new file mode 100644 index 000000000..238d571ea --- /dev/null +++ b/contracts/modules/Wallet/VestingEscrowWalletFactory.sol @@ -0,0 +1,76 @@ +pragma solidity ^0.4.24; + +import "../../proxy/VestingEscrowWalletProxy.sol"; +import "../../interfaces/IBoot.sol"; +import "../ModuleFactory.sol"; +import "../../libraries/Util.sol"; + +/** + * @title Factory for deploying VestingEscrowWallet module + */ +contract VestingEscrowWalletFactory is ModuleFactory { + + address public logicContract; + /** + * @notice Constructor + * @param _polyAddress Address of the polytoken + */ + constructor (address _polyAddress, uint256 _setupCost, uint256 _usageCost, uint256 _subscriptionCost, address _logicContract) public + ModuleFactory(_polyAddress, _setupCost, _usageCost, _subscriptionCost) + { + require(_logicContract != address(0), "Invalid address"); + version = "1.0.0"; + name = "VestingEscrowWallet"; + title = "Vesting Escrow Wallet"; + description = "Manage vesting schedules to employees / affiliates"; + compatibleSTVersionRange["lowerBound"] = VersionUtils.pack(uint8(0), uint8(0), uint8(0)); + compatibleSTVersionRange["upperBound"] = VersionUtils.pack(uint8(0), uint8(0), uint8(0)); + logicContract = _logicContract; + } + + /** + * @notice Used to launch the Module with the help of factory + * _data Data used for the intialization of the module factory variables + * @return address Contract address of the Module + */ + function deploy(bytes _data) external returns(address) { + if (setupCost > 0) { + require(polyToken.transferFrom(msg.sender, owner, setupCost), "Failed transferFrom due to insufficent Allowance provided"); + } + VestingEscrowWalletProxy vestingEscrowWallet = new VestingEscrowWalletProxy(msg.sender, address(polyToken), logicContract); + //Checks that _data is valid (not calling anything it shouldn't) + require(Util.getSig(_data) == IBoot(vestingEscrowWallet).getInitFunction(), "Invalid data"); + /*solium-disable-next-line security/no-low-level-calls*/ + require(address(vestingEscrowWallet).call(_data), "Unsuccessfull call"); + /*solium-disable-next-line security/no-block-members*/ + emit GenerateModuleFromFactory(address(vestingEscrowWallet), getName(), address(this), msg.sender, setupCost, now); + return address(vestingEscrowWallet); + } + + /** + * @notice Type of the Module factory + */ + function getTypes() external view returns(uint8[]) { + uint8[] memory res = new uint8[](1); + res[0] = 6; + return res; + } + + /** + * @notice Returns the instructions associated with the module + */ + function getInstructions() external view returns(string) { + /*solium-disable-next-line max-len*/ + return "Issuer can deposit tokens to the contract and create the vesting schedule for the given address (Affiliate/Employee). These address can withdraw tokens according to there vesting schedule."; + } + + /** + * @notice Get the tags related to the module factory + */ + function getTags() external view returns(bytes32[]) { + bytes32[] memory availableTags = new bytes32[](2); + availableTags[0] = "Vested"; + availableTags[1] = "Escrow Wallet"; + return availableTags; + } +} diff --git a/contracts/modules/Wallet/VestingEscrowWalletStorage.sol b/contracts/modules/Wallet/VestingEscrowWalletStorage.sol new file mode 100644 index 000000000..af40d32bf --- /dev/null +++ b/contracts/modules/Wallet/VestingEscrowWalletStorage.sol @@ -0,0 +1,54 @@ +pragma solidity ^0.4.24; + +/** + * @title Wallet for core vesting escrow functionality + */ +contract VestingEscrowWalletStorage { + + struct Schedule { + // Name of the template + bytes32 templateName; + // Tokens that were already claimed + uint256 claimedTokens; + // Start time of the schedule + uint256 startTime; + } + + struct Template { + // Total amount of tokens + uint256 numberOfTokens; + // Schedule duration (How long the schedule will last) + uint256 duration; + // Schedule frequency (It is a cliff time period) + uint256 frequency; + // Index of the template in an array template names + uint256 index; + } + + // Number of tokens that are hold by the `this` contract but are unassigned to any schedule + uint256 public unassignedTokens; + // Address of the Treasury wallet. All of the unassigned token will transfer to that address. + address public treasuryWallet; + // List of all beneficiaries who have the schedules running/completed/created + address[] public beneficiaries; + // Flag whether beneficiary has been already added or not + mapping(address => bool) internal beneficiaryAdded; + + // Holds schedules array corresponds to the affiliate/employee address + mapping(address => Schedule[]) public schedules; + // Holds template names array corresponds to the affiliate/employee address + mapping(address => bytes32[]) internal userToTemplates; + // Mapping use to store the indexes for different template names for a user. + // affiliate/employee address => template name => index + mapping(address => mapping(bytes32 => uint256)) internal userToTemplateIndex; + // Holds affiliate/employee addresses coressponds to the template name + mapping(bytes32 => address[]) internal templateToUsers; + // Mapping use to store the indexes for different users for a template. + // template name => affiliate/employee address => index + mapping(bytes32 => mapping(address => uint256)) internal templateToUserIndex; + // Store the template details corresponds to the template name + mapping(bytes32 => Template) templates; + + // List of all template names + bytes32[] public templateNames; +} \ No newline at end of file diff --git a/contracts/proxy/VestingEscrowWalletProxy.sol b/contracts/proxy/VestingEscrowWalletProxy.sol new file mode 100644 index 000000000..0138e0402 --- /dev/null +++ b/contracts/proxy/VestingEscrowWalletProxy.sol @@ -0,0 +1,27 @@ +pragma solidity ^0.4.24; + +import "../modules/Wallet/VestingEscrowWalletStorage.sol"; +import "./OwnedProxy.sol"; +import "../Pausable.sol"; +import "../modules/ModuleStorage.sol"; + /** + * @title Escrow wallet module for vesting functionality + */ +contract VestingEscrowWalletProxy is VestingEscrowWalletStorage, ModuleStorage, Pausable, OwnedProxy { + /** + * @notice Constructor + * @param _securityToken Address of the security token + * @param _polyAddress Address of the polytoken + * @param _implementation representing the address of the new implementation to be set + */ + constructor (address _securityToken, address _polyAddress, address _implementation) + public + ModuleStorage(_securityToken, _polyAddress) + { + require( + _implementation != address(0), + "Implementation address should not be 0x" + ); + __implementation = _implementation; + } + } \ No newline at end of file diff --git a/docs/permissions_list.md b/docs/permissions_list.md index f75e9389c..4a4438d5f 100644 --- a/docs/permissions_list.md +++ b/docs/permissions_list.md @@ -264,7 +264,60 @@ removeTransferLimitInPercentageMulti + + + + Wallet + VestingEscrowWallet + changeTreasuryWallet() + onlyOwner + + + depositTokens() + withPerm(ADMIN) + + + sendToTreasury() + + + pushAvailableTokens() + + + addTemplate() + + + removeTemplate() + + + addSchedule() + + + addScheduleFromTemplate() + + modifySchedule() + + + revokeSchedule() + + + revokeAllSchedules() + + + pushAvailableTokensMulti() + + + addScheduleMulti() + + + addScheduleFromTemplateMulti() + + + revokeSchedulesMulti() + + + modifyScheduleMulti() + diff --git a/migrations/2_deploy_contracts.js b/migrations/2_deploy_contracts.js index 0a5c79341..4beb28252 100644 --- a/migrations/2_deploy_contracts.js +++ b/migrations/2_deploy_contracts.js @@ -9,6 +9,8 @@ const EtherDividendCheckpointLogic = artifacts.require('./EtherDividendCheckpoin const ERC20DividendCheckpointLogic = artifacts.require('./ERC20DividendCheckpoint.sol') const EtherDividendCheckpointFactory = artifacts.require('./EtherDividendCheckpointFactory.sol') const ERC20DividendCheckpointFactory = artifacts.require('./ERC20DividendCheckpointFactory.sol') +const VestingEscrowWalletFactory = artifacts.require('./VestingEscrowWalletFactory.sol'); +const VestingEscrowWalletLogic = artifacts.require('./VestingEscrowWallet.sol'); const ModuleRegistry = artifacts.require('./ModuleRegistry.sol'); const ModuleRegistryProxy = artifacts.require('./ModuleRegistryProxy.sol'); const ManualApprovalTransferManagerFactory = artifacts.require('./ManualApprovalTransferManagerFactory.sol') @@ -154,17 +156,25 @@ module.exports = function (deployer, network, accounts) { // manager attach with the securityToken contract at the time of deployment) return deployer.deploy(GeneralTransferManagerLogic, "0x0000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000", {from: PolymathAccount}); }).then(() => { - // B) Deploy the GeneralTransferManagerLogic Contract (Factory used to generate the GeneralTransferManager contract and this + // B) Deploy the ERC20DividendCheckpointLogic Contract (Factory used to generate the ERC20DividendCheckpoint contract and this // manager attach with the securityToken contract at the time of deployment) return deployer.deploy(ERC20DividendCheckpointLogic, "0x0000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000", {from: PolymathAccount}); }).then(() => { - // B) Deploy the GeneralTransferManagerLogic Contract (Factory used to generate the GeneralTransferManager contract and this + // B) Deploy the EtherDividendCheckpointLogic Contract (Factory used to generate the EtherDividendCheckpoint contract and this // manager attach with the securityToken contract at the time of deployment) return deployer.deploy(EtherDividendCheckpointLogic, "0x0000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000", {from: PolymathAccount}); + }).then(() => { + // B) Deploy the VestingEscrowWalletLogic Contract (Factory used to generate the VestingEscrowWallet contract and this + // manager attach with the securityToken contract at the time of deployment) + return deployer.deploy(VestingEscrowWalletLogic, "0x0000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000", {from: PolymathAccount}); }).then(() => { // B) Deploy the USDTieredSTOLogic Contract (Factory used to generate the USDTieredSTO contract and this // manager attach with the securityToken contract at the time of deployment) return deployer.deploy(USDTieredSTOLogic, "0x0000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000", {from: PolymathAccount}); + }).then(() => { + // B) Deploy the VestingEscrowWalletFactory Contract (Factory used to generate the VestingEscrowWallet contract and this + // manager attach with the securityToken contract at the time of deployment) + return deployer.deploy(VestingEscrowWalletFactory, PolyToken, 0, 0, 0, VestingEscrowWalletLogic.address, {from: PolymathAccount}); }).then(() => { // B) Deploy the GeneralTransferManagerFactory Contract (Factory used to generate the GeneralTransferManager contract and this // manager attach with the securityToken contract at the time of deployment) @@ -220,6 +230,10 @@ module.exports = function (deployer, network, accounts) { // D) Register the PercentageTransferManagerFactory in the ModuleRegistry to make the factory available at the protocol level. // So any securityToken can use that factory to generate the PercentageTransferManager contract. return moduleRegistry.registerModule(PercentageTransferManagerFactory.address, {from: PolymathAccount}); + }).then(() => { + // D) Register the VestingEscrowWalletFactory in the ModuleRegistry to make the factory available at the protocol level. + // So any securityToken can use that factory to generate the VestingEscrowWallet contract. + return moduleRegistry.registerModule(VestingEscrowWalletFactory.address, {from: PolymathAccount}); }).then(() => { // D) Register the CountTransferManagerFactory in the ModuleRegistry to make the factory available at the protocol level. // So any securityToken can use that factory to generate the CountTransferManager contract. @@ -279,6 +293,11 @@ module.exports = function (deployer, network, accounts) { // contract, Factory should comes under the verified list of factories or those factories deployed by the securityToken issuers only. // Here it gets verified because it is deployed by the third party account (Polymath Account) not with the issuer accounts. return moduleRegistry.verifyModule(ManualApprovalTransferManagerFactory.address, true, {from: PolymathAccount}); + }).then(() => { + // F) Once the VestingEscrowWalletFactory registered with the ModuleRegistry contract then for making them accessble to the securityToken + // contract, Factory should comes under the verified list of factories or those factories deployed by the securityToken issuers only. + // Here it gets verified because it is deployed by the third party account (Polymath Account) not with the issuer accounts. + return moduleRegistry.verifyModule(VestingEscrowWalletFactory.address, true, {from: PolymathAccount}); }).then(() => { // M) Deploy the CappedSTOFactory (Use to generate the CappedSTO contract which will used to collect the funds ). return deployer.deploy(CappedSTOFactory, PolyToken, cappedSTOSetupCost, 0, 0, {from: PolymathAccount}) @@ -337,6 +356,8 @@ module.exports = function (deployer, network, accounts) { ERC20DividendCheckpointLogic: ${ERC20DividendCheckpointLogic.address} EtherDividendCheckpointFactory: ${EtherDividendCheckpointFactory.address} ERC20DividendCheckpointFactory: ${ERC20DividendCheckpointFactory.address} + VestingEscrowWalletFactory: ${VestingEscrowWalletFactory.address} + VestingEscrowWalletLogic: ${VestingEscrowWalletLogic.address} --------------------------------------------------------------------------------- `); console.log('\n'); diff --git a/test/helpers/createInstances.js b/test/helpers/createInstances.js index 77c7a5e5d..a6f495984 100644 --- a/test/helpers/createInstances.js +++ b/test/helpers/createInstances.js @@ -32,6 +32,8 @@ const PolyTokenFaucet = artifacts.require("./PolyTokenFaucet.sol"); const DummySTOFactory = artifacts.require("./DummySTOFactory.sol"); const MockBurnFactory = artifacts.require("./MockBurnFactory.sol"); const MockWrongTypeFactory = artifacts.require("./MockWrongTypeFactory.sol"); +const VestingEscrowWalletFactory = artifacts.require("./VestingEscrowWalletFactory.sol"); +const VestingEscrowWallet = artifacts.require("./VestingEscrowWallet.sol"); const Web3 = require("web3"); const web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545")); // Hardcoded development port @@ -54,6 +56,7 @@ let I_ERC20DividendCheckpointFactory; let I_GeneralPermissionManagerFactory; let I_GeneralTransferManagerLogic; let I_GeneralTransferManagerFactory; +let I_VestingEscrowWalletFactory; let I_GeneralTransferManager; let I_ModuleRegistryProxy; let I_PreSaleSTOFactory; @@ -68,6 +71,7 @@ let I_STFactory; let I_USDTieredSTOLogic; let I_PolymathRegistry; let I_SecurityTokenRegistryProxy; +let I_VestingEscrowWalletLogic; let I_STRProxied; let I_MRProxied; @@ -104,7 +108,7 @@ export async function setUpPolymathNetwork(account_polymath, token_owner) { } -async function deployPolyRegistryAndPolyToken(account_polymath, token_owner) { +export async function deployPolyRegistryAndPolyToken(account_polymath, token_owner) { // Step 0: Deploy the PolymathRegistry I_PolymathRegistry = await PolymathRegistry.new({ from: account_polymath }); @@ -404,6 +408,19 @@ export async function deployRedemptionAndVerifyed(accountPolymath, MRProxyInstan return new Array(I_TrackedRedemptionFactory); } +export async function deployVestingEscrowWalletAndVerifyed(accountPolymath, MRProxyInstance, polyToken, setupCost) { + I_VestingEscrowWalletLogic = await VestingEscrowWallet.new("0x0000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000", { from: accountPolymath }); + I_VestingEscrowWalletFactory = await VestingEscrowWalletFactory.new(polyToken, setupCost, 0, 0, I_VestingEscrowWalletLogic.address, { from: accountPolymath }); + + assert.notEqual( + I_VestingEscrowWalletFactory.address.valueOf(), + "0x0000000000000000000000000000000000000000", + "VestingEscrowWalletFactory contract was not deployed" + ); + + await registerAndVerifyByMR(I_VestingEscrowWalletFactory.address, accountPolymath, MRProxyInstance); + return new Array(I_VestingEscrowWalletFactory); +} export async function deployMockRedemptionAndVerifyed(accountPolymath, MRProxyInstance, polyToken, setupCost) { I_MockBurnFactory = await MockBurnFactory.new(polyToken, setupCost, 0, 0, { from: accountPolymath }); @@ -431,8 +448,6 @@ export async function deployMockWrongTypeRedemptionAndVerifyed(accountPolymath, return new Array(I_MockWrongTypeBurnFactory); } - - /// Helper function function mergeReturn(returnData) { let returnArray = new Array(); diff --git a/test/helpers/exceptions.js b/test/helpers/exceptions.js index 22c05be07..ea0327af8 100644 --- a/test/helpers/exceptions.js +++ b/test/helpers/exceptions.js @@ -25,6 +25,9 @@ module.exports = { catchRevert: async function(promise) { await tryCatch(promise, "revert"); }, + catchPermission: async function(promise) { + await tryCatch(promise, "revert Permission check failed"); + }, catchOutOfGas: async function(promise) { await tryCatch(promise, "out of gas"); }, diff --git a/test/z_vesting_escrow_wallet.js b/test/z_vesting_escrow_wallet.js new file mode 100644 index 000000000..d15acddd1 --- /dev/null +++ b/test/z_vesting_escrow_wallet.js @@ -0,0 +1,1197 @@ +import {deployGPMAndVerifyed, deployVestingEscrowWalletAndVerifyed, setUpPolymathNetwork} from "./helpers/createInstances"; +import latestTime from "./helpers/latestTime"; +import {duration as durationUtil, latestBlock, promisifyLogWatch} from "./helpers/utils"; +import {catchRevert} from "./helpers/exceptions"; +import {increaseTime} from "./helpers/time"; +import {encodeModuleCall} from "./helpers/encodeCall"; + +const SecurityToken = artifacts.require('./SecurityToken.sol'); +const GeneralTransferManager = artifacts.require('./GeneralTransferManager'); +const GeneralPermissionManager = artifacts.require("./GeneralPermissionManager"); +const VestingEscrowWallet = artifacts.require('./VestingEscrowWallet.sol'); + +const Web3 = require('web3'); +const BigNumber = require('bignumber.js'); +const web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));// Hardcoded development port + +contract('VestingEscrowWallet', accounts => { + + const CREATED = 0; + const STARTED = 1; + const COMPLETED = 2; + + // Accounts Variable declaration + let account_polymath; + let token_owner; + let wallet_admin; + let account_beneficiary1; + let account_beneficiary2; + let account_beneficiary3; + + let beneficiaries; + + let message = "Transaction Should Fail!"; + + // Contract Instance Declaration + let I_SecurityTokenRegistryProxy; + let I_GeneralPermissionManagerFactory; + let I_GeneralTransferManagerFactory; + let I_VestingEscrowWalletFactory; + let I_GeneralPermissionManager; + let I_VestingEscrowWallet; + let I_GeneralTransferManager; + let I_ModuleRegistryProxy; + let I_ModuleRegistry; + let I_FeatureRegistry; + let I_SecurityTokenRegistry; + let I_STRProxied; + let I_MRProxied; + let I_STFactory; + let I_SecurityToken; + let I_PolyToken; + let I_PolymathRegistry; + + // SecurityToken Details + const name = "Team"; + const symbol = "sap"; + const tokenDetails = "This is equity type of issuance"; + const decimals = 18; + const contact = "team@polymath.network"; + const delegateDetails = "Hello I am legit delegate"; + + // Module key + const delegateManagerKey = 1; + const transferManagerKey = 2; + const stoKey = 3; + + // Initial fee for ticker registry and security token registry + const initRegFee = web3.utils.toWei("250"); + + before(async () => { + // Accounts setup + account_polymath = accounts[0]; + token_owner = accounts[1]; + wallet_admin = accounts[2]; + + account_beneficiary1 = accounts[6]; + account_beneficiary2 = accounts[7]; + account_beneficiary3 = accounts[8]; + + beneficiaries = [ + account_beneficiary1, + account_beneficiary2, + account_beneficiary3 + ]; + + // Step 1: Deploy the genral PM ecosystem + let instances = await setUpPolymathNetwork(account_polymath, token_owner); + + [ + I_PolymathRegistry, + I_PolyToken, + I_FeatureRegistry, + I_ModuleRegistry, + I_ModuleRegistryProxy, + I_MRProxied, + I_GeneralTransferManagerFactory, + I_STFactory, + I_SecurityTokenRegistry, + I_SecurityTokenRegistryProxy, + I_STRProxied + ] = instances; + + // STEP 2: Deploy the GeneralDelegateManagerFactory + [I_GeneralPermissionManagerFactory] = await deployGPMAndVerifyed(account_polymath, I_MRProxied, I_PolyToken.address, 0); + + // STEP 3: Deploy the VestingEscrowWallet + [I_VestingEscrowWalletFactory] = await deployVestingEscrowWalletAndVerifyed(account_polymath, I_MRProxied, I_PolyToken.address, 0); + + // Printing all the contract addresses + console.log(` + --------------------- Polymath Network Smart Contracts: --------------------- + PolymathRegistry: ${I_PolymathRegistry.address} + SecurityTokenRegistryProxy: ${I_SecurityTokenRegistryProxy.address} + SecurityTokenRegistry: ${I_SecurityTokenRegistry.address} + ModuleRegistry: ${I_ModuleRegistry.address} + ModuleRegistryProxy: ${I_ModuleRegistryProxy.address} + FeatureRegistry: ${I_FeatureRegistry.address} + + STFactory: ${I_STFactory.address} + GeneralTransferManagerFactory: ${I_GeneralTransferManagerFactory.address} + GeneralPermissionManagerFactory: ${I_GeneralPermissionManagerFactory.address} + + I_VestingEscrowWalletFactory: ${I_VestingEscrowWalletFactory.address} + ----------------------------------------------------------------------------- + `); + }); + + describe("Generate the SecurityToken", async() => { + + it("Should register the ticker before the generation of the security token", async () => { + await I_PolyToken.approve(I_STRProxied.address, initRegFee, { from: token_owner }); + let tx = await I_STRProxied.registerTicker(token_owner, symbol, contact, { from : token_owner }); + assert.equal(tx.logs[0].args._owner, token_owner); + assert.equal(tx.logs[0].args._ticker, symbol.toUpperCase()); + }); + + it("Should generate the new security token with the same symbol as registered above", async () => { + await I_PolyToken.approve(I_STRProxied.address, initRegFee, { from: token_owner }); + let _blockNo = latestBlock(); + let tx = await I_STRProxied.generateSecurityToken(name, symbol, tokenDetails, false, { from: token_owner }); + + // Verify the successful generation of the security token + assert.equal(tx.logs[1].args._ticker, symbol.toUpperCase(), "SecurityToken doesn't get deployed"); + + I_SecurityToken = SecurityToken.at(tx.logs[1].args._securityTokenAddress); + + const log = await promisifyLogWatch(I_SecurityToken.ModuleAdded({from: _blockNo}), 1); + + // Verify that GeneralTransferManager module get added successfully or not + assert.equal(log.args._types[0].toNumber(), 2); + assert.equal( + web3.utils.toAscii(log.args._name) + .replace(/\u0000/g, ''), + "GeneralTransferManager" + ); + }); + + it("Should intialize the auto attached modules", async () => { + let moduleData = (await I_SecurityToken.getModulesByType(2))[0]; + I_GeneralTransferManager = GeneralTransferManager.at(moduleData); + + }); + + it("Should successfully attach the General permission manager factory with the security token", async () => { + const tx = await I_SecurityToken.addModule(I_GeneralPermissionManagerFactory.address, "0x", 0, 0, { from: token_owner }); + assert.equal(tx.logs[2].args._types[0].toNumber(), delegateManagerKey, "General Permission Manager doesn't get deployed"); + assert.equal( + web3.utils.toAscii(tx.logs[2].args._name).replace(/\u0000/g, ""), + "GeneralPermissionManager", + "GeneralPermissionManagerFactory module was not added" + ); + I_GeneralPermissionManager = GeneralPermissionManager.at(tx.logs[2].args._module); + }); + + it("Should successfully attach the VestingEscrowWallet with the security token", async () => { + let bytesData = encodeModuleCall( + ["address"], + [token_owner] + ); + + await I_SecurityToken.changeGranularity(1, {from: token_owner}); + const tx = await I_SecurityToken.addModule(I_VestingEscrowWalletFactory.address, bytesData, 0, 0, { from: token_owner }); + + assert.equal(tx.logs[2].args._types[0].toNumber(), 6, "VestingEscrowWallet doesn't get deployed"); + assert.equal( + web3.utils.toAscii(tx.logs[2].args._name) + .replace(/\u0000/g, ''), + "VestingEscrowWallet", + "VestingEscrowWallet module was not added" + ); + I_VestingEscrowWallet = VestingEscrowWallet.at(tx.logs[2].args._module); + }); + + it("Should Buy the tokens for token_owner", async() => { + // Add the Investor in to the whitelist + let tx = await I_GeneralTransferManager.modifyWhitelist( + token_owner, + latestTime(), + latestTime(), + latestTime() + durationUtil.days(10), + true, + { + from: token_owner, + gas: 6000000 + }); + + assert.equal(tx.logs[0].args._investor.toLowerCase(), token_owner.toLowerCase(), "Failed in adding the token_owner in whitelist"); + + // Mint some tokens + await I_SecurityToken.mint(token_owner, web3.utils.toWei('1', 'ether'), { from: token_owner }); + + assert.equal( + (await I_SecurityToken.balanceOf(token_owner)).toNumber(), + web3.utils.toWei('1', 'ether') + ); + + }); + + it("Should whitelist investors", async() => { + // Add the Investor in to the whitelist + let tx = await I_GeneralTransferManager.modifyWhitelistMulti( + [I_VestingEscrowWallet.address, account_beneficiary1, account_beneficiary2, account_beneficiary3], + [latestTime(), latestTime(), latestTime(), latestTime()], + [latestTime(), latestTime(), latestTime(), latestTime()], + [latestTime() + durationUtil.days(10), latestTime() + durationUtil.days(10), latestTime() + durationUtil.days(10), latestTime() + durationUtil.days(10)], + [true, true, true, true], + { + from: token_owner, + gas: 6000000 + }); + + assert.equal(tx.logs[0].args._investor, I_VestingEscrowWallet.address); + assert.equal(tx.logs[1].args._investor, account_beneficiary1); + assert.equal(tx.logs[2].args._investor, account_beneficiary2); + assert.equal(tx.logs[3].args._investor, account_beneficiary3); + }); + + it("Should successfully add the delegate", async() => { + let tx = await I_GeneralPermissionManager.addDelegate(wallet_admin, delegateDetails, { from: token_owner}); + assert.equal(tx.logs[0].args._delegate, wallet_admin); + }); + + it("Should provide the permission", async() => { + let tx = await I_GeneralPermissionManager.changePermission( + wallet_admin, + I_VestingEscrowWallet.address, + "ADMIN", + true, + {from: token_owner} + ); + assert.equal(tx.logs[0].args._delegate, wallet_admin); + }); + + it("Should get the permission", async () => { + let perm = await I_VestingEscrowWallet.getPermissions.call(); + assert.equal(web3.utils.toAscii(perm[0]).replace(/\u0000/g, ""), "ADMIN"); + }); + + it("Should get the tags of the factory", async () => { + let tags = await I_VestingEscrowWalletFactory.getTags.call(); + assert.equal(tags.length, 2); + assert.equal(web3.utils.toAscii(tags[0]).replace(/\u0000/g, ""), "Vested"); + assert.equal(web3.utils.toAscii(tags[1]).replace(/\u0000/g, ""), "Escrow Wallet"); + }); + + it("Should get the instructions of the factory", async () => { + assert.equal( + (await I_VestingEscrowWalletFactory.getInstructions.call()).replace(/\u0000/g, ""), + "Issuer can deposit tokens to the contract and create the vesting schedule for the given address (Affiliate/Employee). These address can withdraw tokens according to there vesting schedule." + ); + }); + + }); + + describe("Depositing and withdrawing tokens", async () => { + + it("Should not be able to change treasury wallet -- fail because address is invalid", async () => { + await catchRevert( + I_VestingEscrowWallet.changeTreasuryWallet(0, {from: token_owner}) + ); + }); + + it("Should not be able to deposit -- fail because of permissions check", async () => { + await catchRevert( + I_VestingEscrowWallet.changeTreasuryWallet(account_beneficiary1, {from: account_beneficiary1}) + ); + }); + + it("Should change treasury wallet", async () => { + const tx = await I_VestingEscrowWallet.changeTreasuryWallet(account_beneficiary1, {from: token_owner}); + + assert.equal(tx.logs[0].args._newWallet, account_beneficiary1); + assert.equal(tx.logs[0].args._oldWallet, token_owner); + let treasuryWallet = await I_VestingEscrowWallet.treasuryWallet.call(); + assert.equal(treasuryWallet, account_beneficiary1); + + await I_VestingEscrowWallet.changeTreasuryWallet(token_owner, {from: token_owner}); + }); + + it("Should fail to deposit zero amount of tokens", async () => { + await catchRevert( + I_VestingEscrowWallet.depositTokens(0, {from: token_owner}) + ); + }); + + it("Should not be able to deposit -- fail because of permissions check", async () => { + let numberOfTokens = 25000; + await I_SecurityToken.approve(I_VestingEscrowWallet.address, numberOfTokens, { from: token_owner }); + await catchRevert( + I_VestingEscrowWallet.depositTokens(25000, {from: account_beneficiary1}) + ); + }); + + it("Should deposit tokens for new vesting schedules", async () => { + let numberOfTokens = 25000; + await I_SecurityToken.approve(I_VestingEscrowWallet.address, numberOfTokens, { from: token_owner }); + const tx = await I_VestingEscrowWallet.depositTokens(numberOfTokens, {from: token_owner}); + + assert.equal(tx.logs[0].args._numberOfTokens, numberOfTokens); + + let unassignedTokens = await I_VestingEscrowWallet.unassignedTokens.call(); + assert.equal(unassignedTokens, numberOfTokens); + + let balance = await I_SecurityToken.balanceOf.call(I_VestingEscrowWallet.address); + assert.equal(balance.toNumber(), numberOfTokens); + }); + + it("Should not be able to withdraw tokens to a treasury -- fail because of permissions check", async () => { + await catchRevert( + I_VestingEscrowWallet.sendToTreasury(10, {from: account_beneficiary1}) + ); + }); + + it("Should not be able to withdraw tokens to a treasury -- fail because of zero amount", async () => { + await catchRevert( + I_VestingEscrowWallet.sendToTreasury(0, {from: wallet_admin}) + ); + }); + + it("Should not be able to withdraw tokens to a treasury -- fail because amount is greater than unassigned tokens", async () => { + let numberOfTokens = 25000 * 2; + await catchRevert( + I_VestingEscrowWallet.sendToTreasury(numberOfTokens, {from: wallet_admin}) + ); + }); + + it("Should withdraw tokens to a treasury", async () => { + let numberOfTokens = 25000; + const tx = await I_VestingEscrowWallet.sendToTreasury(numberOfTokens, {from: wallet_admin}); + + assert.equal(tx.logs[0].args._numberOfTokens, numberOfTokens); + + let unassignedTokens = await I_VestingEscrowWallet.unassignedTokens.call(); + assert.equal(unassignedTokens, 0); + + let balance = await I_SecurityToken.balanceOf.call(I_VestingEscrowWallet.address); + assert.equal(balance.toNumber(), 0); + }); + + it("Should not be able to push available tokens -- fail because of permissions check", async () => { + let templateName = "template-01"; + let numberOfTokens = 75000; + let duration = durationUtil.seconds(30); + let frequency = durationUtil.seconds(10); + let timeShift = durationUtil.seconds(100); + let startTime = latestTime() + timeShift; + await I_SecurityToken.approve(I_VestingEscrowWallet.address, numberOfTokens, { from: token_owner }); + await I_VestingEscrowWallet.depositTokens(numberOfTokens, {from: token_owner}); + await I_VestingEscrowWallet.addSchedule(account_beneficiary3, templateName, numberOfTokens, duration, frequency, startTime, {from: wallet_admin}); + await increaseTime(timeShift + frequency); + + await catchRevert( + I_VestingEscrowWallet.pushAvailableTokens(account_beneficiary3, {from: account_beneficiary1}) + ); + }); + + it("Should not be able to remove template -- fail because template is used", async () => { + await catchRevert( + I_VestingEscrowWallet.removeTemplate("template-01", {from: wallet_admin}) + ); + }); + + it("Should push available tokens to the beneficiary address", async () => { + let numberOfTokens = 75000; + const tx = await I_VestingEscrowWallet.pushAvailableTokens(account_beneficiary3, {from: wallet_admin}); + assert.equal(tx.logs[0].args._beneficiary, account_beneficiary3); + assert.equal(tx.logs[0].args._numberOfTokens.toNumber(), numberOfTokens / 3); + + let balance = await I_SecurityToken.balanceOf.call(account_beneficiary3); + assert.equal(balance.toNumber(), numberOfTokens / 3); + + await I_SecurityToken.transfer(token_owner, balance, {from: account_beneficiary3}); + }); + + it("Should fail to modify vesting schedule -- fail because schedule already started", async () => { + let templateName = "template-01"; + let startTime = latestTime() + 100; + await catchRevert( + I_VestingEscrowWallet.modifySchedule(account_beneficiary3, templateName, startTime, {from: wallet_admin}) + ); + + await I_VestingEscrowWallet.revokeAllSchedules(account_beneficiary3, {from: wallet_admin}); + await I_VestingEscrowWallet.removeTemplate(templateName, {from: wallet_admin}); + let unassignedTokens = await I_VestingEscrowWallet.unassignedTokens.call(); + await I_VestingEscrowWallet.sendToTreasury(unassignedTokens, {from: wallet_admin}); + }); + + it("Should fail to modify vesting schedule -- fail because date in the past", async () => { + await catchRevert( + I_VestingEscrowWallet.modifySchedule(account_beneficiary3, "template-01", latestTime() - 1000, {from: wallet_admin}) + ); + }); + + it("Should withdraw available tokens to the beneficiary address", async () => { + let templateName = "template-02"; + let numberOfTokens = 33000; + let duration = durationUtil.seconds(30); + let frequency = durationUtil.seconds(10); + let timeShift = durationUtil.seconds(100); + let startTime = latestTime() + timeShift; + await I_SecurityToken.approve(I_VestingEscrowWallet.address, numberOfTokens, { from: token_owner }); + await I_VestingEscrowWallet.depositTokens(numberOfTokens, {from: token_owner}); + await I_VestingEscrowWallet.addSchedule(account_beneficiary3, templateName, numberOfTokens, duration, frequency, startTime, {from: wallet_admin}); + await increaseTime(timeShift + frequency * 3); + + const tx = await I_VestingEscrowWallet.pullAvailableTokens({from: account_beneficiary3}); + assert.equal(tx.logs[0].args._beneficiary, account_beneficiary3); + assert.equal(tx.logs[0].args._numberOfTokens.toNumber(), numberOfTokens); + + let balance = await I_SecurityToken.balanceOf.call(account_beneficiary3); + assert.equal(balance.toNumber(), numberOfTokens); + + let schedule = await I_VestingEscrowWallet.getSchedule.call(account_beneficiary3, templateName); + checkSchedule(schedule, numberOfTokens, duration, frequency, startTime, COMPLETED); + + await I_SecurityToken.transfer(token_owner, balance, {from: account_beneficiary3}); + await I_VestingEscrowWallet.revokeAllSchedules(account_beneficiary3, {from: wallet_admin}); + await I_VestingEscrowWallet.removeTemplate(templateName, {from: wallet_admin}); + }); + + it("Should withdraw available tokens 2 times by 3 schedules to the beneficiary address", async () => { + let schedules = [ + { + templateName: "template-1-01", + numberOfTokens: 100000, + duration: durationUtil.minutes(4), + frequency: durationUtil.minutes(1) + }, + { + templateName: "template-1-02", + numberOfTokens: 30000, + duration: durationUtil.minutes(6), + frequency: durationUtil.minutes(1) + }, + { + templateName: "template-1-03", + numberOfTokens: 2000, + duration: durationUtil.minutes(10), + frequency: durationUtil.minutes(1) + } + ]; + + let totalNumberOfTokens = getTotalNumberOfTokens(schedules); + await I_SecurityToken.approve(I_VestingEscrowWallet.address, totalNumberOfTokens, {from: token_owner}); + await I_VestingEscrowWallet.depositTokens(totalNumberOfTokens, {from: token_owner}); + for (let i = 0; i < schedules.length; i++) { + let templateName = schedules[i].templateName; + let numberOfTokens = schedules[i].numberOfTokens; + let duration = schedules[i].duration; + let frequency = schedules[i].frequency; + let startTime = latestTime() + durationUtil.seconds(100); + await I_VestingEscrowWallet.addSchedule(account_beneficiary3, templateName, numberOfTokens, duration, frequency, startTime, {from: wallet_admin}); + } + let stepCount = 6; + await increaseTime(durationUtil.minutes(stepCount) + durationUtil.seconds(100)); + + let numberOfTokens = 100000 + (30000 / 6 * stepCount) + (2000 / 10 * stepCount); + const tx = await I_VestingEscrowWallet.pullAvailableTokens({from: account_beneficiary3}); + + assert.equal(tx.logs[0].args._beneficiary, account_beneficiary3); + assert.equal(tx.logs[0].args._numberOfTokens.toNumber(), 100000); + assert.equal(tx.logs[1].args._beneficiary, account_beneficiary3); + assert.equal(tx.logs[1].args._numberOfTokens.toNumber(), 30000 / 6 * stepCount); + assert.equal(tx.logs[2].args._beneficiary, account_beneficiary3); + assert.equal(tx.logs[2].args._numberOfTokens.toNumber(), 2000 / 10 * stepCount); + + let balance = await I_SecurityToken.balanceOf.call(account_beneficiary3); + assert.equal(balance.toNumber(), numberOfTokens); + + stepCount = 4; + await increaseTime(durationUtil.minutes(stepCount) + durationUtil.seconds(100)); + + const tx2 = await I_VestingEscrowWallet.pullAvailableTokens({from: account_beneficiary3}); + assert.equal(tx2.logs[0].args._beneficiary, account_beneficiary3); + assert.equal(tx2.logs[0].args._numberOfTokens.toNumber(), 2000 / 10 * stepCount); + + balance = await I_SecurityToken.balanceOf.call(account_beneficiary3); + assert.equal(balance.toNumber(), totalNumberOfTokens); + + await I_SecurityToken.transfer(token_owner, balance, {from: account_beneficiary3}); + await I_VestingEscrowWallet.revokeAllSchedules(account_beneficiary3, {from: wallet_admin}); + for (let i = 0; i < schedules.length; i++) { + await I_VestingEscrowWallet.removeTemplate(schedules[i].templateName, {from: wallet_admin}); + } + }); + + }); + + describe("Adding, modifying and revoking vesting schedule", async () => { + + let schedules = [ + { + templateName: "template-2-01", + numberOfTokens: 100000, + duration: durationUtil.years(4), + frequency: durationUtil.years(1), + startTime: latestTime() + durationUtil.days(1) + }, + { + templateName: "template-2-02", + numberOfTokens: 30000, + duration: durationUtil.weeks(6), + frequency: durationUtil.weeks(1), + startTime: latestTime() + durationUtil.days(2) + }, + { + templateName: "template-2-03", + numberOfTokens: 2000, + duration: durationUtil.days(10), + frequency: durationUtil.days(2), + startTime: latestTime() + durationUtil.days(3) + } + ]; + + it("Should fail to add vesting schedule to the beneficiary address -- fail because address in invalid", async () => { + await catchRevert( + I_VestingEscrowWallet.addSchedule(0, "template-2-01", 100000, 4, 1, latestTime() + durationUtil.days(1), {from: wallet_admin}) + ); + }); + + it("Should fail to add vesting schedule to the beneficiary address -- fail because start date in the past", async () => { + await catchRevert( + I_VestingEscrowWallet.addSchedule(account_beneficiary1, "template-2-01", 100000, 4, 1, latestTime() - durationUtil.days(1), {from: wallet_admin}) + ); + }); + + it("Should fail to add vesting schedule to the beneficiary address -- fail because number of tokens is 0", async () => { + await catchRevert( + I_VestingEscrowWallet.addSchedule(account_beneficiary1, "template-2-01", 0, 4, 1, latestTime() + durationUtil.days(1), {from: wallet_admin}) + ); + }); + + it("Should fail to add vesting schedule to the beneficiary address -- fail because duration can't be divided entirely by frequency", async () => { + await catchRevert( + I_VestingEscrowWallet.addSchedule(account_beneficiary1, "template-2-01", 100000, 4, 3, latestTime() + durationUtil.days(1), {from: wallet_admin}) + ); + }); + + it("Should fail to add vesting schedule to the beneficiary address -- fail because number of tokens can't be divided entirely by period count", async () => { + await catchRevert( + I_VestingEscrowWallet.addSchedule(account_beneficiary1, "template-2-01", 5, 4, 1, latestTime() + durationUtil.days(1), {from: wallet_admin}) + ); + }); + + it("Should fail to get vesting schedule -- fail because address is invalid", async () => { + await catchRevert( + I_VestingEscrowWallet.getSchedule(0, "template-2-01") + ); + }); + + it("Should fail to get vesting schedule -- fail because schedule not found", async () => { + await catchRevert( + I_VestingEscrowWallet.getSchedule(account_beneficiary1, "template-2-01") + ); + }); + + it("Should fail to get count of vesting schedule -- fail because address is invalid", async () => { + await catchRevert( + I_VestingEscrowWallet.getScheduleCount(0) + ); + }); + + it("Should not be able to add schedule -- fail because of permissions check", async () => { + let templateName = schedules[0].templateName; + let numberOfTokens = schedules[0].numberOfTokens; + let duration = schedules[0].duration; + let frequency = schedules[0].frequency; + let startTime = schedules[0].startTime; + await I_SecurityToken.approve(I_VestingEscrowWallet.address, numberOfTokens, {from: token_owner}); + await I_VestingEscrowWallet.depositTokens(numberOfTokens, {from: token_owner}); + await catchRevert( + I_VestingEscrowWallet.addSchedule(account_beneficiary1, templateName, numberOfTokens, duration, frequency, startTime, {from: account_beneficiary1}) + ); + let unassignedTokens = await I_VestingEscrowWallet.unassignedTokens.call(); + await I_VestingEscrowWallet.sendToTreasury(unassignedTokens, {from: wallet_admin}); + }); + + it("Should add vesting schedule to the beneficiary address", async () => { + let templateName = schedules[0].templateName; + let numberOfTokens = schedules[0].numberOfTokens; + let duration = schedules[0].duration; + let frequency = schedules[0].frequency; + let startTime = schedules[0].startTime; + await I_SecurityToken.approve(I_VestingEscrowWallet.address, numberOfTokens, {from: token_owner}); + await I_VestingEscrowWallet.depositTokens(numberOfTokens, {from: token_owner}); + const tx = await I_VestingEscrowWallet.addSchedule(account_beneficiary1, templateName, numberOfTokens, duration, frequency, startTime, {from: wallet_admin}); + + checkTemplateLog(tx.logs[0], templateName, numberOfTokens, duration, frequency); + checkScheduleLog(tx.logs[1], account_beneficiary1, templateName, startTime); + + let scheduleCount = await I_VestingEscrowWallet.getScheduleCount.call(account_beneficiary1); + assert.equal(scheduleCount, 1); + + let schedule = await I_VestingEscrowWallet.getSchedule.call(account_beneficiary1, templateName); + checkSchedule(schedule, numberOfTokens, duration, frequency, startTime, CREATED); + + let templates = await I_VestingEscrowWallet.getTemplateNames.call(account_beneficiary1); + assert.equal(web3.utils.hexToUtf8(templates[0]), templateName); + }); + + it("Should add vesting schedule without depositing to the beneficiary address", async () => { + let templateName = "template-2-01-2"; + let numberOfTokens = schedules[0].numberOfTokens; + let duration = schedules[0].duration; + let frequency = schedules[0].frequency; + let startTime = schedules[0].startTime; + await I_SecurityToken.approve(I_VestingEscrowWallet.address, numberOfTokens, {from: token_owner}); + const tx = await I_VestingEscrowWallet.addSchedule(account_beneficiary1, templateName, numberOfTokens, duration, frequency, startTime, {from: token_owner}); + + checkTemplateLog(tx.logs[0], templateName, numberOfTokens, duration, frequency); + assert.equal(tx.logs[1].args._numberOfTokens, numberOfTokens); + checkScheduleLog(tx.logs[2], account_beneficiary1, templateName, startTime); + + let scheduleCount = await I_VestingEscrowWallet.getScheduleCount.call(account_beneficiary1); + assert.equal(scheduleCount, 2); + + let schedule = await I_VestingEscrowWallet.getSchedule.call(account_beneficiary1, templateName); + checkSchedule(schedule, numberOfTokens, duration, frequency, startTime, CREATED); + + await I_VestingEscrowWallet.revokeSchedule(account_beneficiary1, templateName, {from: wallet_admin}); + await I_VestingEscrowWallet.removeTemplate(templateName, {from: wallet_admin}); + let unassignedTokens = await I_VestingEscrowWallet.unassignedTokens.call(); + await I_VestingEscrowWallet.sendToTreasury(unassignedTokens, {from: wallet_admin}); + }); + + it("Should fail to modify vesting schedule -- fail because schedule not found", async () => { + let templateName = "template-2-03"; + let startTime = schedules[0].startTime; + await catchRevert( + I_VestingEscrowWallet.modifySchedule(account_beneficiary1, templateName, startTime, {from: wallet_admin}) + ); + }); + + it("Should not be able to modify schedule -- fail because of permissions check", async () => { + await catchRevert( + I_VestingEscrowWallet.modifySchedule(account_beneficiary1, "template-2-01", latestTime() + 100, {from: account_beneficiary1}) + ); + }); + + it("Should modify vesting schedule for the beneficiary's address", async () => { + let templateName = "template-2-01"; + let numberOfTokens = schedules[0].numberOfTokens; + let duration = schedules[0].duration; + let frequency = schedules[0].frequency; + let startTime = schedules[1].startTime; + const tx = await I_VestingEscrowWallet.modifySchedule(account_beneficiary1, templateName, startTime, {from: wallet_admin}); + + checkScheduleLog(tx.logs[0], account_beneficiary1, templateName, startTime); + + let scheduleCount = await I_VestingEscrowWallet.getScheduleCount.call(account_beneficiary1); + assert.equal(scheduleCount.toNumber(), 1); + + let schedule = await I_VestingEscrowWallet.getSchedule.call(account_beneficiary1, "template-2-01"); + checkSchedule(schedule, numberOfTokens, duration, frequency, startTime, CREATED); + }); + + it("Should not be able to revoke schedule -- fail because of permissions check", async () => { + await catchRevert( + I_VestingEscrowWallet.revokeSchedule(account_beneficiary1, "template-2-01", {from: account_beneficiary1}) + ); + }); + + it("Should revoke vesting schedule from the beneficiary address", async () => { + let templateName = "template-2-01"; + const tx = await I_VestingEscrowWallet.revokeSchedule(account_beneficiary1, templateName, {from: wallet_admin}); + let unassignedTokens = await I_VestingEscrowWallet.unassignedTokens.call(); + await I_VestingEscrowWallet.sendToTreasury(unassignedTokens, {from: wallet_admin}); + + assert.equal(tx.logs[0].args._beneficiary, account_beneficiary1); + assert.equal(web3.utils.hexToUtf8(tx.logs[0].args._templateName), templateName); + + let scheduleCount = await I_VestingEscrowWallet.getScheduleCount.call(account_beneficiary1); + assert.equal(scheduleCount, 0); + + await I_VestingEscrowWallet.removeTemplate(templateName, {from: wallet_admin}) + }); + + it("Should fail to revoke vesting schedule -- fail because address is invalid", async () => { + await catchRevert( + I_VestingEscrowWallet.revokeSchedule(0, "template-2-01", {from: wallet_admin}) + ); + }); + + it("Should fail to revoke vesting schedule -- fail because schedule not found", async () => { + await catchRevert( + I_VestingEscrowWallet.revokeSchedule(account_beneficiary1, "template-2-02", {from: wallet_admin}) + ); + }); + + it("Should fail to revoke vesting schedules -- fail because address is invalid", async () => { + await catchRevert( + I_VestingEscrowWallet.revokeAllSchedules(0, {from: wallet_admin}) + ); + }); + + it("Should add 3 vesting schedules to the beneficiary address", async () => { + let totalNumberOfTokens = getTotalNumberOfTokens(schedules); + await I_SecurityToken.approve(I_VestingEscrowWallet.address, totalNumberOfTokens, {from: token_owner}); + await I_VestingEscrowWallet.depositTokens(totalNumberOfTokens, {from: token_owner}); + for (let i = 0; i < schedules.length; i++) { + let templateName = schedules[i].templateName; + let numberOfTokens = schedules[i].numberOfTokens; + let duration = schedules[i].duration; + let frequency = schedules[i].frequency; + let startTime = schedules[i].startTime; + const tx = await I_VestingEscrowWallet.addSchedule(account_beneficiary2, templateName, numberOfTokens, duration, frequency, startTime, {from: wallet_admin}); + + checkTemplateLog(tx.logs[0], templateName, numberOfTokens, duration, frequency); + checkScheduleLog(tx.logs[1], account_beneficiary2, templateName, startTime); + + let scheduleCount = await I_VestingEscrowWallet.getScheduleCount.call(account_beneficiary2); + assert.equal(scheduleCount, i + 1); + + let schedule = await I_VestingEscrowWallet.getSchedule.call(account_beneficiary2, templateName); + checkSchedule(schedule, numberOfTokens, duration, frequency, startTime, CREATED); + } + }); + + it("Should not be able to revoke schedules -- fail because of permissions check", async () => { + await catchRevert( + I_VestingEscrowWallet.revokeAllSchedules(account_beneficiary1, {from: account_beneficiary1}) + ); + }); + + it("Should revoke 1 of 3 vesting schedule from the beneficiary address", async () => { + let templateName = schedules[1].templateName; + const tx = await I_VestingEscrowWallet.revokeSchedule(account_beneficiary2, templateName, {from: wallet_admin}); + let unassignedTokens = await I_VestingEscrowWallet.unassignedTokens.call(); + await I_VestingEscrowWallet.sendToTreasury(unassignedTokens, {from: wallet_admin}); + + assert.equal(tx.logs[0].args._beneficiary, account_beneficiary2); + assert.equal(web3.utils.hexToUtf8(tx.logs[0].args._templateName), templateName); + + let scheduleCount = await I_VestingEscrowWallet.getScheduleCount.call(account_beneficiary2); + assert.equal(scheduleCount, 2); + }); + + it("Should revoke 2 vesting schedules from the beneficiary address", async () => { + const tx = await I_VestingEscrowWallet.revokeAllSchedules(account_beneficiary2, {from: wallet_admin}); + let unassignedTokens = await I_VestingEscrowWallet.unassignedTokens.call(); + await I_VestingEscrowWallet.sendToTreasury(unassignedTokens, {from: wallet_admin}); + + assert.equal(tx.logs[0].args._beneficiary, account_beneficiary2); + + let scheduleCount = await I_VestingEscrowWallet.getScheduleCount.call(account_beneficiary2); + assert.equal(scheduleCount, 0); + }); + + it("Should push available tokens during revoking vesting schedule", async () => { + let schedules = [ + { + templateName: "template-3-01", + numberOfTokens: 100000, + duration: durationUtil.minutes(4), + frequency: durationUtil.minutes(1) + }, + { + templateName: "template-3-02", + numberOfTokens: 30000, + duration: durationUtil.minutes(6), + frequency: durationUtil.minutes(1) + }, + { + templateName: "template-3-03", + numberOfTokens: 2000, + duration: durationUtil.minutes(10), + frequency: durationUtil.minutes(1) + } + ]; + + let totalNumberOfTokens = getTotalNumberOfTokens(schedules); + await I_SecurityToken.approve(I_VestingEscrowWallet.address, totalNumberOfTokens, {from: token_owner}); + await I_VestingEscrowWallet.depositTokens(totalNumberOfTokens, {from: token_owner}); + for (let i = 0; i < schedules.length; i++) { + let templateName = schedules[i].templateName; + let numberOfTokens = schedules[i].numberOfTokens; + let duration = schedules[i].duration; + let frequency = schedules[i].frequency; + let startTime = latestTime() + durationUtil.seconds(100); + await I_VestingEscrowWallet.addSchedule(account_beneficiary3, templateName, numberOfTokens, duration, frequency, startTime, {from: wallet_admin}); + } + let stepCount = 3; + await increaseTime(durationUtil.minutes(stepCount) + durationUtil.seconds(100)); + + const tx = await I_VestingEscrowWallet.revokeSchedule(account_beneficiary3, "template-3-01", {from: wallet_admin}); + assert.equal(tx.logs[0].args._beneficiary, account_beneficiary3); + assert.equal(tx.logs[0].args._numberOfTokens.toNumber(), 100000 / 4 * stepCount); + + let balance = await I_SecurityToken.balanceOf.call(account_beneficiary3); + assert.equal(balance.toNumber(), 100000 / 4 * stepCount); + + stepCount = 7; + await increaseTime(durationUtil.minutes(stepCount)); + + const tx2 = await I_VestingEscrowWallet.revokeAllSchedules(account_beneficiary3, {from: wallet_admin}); + assert.equal(tx2.logs[0].args._beneficiary, account_beneficiary3); + assert.equal(tx2.logs[0].args._numberOfTokens.toNumber(), 2000); + assert.equal(tx2.logs[1].args._beneficiary, account_beneficiary3); + assert.equal(tx2.logs[1].args._numberOfTokens.toNumber(), 30000); + + for (let i = 0; i < schedules.length; i++) { + await I_VestingEscrowWallet.removeTemplate(schedules[i].templateName, {from: wallet_admin}); + } + + balance = await I_SecurityToken.balanceOf.call(account_beneficiary3); + assert.equal(balance.toNumber(), totalNumberOfTokens - 100000 / 4); + + await I_SecurityToken.transfer(token_owner, balance, {from: account_beneficiary3}); + let unassignedTokens = await I_VestingEscrowWallet.unassignedTokens.call(); + await I_VestingEscrowWallet.sendToTreasury(unassignedTokens, {from: wallet_admin}); + }); + + }); + + describe("Adding, using and removing templates", async () => { + + let schedules = [ + { + templateName: "template-4-01", + numberOfTokens: 100000, + duration: durationUtil.years(4), + frequency: durationUtil.years(1), + startTime: latestTime() + durationUtil.days(1) + }, + { + templateName: "template-4-02", + numberOfTokens: 30000, + duration: durationUtil.weeks(6), + frequency: durationUtil.weeks(1), + startTime: latestTime() + durationUtil.days(2) + }, + { + templateName: "template-4-03", + numberOfTokens: 2000, + duration: durationUtil.days(10), + frequency: durationUtil.days(2), + startTime: latestTime() + durationUtil.days(3) + } + ]; + + it("Should not be able to add template -- fail because of permissions check", async () => { + await catchRevert( + I_VestingEscrowWallet.addTemplate("template-4-01", 25000, 4, 1, {from: account_beneficiary1}) + ); + }); + + it("Should not be able to add template -- fail because of invalid name", async () => { + await catchRevert( + I_VestingEscrowWallet.addTemplate("", 25000, 4, 1, {from: wallet_admin}) + ); + }); + + it("Should add 3 Templates", async () => { + let oldTemplateCount = await I_VestingEscrowWallet.getTemplateCount.call(); + for (let i = 0; i < schedules.length; i++) { + let templateName = schedules[i].templateName; + let numberOfTokens = schedules[i].numberOfTokens; + let duration = schedules[i].duration; + let frequency = schedules[i].frequency; + const tx = await I_VestingEscrowWallet.addTemplate(templateName, numberOfTokens, duration, frequency, {from: wallet_admin}); + + assert.equal(web3.utils.hexToUtf8(tx.logs[0].args._name), templateName); + assert.equal(tx.logs[0].args._numberOfTokens.toNumber(), numberOfTokens); + assert.equal(tx.logs[0].args._duration.toNumber(), duration); + assert.equal(tx.logs[0].args._frequency.toNumber(), frequency); + } + let templateNames = await I_VestingEscrowWallet.getAllTemplateNames.call(); + + for (let i = 0, j = oldTemplateCount; i < schedules.length; i++, j++) { + assert.equal(web3.utils.hexToUtf8(templateNames[j]), schedules[i].templateName); + } + }); + + it("Should not be able to add template -- fail because template already exists", async () => { + await catchRevert( + I_VestingEscrowWallet.addTemplate("template-4-01", 25000, 4, 1, {from: wallet_admin}) + ); + }); + + it("Should not be able to remove template -- fail because of permissions check", async () => { + await catchRevert( + I_VestingEscrowWallet.removeTemplate("template-4-02", {from: account_beneficiary1}) + ); + }); + + it("Should not be able to remove template -- fail because template not found", async () => { + await catchRevert( + I_VestingEscrowWallet.removeTemplate("template-444-02", {from: wallet_admin}) + ); + }); + + it("Should remove template", async () => { + const tx = await I_VestingEscrowWallet.removeTemplate("template-4-02", {from: wallet_admin}); + + assert.equal(web3.utils.hexToUtf8(tx.logs[0].args._name), "template-4-02"); + }); + + it("Should fail to add vesting schedule from template -- fail because template not found", async () => { + let startTime = schedules[2].startTime; + await catchRevert( + I_VestingEscrowWallet.addScheduleFromTemplate(account_beneficiary1, "template-4-02", startTime, {from: wallet_admin}) + ); + }); + + it("Should not be able to add schedule from template -- fail because of permissions check", async () => { + await catchRevert( + I_VestingEscrowWallet.addScheduleFromTemplate(account_beneficiary1, "template-4-01", latestTime(), {from: account_beneficiary1}) + ); + }); + + it("Should not be able to add vesting schedule from template -- fail because template not found", async () => { + await catchRevert( + I_VestingEscrowWallet.addScheduleFromTemplate(account_beneficiary1, "template-777", latestTime() + 100, {from: wallet_admin}) + ); + }); + + it("Should add vesting schedule from template", async () => { + let templateName = schedules[2].templateName; + let numberOfTokens = schedules[2].numberOfTokens; + let duration = schedules[2].duration; + let frequency = schedules[2].frequency; + let startTime = schedules[2].startTime; + await I_SecurityToken.approve(I_VestingEscrowWallet.address, numberOfTokens, { from: token_owner }); + await I_VestingEscrowWallet.depositTokens(numberOfTokens, {from: token_owner}); + const tx = await I_VestingEscrowWallet.addScheduleFromTemplate(account_beneficiary1, templateName, startTime, {from: wallet_admin}); + + checkScheduleLog(tx.logs[0], account_beneficiary1, templateName, startTime); + + let scheduleCount = await I_VestingEscrowWallet.getScheduleCount.call(account_beneficiary1); + assert.equal(scheduleCount, 1); + + let schedule = await I_VestingEscrowWallet.getSchedule.call(account_beneficiary1, templateName); + checkSchedule(schedule, numberOfTokens, duration, frequency, startTime, CREATED); + + await I_VestingEscrowWallet.revokeSchedule(account_beneficiary1, templateName, {from: wallet_admin}); + let unassignedTokens = await I_VestingEscrowWallet.unassignedTokens.call(); + await I_VestingEscrowWallet.sendToTreasury(unassignedTokens, {from: wallet_admin}); + }); + + it("Should not be able to add vesting schedule from template -- fail because template already added", async () => { + let templateName = schedules[2].templateName; + await catchRevert( + I_VestingEscrowWallet.addScheduleFromTemplate(account_beneficiary1, templateName, latestTime() + 100, {from: wallet_admin}) + ); + }); + + it("Should fail to remove template", async () => { + await catchRevert( + I_VestingEscrowWallet.removeTemplate("template-4-02", {from: wallet_admin}) + ); + }); + + it("Should remove 2 Templates", async () => { + let templateCount = await I_VestingEscrowWallet.getTemplateCount.call({from: wallet_admin}); + + await I_VestingEscrowWallet.removeTemplate("template-4-01", {from: wallet_admin}); + await I_VestingEscrowWallet.removeTemplate("template-4-03", {from: wallet_admin}); + + let templateCountAfterRemoving = await I_VestingEscrowWallet.getTemplateCount.call({from: wallet_admin}); + assert.equal(templateCount - templateCountAfterRemoving, 2); + }); + + }); + + describe("Tests for multi operations", async () => { + + let templateNames = ["template-5-01", "template-5-02", "template-5-03"]; + + it("Should not be able to add schedules to the beneficiaries -- fail because of permissions check", async () => { + let startTimes = [latestTime() + 100, latestTime() + 100, latestTime() + 100]; + await catchRevert( + I_VestingEscrowWallet.addScheduleMulti(beneficiaries, templateNames, [10000, 10000, 10000], [4, 4, 4], [1, 1, 1], startTimes, {from: account_beneficiary1}) + ); + }); + + it("Should not be able to add schedules to the beneficiaries -- fail because of arrays sizes mismatch", async () => { + let startTimes = [latestTime() + 100, latestTime() + 100, latestTime() + 100]; + let totalNumberOfTokens = 60000; + await I_SecurityToken.approve(I_VestingEscrowWallet.address, totalNumberOfTokens, {from: token_owner}); + await I_VestingEscrowWallet.depositTokens(totalNumberOfTokens, {from: token_owner}); + await catchRevert( + I_VestingEscrowWallet.addScheduleMulti(beneficiaries, templateNames, [20000, 30000, 10000], [4, 4], [1, 1, 1], startTimes, {from: wallet_admin}) + ); + let unassignedTokens = await I_VestingEscrowWallet.unassignedTokens.call(); + await I_VestingEscrowWallet.sendToTreasury(unassignedTokens, {from: wallet_admin}); + }); + + it("Should add schedules for 3 beneficiaries", async () => { + let numberOfTokens = [15000, 15000, 15000]; + let durations = [durationUtil.seconds(50), durationUtil.seconds(50), durationUtil.seconds(50)]; + let frequencies = [durationUtil.seconds(10), durationUtil.seconds(10), durationUtil.seconds(10)]; + let timeShift = durationUtil.seconds(100); + let startTimes = [latestTime() + timeShift, latestTime() + timeShift, latestTime() + timeShift]; + + let totalNumberOfTokens = 60000; + await I_SecurityToken.approve(I_VestingEscrowWallet.address, totalNumberOfTokens, {from: token_owner}); + await I_VestingEscrowWallet.depositTokens(totalNumberOfTokens, {from: token_owner}); + + let tx = await I_VestingEscrowWallet.addScheduleMulti(beneficiaries, templateNames, numberOfTokens, durations, frequencies, startTimes, {from: wallet_admin}); + + for (let i = 0; i < beneficiaries.length; i++) { + let templateName = templateNames[i]; + let beneficiary = beneficiaries[i]; + checkTemplateLog(tx.logs[i* 2], templateName, numberOfTokens[i], durations[i], frequencies[i]); + checkScheduleLog(tx.logs[i * 2 + 1], beneficiary, templateName, startTimes[i]); + + let scheduleCount = await I_VestingEscrowWallet.getScheduleCount.call(beneficiary); + assert.equal(scheduleCount, 1); + + let schedule = await I_VestingEscrowWallet.getSchedule.call(beneficiary, templateName); + checkSchedule(schedule, numberOfTokens[i], durations[i], frequencies[i], startTimes[i], CREATED); + } + }); + + it("Should not be able modify vesting schedule for 3 beneficiary's addresses -- fail because of arrays sizes mismatch", async () => { + let timeShift = durationUtil.seconds(100); + let startTimes = [latestTime() + timeShift, latestTime() + timeShift, latestTime() + timeShift]; + + await catchRevert( + I_VestingEscrowWallet.modifyScheduleMulti(beneficiaries, ["template-5-01"], startTimes, {from: wallet_admin}) + ); + }); + + it("Should not be able to modify schedules for the beneficiaries -- fail because of permissions check", async () => { + let timeShift = durationUtil.seconds(100); + let startTimes = [latestTime() + timeShift, latestTime() + timeShift, latestTime() + timeShift]; + + await catchRevert( + I_VestingEscrowWallet.modifyScheduleMulti(beneficiaries, templateNames, startTimes, {from: account_beneficiary1}) + ); + }); + + it("Should modify vesting schedule for 3 beneficiary's addresses", async () => { + let numberOfTokens = [15000, 15000, 15000]; + let durations = [durationUtil.seconds(50), durationUtil.seconds(50), durationUtil.seconds(50)]; + let frequencies = [durationUtil.seconds(10), durationUtil.seconds(10), durationUtil.seconds(10)]; + let timeShift = durationUtil.seconds(100); + let startTimes = [latestTime() + timeShift, latestTime() + timeShift, latestTime() + timeShift]; + + const tx = await I_VestingEscrowWallet.modifyScheduleMulti(beneficiaries, templateNames, startTimes, {from: wallet_admin}); + await increaseTime(timeShift + frequencies[0]); + + for (let i = 0; i < beneficiaries.length; i++) { + let log = tx.logs[i]; + let beneficiary = beneficiaries[i]; + checkScheduleLog(log, beneficiary, templateNames[i], startTimes[i]); + + let scheduleCount = await I_VestingEscrowWallet.getScheduleCount.call(beneficiary); + assert.equal(scheduleCount, 1); + + let schedule = await I_VestingEscrowWallet.getSchedule.call(beneficiary, templateNames[i]); + checkSchedule(schedule, numberOfTokens[i], durations[i], frequencies[i], startTimes[i], STARTED); + } + }); + + it("Should not be able to send available tokens to the beneficiaries addresses -- fail because of array size", async () => { + await catchRevert( + I_VestingEscrowWallet.pushAvailableTokensMulti(0, 3, {from: wallet_admin}) + ); + }); + + it("Should not be able to send available tokens to the beneficiaries -- fail because of permissions check", async () => { + await catchRevert( + I_VestingEscrowWallet.pushAvailableTokensMulti(0, 2, {from: account_beneficiary1}) + ); + }); + + it("Should send available tokens to the beneficiaries addresses", async () => { + const tx = await I_VestingEscrowWallet.pushAvailableTokensMulti(0, 2, {from: wallet_admin}); + + for (let i = 0; i < beneficiaries.length; i++) { + let log = tx.logs[i]; + let beneficiary = beneficiaries[i]; + assert.equal(log.args._numberOfTokens.toNumber(), 3000); + + let balance = await I_SecurityToken.balanceOf.call(beneficiary); + assert.equal(balance.toNumber(), 3000); + + await I_SecurityToken.transfer(token_owner, balance, {from: beneficiary}); + await I_VestingEscrowWallet.revokeAllSchedules(beneficiary, {from: wallet_admin}); + await I_VestingEscrowWallet.removeTemplate(templateNames[i], {from: wallet_admin}); + let unassignedTokens = await I_VestingEscrowWallet.unassignedTokens.call(); + await I_VestingEscrowWallet.sendToTreasury(unassignedTokens, {from: wallet_admin}); + } + }); + + it("Should not be able to add schedules from template to the beneficiaries -- fail because of permissions check", async () => { + let templateName = "template-6-01"; + let numberOfTokens = 18000; + let duration = durationUtil.weeks(3); + let frequency = durationUtil.weeks(1); + let templateNames = [templateName, templateName, templateName]; + let startTimes = [latestTime() + durationUtil.seconds(100), latestTime() + durationUtil.seconds(100), latestTime() + durationUtil.seconds(100)]; + + let totalNumberOfTokens = numberOfTokens * 3; + await I_SecurityToken.approve(I_VestingEscrowWallet.address, totalNumberOfTokens, {from: token_owner}); + await I_VestingEscrowWallet.depositTokens(totalNumberOfTokens, {from: token_owner}); + await I_VestingEscrowWallet.addTemplate(templateName, numberOfTokens, duration, frequency, {from: wallet_admin}); + + await catchRevert( + I_VestingEscrowWallet.addScheduleFromTemplateMulti(beneficiaries, templateNames, startTimes, {from: account_beneficiary1}) + ); + }); + + it("Should add schedules from template for 3 beneficiaries", async () => { + let templateName = "template-6-01"; + let numberOfTokens = 18000; + let duration = durationUtil.weeks(3); + let frequency = durationUtil.weeks(1); + let templateNames = [templateName, templateName, templateName]; + let startTimes = [latestTime() + 100, latestTime() + 100, latestTime() + 100]; + + let tx = await I_VestingEscrowWallet.addScheduleFromTemplateMulti(beneficiaries, templateNames, startTimes, {from: wallet_admin}); + for (let i = 0; i < beneficiaries.length; i++) { + let log = tx.logs[i]; + let beneficiary = beneficiaries[i]; + checkScheduleLog(log, beneficiary, templateName, startTimes[i]); + + let schedule = await I_VestingEscrowWallet.getSchedule.call(beneficiary, templateName); + checkSchedule(schedule, numberOfTokens, duration, frequency, startTimes[i], CREATED); + } + }); + + it("Should not be able to revoke schedules of the beneficiaries -- fail because of permissions check", async () => { + await catchRevert( + I_VestingEscrowWallet.revokeSchedulesMulti(beneficiaries, {from: account_beneficiary1}) + ); + }); + + it("Should revoke vesting schedule from the 3 beneficiary's addresses", async () => { + const tx = await I_VestingEscrowWallet.revokeSchedulesMulti(beneficiaries, {from: wallet_admin}); + + for (let i = 0; i < beneficiaries.length; i++) { + let log = tx.logs[i]; + let beneficiary = beneficiaries[i]; + assert.equal(log.args._beneficiary, beneficiary); + + let scheduleCount = await I_VestingEscrowWallet.getScheduleCount.call(beneficiary); + assert.equal(scheduleCount, 0); + } + + let unassignedTokens = await I_VestingEscrowWallet.unassignedTokens.call(); + await I_VestingEscrowWallet.sendToTreasury(unassignedTokens, {from: wallet_admin}); + }); + + }); + +}); + +function checkTemplateLog(log, templateName, numberOfTokens, duration, frequency) { + assert.equal(web3.utils.hexToUtf8(log.args._name), templateName); + assert.equal(log.args._numberOfTokens.toNumber(), numberOfTokens); + assert.equal(log.args._duration.toNumber(), duration); + assert.equal(log.args._frequency.toNumber(), frequency); +} + +function checkScheduleLog(log, beneficiary, templateName, startTime) { + assert.equal(log.args._beneficiary, beneficiary); + assert.equal(web3.utils.hexToUtf8(log.args._templateName), templateName); + assert.equal(log.args._startTime.toNumber(), startTime); +} + +function checkSchedule(schedule, numberOfTokens, duration, frequency, startTime, state) { + assert.equal(schedule[0].toNumber(), numberOfTokens); + assert.equal(schedule[1].toNumber(), duration); + assert.equal(schedule[2].toNumber(), frequency); + assert.equal(schedule[3].toNumber(), startTime); + assert.equal(schedule[5].toNumber(), state); +} + +function getTotalNumberOfTokens(schedules) { + let numberOfTokens = 0; + for (let i = 0; i < schedules.length; i++) { + numberOfTokens += schedules[i].numberOfTokens; + } + return numberOfTokens; +}