From c9cb83c5c6913d2ee2596c27622fbc320ecce0fc Mon Sep 17 00:00:00 2001 From: Satyam Agrawal Date: Fri, 12 Apr 2019 02:05:43 +0530 Subject: [PATCH] Port the VRTM audit fixes (#635) * port the VRTM audit fixes * cleanup code --- contracts/libraries/VolumeRestrictionLib.sol | 114 ++-- .../VRTM/VolumeRestrictionTM.sol | 491 ++++++++++-------- .../VRTM/VolumeRestrictionTMStorage.sol | 5 +- test/y_volume_restriction_tm.js | 223 +++++++- 4 files changed, 555 insertions(+), 278 deletions(-) diff --git a/contracts/libraries/VolumeRestrictionLib.sol b/contracts/libraries/VolumeRestrictionLib.sol index 7484c62fd..3672f216e 100644 --- a/contracts/libraries/VolumeRestrictionLib.sol +++ b/contracts/libraries/VolumeRestrictionLib.sol @@ -1,6 +1,7 @@ pragma solidity ^0.5.0; import "../interfaces/IDataStore.sol"; +import "./BokkyPooBahsDateTimeLibrary.sol"; import "openzeppelin-solidity/contracts/math/SafeMath.sol"; import "../modules/TransferManager/VRTM/VolumeRestrictionTMStorage.sol"; @@ -14,32 +15,12 @@ library VolumeRestrictionLib { bytes32 internal constant INVESTORSKEY = 0xdf3a8dd24acdd05addfc6aeffef7574d2de3f844535ec91e8e0f3e45dba96731; //keccak256(abi.encodePacked("INVESTORS")) bytes32 internal constant WHITELIST = "WHITELIST"; - function _checkLengthOfArray( - address[] memory _holders, - uint256[] memory _allowedTokens, - uint256[] memory _startTimes, - uint256[] memory _rollingPeriodInDays, - uint256[] memory _endTimes, - uint256[] memory _restrictionTypes - ) - internal - pure - { - require( - _holders.length == _allowedTokens.length && - _allowedTokens.length == _startTimes.length && - _startTimes.length == _rollingPeriodInDays.length && - _rollingPeriodInDays.length == _endTimes.length && - _endTimes.length == _restrictionTypes.length, - "Length mismatch" - ); - } function deleteHolderFromList( - mapping(address => uint8) storage holderToRestrictionType, + mapping(address => VolumeRestrictionTMStorage.TypeOfPeriod) storage _holderToRestrictionType, address _holder, IDataStore _dataStore, - uint8 _typeOfPeriod + VolumeRestrictionTMStorage.TypeOfPeriod _typeOfPeriod ) public { @@ -49,19 +30,19 @@ library VolumeRestrictionLib { // if removing restriction is individual then typeOfPeriod is TypeOfPeriod.OneDay // in uint8 its value is 1. if removing restriction is daily individual then typeOfPeriod // is TypeOfPeriod.MultipleDays in uint8 its value is 0. - if (holderToRestrictionType[_holder] != uint8(VolumeRestrictionTMStorage.TypeOfPeriod.Both)) { + if (_holderToRestrictionType[_holder] != VolumeRestrictionTMStorage.TypeOfPeriod.Both) { uint256 flags = _dataStore.getUint256(_getKey(INVESTORFLAGS, _holder)); flags = flags & ~(ONE << INDEX); _dataStore.setUint256(_getKey(INVESTORFLAGS, _holder), flags); } else { - holderToRestrictionType[_holder] = _typeOfPeriod; + _holderToRestrictionType[_holder] = _typeOfPeriod; } } function addRestrictionData( - mapping(address => uint8) storage holderToRestrictionType, + mapping(address => VolumeRestrictionTMStorage.TypeOfPeriod) storage _holderToRestrictionType, address _holder, - uint8 _callFrom, + VolumeRestrictionTMStorage.TypeOfPeriod _callFrom, uint256 _endTime, IDataStore _dataStore ) @@ -77,10 +58,37 @@ library VolumeRestrictionLib { flags = flags | (ONE << INDEX); _dataStore.setUint256(_getKey(INVESTORFLAGS, _holder), flags); } - uint8 _type = _getTypeOfPeriod(holderToRestrictionType[_holder], _callFrom, _endTime); - holderToRestrictionType[_holder] = _type; + VolumeRestrictionTMStorage.TypeOfPeriod _type = _getTypeOfPeriod(_holderToRestrictionType[_holder], _callFrom, _endTime); + _holderToRestrictionType[_holder] = _type; } + function isValidAmountAfterRestrictionChanges( + uint256 _amountTradedLastDay, + uint256 _amount, + uint256 _sumOfLastPeriod, + uint256 _allowedAmount, + uint256 _lastTradedTimestamp + ) + public + view + returns(bool) + { + // if restriction is to check whether the current transaction is performed within the 24 hours + // span after the last transaction performed by the user + if (BokkyPooBahsDateTimeLibrary.diffSeconds(_lastTradedTimestamp, now) < 86400) { + (,, uint256 lastTxDay) = BokkyPooBahsDateTimeLibrary.timestampToDate(_lastTradedTimestamp); + (,, uint256 currentTxDay) = BokkyPooBahsDateTimeLibrary.timestampToDate(now); + // This if statement is to check whether the last transaction timestamp (of `individualRestriction[_from]` + // when `_isDefault` is true or defaultRestriction when `_isDefault` is false) is comes within the same day of the current + // transaction timestamp or not. + if (lastTxDay == currentTxDay) { + // Not allow to transact more than the current transaction restriction allowed amount + if ((_sumOfLastPeriod.add(_amount)).add(_amountTradedLastDay) > _allowedAmount) + return false; + } + } + return true; + } /** * @notice Provide the restriction details of all the restricted addresses @@ -93,7 +101,7 @@ library VolumeRestrictionLib { * of the restriction corresponds to restricted address */ function getRestrictionData( - mapping(address => uint8) storage holderToRestrictionType, + mapping(address => VolumeRestrictionTMStorage.TypeOfPeriod) storage _holderToRestrictionType, VolumeRestrictionTMStorage.IndividualRestrictions storage _individualRestrictions, IDataStore _dataStore ) @@ -105,7 +113,7 @@ library VolumeRestrictionLib { uint256[] memory startTime, uint256[] memory rollingPeriodInDays, uint256[] memory endTime, - uint8[] memory typeOfRestriction + VolumeRestrictionTMStorage.RestrictionType[] memory typeOfRestriction ) { address[] memory investors = _dataStore.getAddressArray(INVESTORSKEY); @@ -113,7 +121,7 @@ library VolumeRestrictionLib { uint256 i; for (i = 0; i < investors.length; i++) { if (_isVolRestricted(_dataStore.getUint256(_getKey(INVESTORFLAGS, investors[i])))) { - counter = counter + (holderToRestrictionType[investors[i]] == uint8(2) ? 2 : 1); + counter = counter + (_holderToRestrictionType[investors[i]] == VolumeRestrictionTMStorage.TypeOfPeriod.Both ? 2 : 1); } } allAddresses = new address[](counter); @@ -121,18 +129,18 @@ library VolumeRestrictionLib { startTime = new uint256[](counter); rollingPeriodInDays = new uint256[](counter); endTime = new uint256[](counter); - typeOfRestriction = new uint8[](counter); + typeOfRestriction = new VolumeRestrictionTMStorage.RestrictionType[](counter); counter = 0; for (i = 0; i < investors.length; i++) { if (_isVolRestricted(_dataStore.getUint256(_getKey(INVESTORFLAGS, investors[i])))) { allAddresses[counter] = investors[i]; - if (holderToRestrictionType[investors[i]] == uint8(VolumeRestrictionTMStorage.TypeOfPeriod.MultipleDays)) { + if (_holderToRestrictionType[investors[i]] == VolumeRestrictionTMStorage.TypeOfPeriod.MultipleDays) { _setValues(_individualRestrictions.individualRestriction[investors[i]], allowedTokens, startTime, rollingPeriodInDays, endTime, typeOfRestriction, counter); } - else if (holderToRestrictionType[investors[i]] == uint8(VolumeRestrictionTMStorage.TypeOfPeriod.OneDay)) { + else if (_holderToRestrictionType[investors[i]] == VolumeRestrictionTMStorage.TypeOfPeriod.OneDay) { _setValues(_individualRestrictions.individualDailyRestriction[investors[i]], allowedTokens, startTime, rollingPeriodInDays, endTime, typeOfRestriction, counter); } - else if (holderToRestrictionType[investors[i]] == uint8(VolumeRestrictionTMStorage.TypeOfPeriod.Both)) { + else if (_holderToRestrictionType[investors[i]] == VolumeRestrictionTMStorage.TypeOfPeriod.Both) { _setValues(_individualRestrictions.individualRestriction[investors[i]], allowedTokens, startTime, rollingPeriodInDays, endTime, typeOfRestriction, counter); counter++; allAddresses[counter] = investors[i]; @@ -144,22 +152,22 @@ library VolumeRestrictionLib { } function _setValues( - VolumeRestrictionTMStorage.VolumeRestriction memory restriction, - uint256[] memory allowedTokens, - uint256[] memory startTime, - uint256[] memory rollingPeriodInDays, - uint256[] memory endTime, - uint8[] memory typeOfRestriction, - uint256 index + VolumeRestrictionTMStorage.VolumeRestriction memory _restriction, + uint256[] memory _allowedTokens, + uint256[] memory _startTime, + uint256[] memory _rollingPeriodInDays, + uint256[] memory _endTime, + VolumeRestrictionTMStorage.RestrictionType[] memory _typeOfRestriction, + uint256 _index ) internal pure { - allowedTokens[index] = restriction.allowedTokens; - startTime[index] = restriction.startTime; - rollingPeriodInDays[index] = restriction.rollingPeriodInDays; - endTime[index] = restriction.endTime; - typeOfRestriction[index] = uint8(restriction.typeOfRestriction); + _allowedTokens[_index] = _restriction.allowedTokens; + _startTime[_index] = _restriction.startTime; + _rollingPeriodInDays[_index] = _restriction.rollingPeriodInDays; + _endTime[_index] = _restriction.endTime; + _typeOfRestriction[_index] = _restriction.typeOfRestriction; } function _isVolRestricted(uint256 _flags) internal pure returns(bool) { @@ -167,9 +175,17 @@ library VolumeRestrictionLib { return (volRestricted > 0 ? true : false); } - function _getTypeOfPeriod(uint8 _currentTypeOfPeriod, uint8 _callFrom, uint256 _endTime) internal pure returns(uint8) { + function _getTypeOfPeriod( + VolumeRestrictionTMStorage.TypeOfPeriod _currentTypeOfPeriod, + VolumeRestrictionTMStorage.TypeOfPeriod _callFrom, + uint256 _endTime + ) + internal + pure + returns(VolumeRestrictionTMStorage.TypeOfPeriod) + { if (_currentTypeOfPeriod != _callFrom && _endTime != uint256(0)) - return uint8(VolumeRestrictionTMStorage.TypeOfPeriod.Both); + return VolumeRestrictionTMStorage.TypeOfPeriod.Both; else return _callFrom; } diff --git a/contracts/modules/TransferManager/VRTM/VolumeRestrictionTM.sol b/contracts/modules/TransferManager/VRTM/VolumeRestrictionTM.sol index ba574abcd..366742fcc 100644 --- a/contracts/modules/TransferManager/VRTM/VolumeRestrictionTM.sol +++ b/contracts/modules/TransferManager/VRTM/VolumeRestrictionTM.sol @@ -17,7 +17,7 @@ contract VolumeRestrictionTM is VolumeRestrictionTMStorage, TransferManager { uint256 _startTime, uint256 _rollingPeriodInDays, uint256 _endTime, - uint256 _typeOfRestriction + RestrictionType _typeOfRestriction ); // Emit when the new daily (Individual) restriction is added event AddIndividualDailyRestriction( @@ -26,7 +26,7 @@ contract VolumeRestrictionTM is VolumeRestrictionTMStorage, TransferManager { uint256 _startTime, uint256 _rollingPeriodInDays, uint256 _endTime, - uint256 _typeOfRestriction + RestrictionType _typeOfRestriction ); // Emit when the individual restriction is modified for a given address event ModifyIndividualRestriction( @@ -35,7 +35,7 @@ contract VolumeRestrictionTM is VolumeRestrictionTMStorage, TransferManager { uint256 _startTime, uint256 _rollingPeriodInDays, uint256 _endTime, - uint256 _typeOfRestriction + RestrictionType _typeOfRestriction ); // Emit when individual daily restriction get modified event ModifyIndividualDailyRestriction( @@ -44,7 +44,7 @@ contract VolumeRestrictionTM is VolumeRestrictionTMStorage, TransferManager { uint256 _startTime, uint256 _rollingPeriodInDays, uint256 _endTime, - uint256 _typeOfRestriction + RestrictionType _typeOfRestriction ); // Emit when the new global restriction is added event AddDefaultRestriction( @@ -52,7 +52,7 @@ contract VolumeRestrictionTM is VolumeRestrictionTMStorage, TransferManager { uint256 _startTime, uint256 _rollingPeriodInDays, uint256 _endTime, - uint256 _typeOfRestriction + RestrictionType _typeOfRestriction ); // Emit when the new daily (Default) restriction is added event AddDefaultDailyRestriction( @@ -60,7 +60,7 @@ contract VolumeRestrictionTM is VolumeRestrictionTMStorage, TransferManager { uint256 _startTime, uint256 _rollingPeriodInDays, uint256 _endTime, - uint256 _typeOfRestriction + RestrictionType _typeOfRestriction ); // Emit when default restriction get modified event ModifyDefaultRestriction( @@ -68,7 +68,7 @@ contract VolumeRestrictionTM is VolumeRestrictionTMStorage, TransferManager { uint256 _startTime, uint256 _rollingPeriodInDays, uint256 _endTime, - uint256 _typeOfRestriction + RestrictionType _typeOfRestriction ); // Emit when daily default restriction get modified event ModifyDefaultDailyRestriction( @@ -76,7 +76,7 @@ contract VolumeRestrictionTM is VolumeRestrictionTMStorage, TransferManager { uint256 _startTime, uint256 _rollingPeriodInDays, uint256 _endTime, - uint256 _typeOfRestriction + RestrictionType _typeOfRestriction ); // Emit when the individual restriction gets removed event IndividualRestrictionRemoved(address indexed _holder); @@ -171,12 +171,12 @@ contract VolumeRestrictionTM is VolumeRestrictionTMStorage, TransferManager { if ((individualRestrictions.individualRestriction[_from].endTime >= now && individualRestrictions.individualRestriction[_from].startTime <= now) || (individualRestrictions.individualDailyRestriction[_from].endTime >= now && individualRestrictions.individualDailyRestriction[_from].startTime <= now)) { - return _individualRestrictionCheck(_from, _amount); + return _restrictionCheck(_amount, _from, false, individualRestrictions.individualRestriction[_from]); // If the `_from` doesn't fall under the individual category. It will processed with in the global category automatically } else if ((globalRestrictions.defaultRestriction.endTime >= now && globalRestrictions.defaultRestriction.startTime <= now) || (globalRestrictions.defaultDailyRestriction.endTime >= now && globalRestrictions.defaultDailyRestriction.startTime <= now)) { - return _defaultRestrictionCheck(_from, _amount); + return _restrictionCheck(_amount, _from, true, globalRestrictions.defaultRestriction); } } return (Result.NA, 0, 0, 0, 0, 0, false); @@ -197,7 +197,7 @@ contract VolumeRestrictionTM is VolumeRestrictionTMStorage, TransferManager { exemptions.exemptAddresses[exemptions.exemptIndex[_wallet] - 1] = exemptions.exemptAddresses[exemptions.exemptAddresses.length - 1]; exemptions.exemptIndex[exemptions.exemptAddresses[exemptions.exemptIndex[_wallet] - 1]] = exemptions.exemptIndex[_wallet]; delete exemptions.exemptIndex[_wallet]; - exemptions.exemptAddresses.length --; + exemptions.exemptAddresses.length--; } emit ChangedExemptWalletList(_wallet, _change); } @@ -209,7 +209,8 @@ contract VolumeRestrictionTM is VolumeRestrictionTMStorage, TransferManager { * @param _startTime Unix timestamp at which restriction get into effect * @param _rollingPeriodInDays Rolling period in days (Minimum value should be 1 day) * @param _endTime Unix timestamp at which restriction effects will gets end. - * @param _restrictionType It will be 0 or 1 (i.e 0 for fixed while 1 for Percentage) + * @param _restrictionType Whether it will be `Fixed` (fixed no. of tokens allowed to transact) + * or `Percentage` (tokens are calculated as per the totalSupply in the fly). */ function addIndividualRestriction( address _holder, @@ -217,43 +218,41 @@ contract VolumeRestrictionTM is VolumeRestrictionTMStorage, TransferManager { uint256 _startTime, uint256 _rollingPeriodInDays, uint256 _endTime, - uint256 _restrictionType + RestrictionType _restrictionType ) public withPerm(ADMIN) { - require( - individualRestrictions.individualRestriction[_holder].endTime < now, - "Not Allowed" - ); - if (_startTime == 0) { - _startTime = now; - } + // It will help to reduce the chances of transaction failure (Specially when the issuer + // wants to set the startTime near to the current block.timestamp) and minting delayed because + // of the gas fee or network congestion that lead to the process block timestamp may grater + // than the given startTime. + uint256 startTime = _getValidStartTime(_startTime); require(_holder != address(0) && exemptions.exemptIndex[_holder] == 0, "Invalid address"); - _checkInputParams(_allowedTokens, _startTime, _rollingPeriodInDays, _endTime, _restrictionType, now); + _checkInputParams(_allowedTokens, startTime, _rollingPeriodInDays, _endTime, _restrictionType, now, false); if (individualRestrictions.individualRestriction[_holder].endTime != 0) { removeIndividualRestriction(_holder); } individualRestrictions.individualRestriction[_holder] = VolumeRestriction( _allowedTokens, - _startTime, + startTime, _rollingPeriodInDays, _endTime, - RestrictionType(_restrictionType) + _restrictionType ); VolumeRestrictionLib .addRestrictionData( holderToRestrictionType, _holder, - uint8(TypeOfPeriod.MultipleDays), + TypeOfPeriod.MultipleDays, individualRestrictions.individualRestriction[_holder].endTime, getDataStore() ); emit AddIndividualRestriction( _holder, _allowedTokens, - _startTime, + startTime, _rollingPeriodInDays, _endTime, _restrictionType @@ -266,48 +265,43 @@ contract VolumeRestrictionTM is VolumeRestrictionTMStorage, TransferManager { * @param _allowedTokens Amount of tokens allowed to be traded for all token holder. * @param _startTime Unix timestamp at which restriction get into effect * @param _endTime Unix timestamp at which restriction effects will gets end. - * @param _restrictionType It will be 0 or 1 (i.e 0 for fixed while 1 for Percentage) + * @param _restrictionType Whether it will be `Fixed` (fixed no. of tokens allowed to transact) + * or `Percentage` (tokens are calculated as per the totalSupply in the fly). */ function addIndividualDailyRestriction( address _holder, uint256 _allowedTokens, uint256 _startTime, uint256 _endTime, - uint256 _restrictionType + RestrictionType _restrictionType ) public withPerm(ADMIN) { - if (_startTime == 0) { - _startTime = now; - } - require( - individualRestrictions.individualDailyRestriction[_holder].endTime < now, - "Not Allowed" - ); - _checkInputParams(_allowedTokens, _startTime, 1, _endTime, _restrictionType, now); + uint256 startTime = _getValidStartTime(_startTime); + _checkInputParams(_allowedTokens, startTime, 1, _endTime, _restrictionType, now, false); if (individualRestrictions.individualDailyRestriction[_holder].endTime != 0) { removeIndividualDailyRestriction(_holder); } individualRestrictions.individualDailyRestriction[_holder] = VolumeRestriction( _allowedTokens, - _startTime, + startTime, 1, _endTime, - RestrictionType(_restrictionType) + _restrictionType ); VolumeRestrictionLib .addRestrictionData( holderToRestrictionType, _holder, - uint8(TypeOfPeriod.OneDay), + TypeOfPeriod.OneDay, individualRestrictions.individualRestriction[_holder].endTime, getDataStore() ); emit AddIndividualDailyRestriction( _holder, _allowedTokens, - _startTime, + startTime, 1, _endTime, _restrictionType @@ -320,19 +314,20 @@ contract VolumeRestrictionTM is VolumeRestrictionTMStorage, TransferManager { * @param _allowedTokens Array of amount of tokens allowed to be trade for a given address. * @param _startTimes Array of unix timestamps at which restrictions get into effect * @param _endTimes Array of unix timestamps at which restriction effects will gets end. - * @param _restrictionTypes Array of restriction types value will be 0 or 1 (i.e 0 for fixed while 1 for Percentage) + * @param _restrictionTypes Array of restriction types value whether it will be `Fixed` (fixed no. of tokens allowed to transact) + * or `Percentage` (tokens are calculated as per the totalSupply in the fly). */ function addIndividualDailyRestrictionMulti( address[] memory _holders, uint256[] memory _allowedTokens, uint256[] memory _startTimes, uint256[] memory _endTimes, - uint256[] memory _restrictionTypes + RestrictionType[] memory _restrictionTypes ) public { //NB - we duplicate _startTimes below to allow function reuse - VolumeRestrictionLib._checkLengthOfArray(_holders, _allowedTokens, _startTimes, _startTimes, _endTimes, _restrictionTypes); + _checkLengthOfArray(_holders, _allowedTokens, _startTimes, _startTimes, _endTimes, _restrictionTypes); for (uint256 i = 0; i < _holders.length; i++) { addIndividualDailyRestriction( _holders[i], @@ -351,7 +346,8 @@ contract VolumeRestrictionTM is VolumeRestrictionTMStorage, TransferManager { * @param _startTimes Array of unix timestamps at which restrictions get into effect * @param _rollingPeriodInDays Array of rolling period in days (Minimum value should be 1 day) * @param _endTimes Array of unix timestamps at which restriction effects will gets end. - * @param _restrictionTypes Array of restriction types value will be 0 or 1 (i.e 0 for fixed while 1 for Percentage) + * @param _restrictionTypes Array of restriction types value whether it will be `Fixed` (fixed no. of tokens allowed to transact) + * or `Percentage` (tokens are calculated as per the totalSupply in the fly). */ function addIndividualRestrictionMulti( address[] memory _holders, @@ -359,11 +355,11 @@ contract VolumeRestrictionTM is VolumeRestrictionTMStorage, TransferManager { uint256[] memory _startTimes, uint256[] memory _rollingPeriodInDays, uint256[] memory _endTimes, - uint256[] memory _restrictionTypes + RestrictionType[] memory _restrictionTypes ) public { - VolumeRestrictionLib._checkLengthOfArray(_holders, _allowedTokens, _startTimes, _rollingPeriodInDays, _endTimes, _restrictionTypes); + _checkLengthOfArray(_holders, _allowedTokens, _startTimes, _rollingPeriodInDays, _endTimes, _restrictionTypes); for (uint256 i = 0; i < _holders.length; i++) { addIndividualRestriction( _holders[i], @@ -382,33 +378,27 @@ contract VolumeRestrictionTM is VolumeRestrictionTMStorage, TransferManager { * @param _startTime Unix timestamp at which restriction get into effect * @param _rollingPeriodInDays Rolling period in days (Minimum value should be 1 day) * @param _endTime Unix timestamp at which restriction effects will gets end. - * @param _restrictionType It will be 0 or 1 (i.e 0 for fixed while 1 for Percentage) + * @param _restrictionType Whether it will be `Fixed` (fixed no. of tokens allowed to transact) + * or `Percentage` (tokens are calculated as per the totalSupply in the fly). */ function addDefaultRestriction( uint256 _allowedTokens, uint256 _startTime, uint256 _rollingPeriodInDays, uint256 _endTime, - uint256 _restrictionType + RestrictionType _restrictionType ) external withPerm(ADMIN) { - uint256 startTime = _startTime; - if (_startTime == 0) { - startTime = now; - } - require( - globalRestrictions.defaultRestriction.endTime < now, - "Not Allowed" - ); - _checkInputParams(_allowedTokens, startTime, _rollingPeriodInDays, _endTime, _restrictionType, now); + uint256 startTime = _getValidStartTime(_startTime); + _checkInputParams(_allowedTokens, startTime, _rollingPeriodInDays, _endTime, _restrictionType, now, false); globalRestrictions.defaultRestriction = VolumeRestriction( _allowedTokens, startTime, _rollingPeriodInDays, _endTime, - RestrictionType(_restrictionType) + _restrictionType ); emit AddDefaultRestriction( _allowedTokens, @@ -424,32 +414,26 @@ contract VolumeRestrictionTM is VolumeRestrictionTMStorage, TransferManager { * @param _allowedTokens Amount of tokens allowed to be traded for all token holder. * @param _startTime Unix timestamp at which restriction get into effect * @param _endTime Unix timestamp at which restriction effects will gets end. - * @param _restrictionType It will be 0 or 1 (i.e 0 for fixed while 1 for Percentage) + * @param _restrictionType Whether it will be `Fixed` (fixed no. of tokens allowed to transact) + * or `Percentage` (tokens are calculated as per the totalSupply in the fly). */ function addDefaultDailyRestriction( uint256 _allowedTokens, uint256 _startTime, uint256 _endTime, - uint256 _restrictionType + RestrictionType _restrictionType ) external withPerm(ADMIN) { - uint256 startTime = _startTime; - if (_startTime == 0) { - startTime = now; - } - require( - globalRestrictions.defaultDailyRestriction.endTime < now, - "Not Allowed" - ); - _checkInputParams(_allowedTokens, startTime, 1, _endTime, _restrictionType, now); + uint256 startTime = _getValidStartTime(_startTime); + _checkInputParams(_allowedTokens, startTime, 1, _endTime, _restrictionType, now, false); globalRestrictions.defaultDailyRestriction = VolumeRestriction( _allowedTokens, startTime, 1, _endTime, - RestrictionType(_restrictionType) + _restrictionType ); emit AddDefaultDailyRestriction( _allowedTokens, @@ -468,7 +452,7 @@ contract VolumeRestrictionTM is VolumeRestrictionTMStorage, TransferManager { require(_holder != address(0)); require(individualRestrictions.individualRestriction[_holder].endTime != 0); individualRestrictions.individualRestriction[_holder] = VolumeRestriction(0, 0, 0, 0, RestrictionType(0)); - VolumeRestrictionLib.deleteHolderFromList(holderToRestrictionType, _holder, getDataStore(), uint8(TypeOfPeriod.OneDay)); + VolumeRestrictionLib.deleteHolderFromList(holderToRestrictionType, _holder, getDataStore(), TypeOfPeriod.OneDay); bucketData.userToBucket[_holder].lastTradedDayTime = 0; bucketData.userToBucket[_holder].sumOfLastPeriod = 0; bucketData.userToBucket[_holder].daysCovered = 0; @@ -493,7 +477,7 @@ contract VolumeRestrictionTM is VolumeRestrictionTMStorage, TransferManager { require(_holder != address(0)); require(individualRestrictions.individualDailyRestriction[_holder].endTime != 0); individualRestrictions.individualDailyRestriction[_holder] = VolumeRestriction(0, 0, 0, 0, RestrictionType(0)); - VolumeRestrictionLib.deleteHolderFromList(holderToRestrictionType, _holder, getDataStore(), uint8(TypeOfPeriod.MultipleDays)); + VolumeRestrictionLib.deleteHolderFromList(holderToRestrictionType, _holder, getDataStore(), TypeOfPeriod.MultipleDays); bucketData.userToBucket[_holder].dailyLastTradedDayTime = 0; emit IndividualDailyRestrictionRemoved(_holder); } @@ -533,7 +517,8 @@ contract VolumeRestrictionTM is VolumeRestrictionTMStorage, TransferManager { * @param _startTime Unix timestamp at which restriction get into effect * @param _rollingPeriodInDays Rolling period in days (Minimum value should be 1 day) * @param _endTime Unix timestamp at which restriction effects will gets end. - * @param _restrictionType It will be 0 or 1 (i.e 0 for fixed while 1 for Percentage) + * @param _restrictionType Whether it will be `Fixed` (fixed no. of tokens allowed to transact) + * or `Percentage` (tokens are calculated as per the totalSupply in the fly). */ function modifyIndividualRestriction( address _holder, @@ -541,27 +526,25 @@ contract VolumeRestrictionTM is VolumeRestrictionTMStorage, TransferManager { uint256 _startTime, uint256 _rollingPeriodInDays, uint256 _endTime, - uint256 _restrictionType + RestrictionType _restrictionType ) public withPerm(ADMIN) { - if (_startTime == 0) { - _startTime = now; - } - require(individualRestrictions.individualRestriction[_holder].startTime > now, "Not Allowed"); - _checkInputParams(_allowedTokens, _startTime, _rollingPeriodInDays, _endTime, _restrictionType, now); + _isAllowedToModify(individualRestrictions.individualRestriction[_holder].startTime); + uint256 startTime = _getValidStartTime(_startTime); + _checkInputParams(_allowedTokens, startTime, _rollingPeriodInDays, _endTime, _restrictionType, now, false); individualRestrictions.individualRestriction[_holder] = VolumeRestriction( _allowedTokens, - _startTime, + startTime, _rollingPeriodInDays, _endTime, - RestrictionType(_restrictionType) + _restrictionType ); emit ModifyIndividualRestriction( _holder, _allowedTokens, - _startTime, + startTime, _rollingPeriodInDays, _endTime, _restrictionType @@ -576,34 +559,33 @@ contract VolumeRestrictionTM is VolumeRestrictionTMStorage, TransferManager { * @param _allowedTokens Amount of tokens allowed to be trade for a given address. * @param _startTime Unix timestamp at which restriction get into effect * @param _endTime Unix timestamp at which restriction effects will gets end. - * @param _restrictionType It will be 0 or 1 (i.e 0 for fixed while 1 for Percentage) + * @param _restrictionType Whether it will be `Fixed` (fixed no. of tokens allowed to transact) + * or `Percentage` (tokens are calculated as per the totalSupply in the fly). */ function modifyIndividualDailyRestriction( address _holder, uint256 _allowedTokens, uint256 _startTime, uint256 _endTime, - uint256 _restrictionType + RestrictionType _restrictionType ) public withPerm(ADMIN) { - if (_startTime == 0) { - _startTime = now; - } - uint checkTime = (individualRestrictions.individualDailyRestriction[_holder].startTime <= now ? individualRestrictions.individualDailyRestriction[_holder].startTime : now); - _checkInputParams(_allowedTokens, _startTime, 1, _endTime, _restrictionType, checkTime); + uint256 startTime = _getValidStartTime(_startTime); + uint256 checkTime = (individualRestrictions.individualDailyRestriction[_holder].startTime <= now ? individualRestrictions.individualDailyRestriction[_holder].startTime : now); + _checkInputParams(_allowedTokens, startTime, 1, _endTime, _restrictionType, checkTime, true); individualRestrictions.individualDailyRestriction[_holder] = VolumeRestriction( _allowedTokens, - _startTime, + startTime, 1, _endTime, - RestrictionType(_restrictionType) + _restrictionType ); emit ModifyIndividualDailyRestriction( _holder, _allowedTokens, - _startTime, + startTime, 1, _endTime, _restrictionType @@ -616,19 +598,20 @@ contract VolumeRestrictionTM is VolumeRestrictionTMStorage, TransferManager { * @param _allowedTokens Array of amount of tokens allowed to be trade for a given address. * @param _startTimes Array of unix timestamps at which restrictions get into effect * @param _endTimes Array of unix timestamps at which restriction effects will gets end. - * @param _restrictionTypes Array of restriction types value will be 0 or 1 (i.e 0 for fixed while 1 for Percentage) + * @param _restrictionTypes Array of restriction types value whether it will be `Fixed` (fixed no. of tokens allowed to transact) + * or `Percentage` (tokens are calculated as per the totalSupply in the fly). */ function modifyIndividualDailyRestrictionMulti( address[] memory _holders, uint256[] memory _allowedTokens, uint256[] memory _startTimes, uint256[] memory _endTimes, - uint256[] memory _restrictionTypes + RestrictionType[] memory _restrictionTypes ) public { //NB - we duplicate _startTimes below to allow function reuse - VolumeRestrictionLib._checkLengthOfArray(_holders, _allowedTokens, _startTimes, _startTimes, _endTimes, _restrictionTypes); + _checkLengthOfArray(_holders, _allowedTokens, _startTimes, _startTimes, _endTimes, _restrictionTypes); for (uint256 i = 0; i < _holders.length; i++) { modifyIndividualDailyRestriction( _holders[i], @@ -647,7 +630,8 @@ contract VolumeRestrictionTM is VolumeRestrictionTMStorage, TransferManager { * @param _startTimes Array of unix timestamps at which restrictions get into effect * @param _rollingPeriodInDays Array of rolling period in days (Minimum value should be 1 day) * @param _endTimes Array of unix timestamps at which restriction effects will gets end. - * @param _restrictionTypes Array of restriction types value will be 0 or 1 (i.e 0 for fixed while 1 for Percentage) + * @param _restrictionTypes Array of restriction types value whether it will be `Fixed` (fixed no. of tokens allowed to transact) + * or `Percentage` (tokens are calculated as per the totalSupply in the fly). */ function modifyIndividualRestrictionMulti( address[] memory _holders, @@ -655,11 +639,11 @@ contract VolumeRestrictionTM is VolumeRestrictionTMStorage, TransferManager { uint256[] memory _startTimes, uint256[] memory _rollingPeriodInDays, uint256[] memory _endTimes, - uint256[] memory _restrictionTypes + RestrictionType[] memory _restrictionTypes ) public { - VolumeRestrictionLib._checkLengthOfArray(_holders, _allowedTokens, _startTimes, _rollingPeriodInDays, _endTimes, _restrictionTypes); + _checkLengthOfArray(_holders, _allowedTokens, _startTimes, _rollingPeriodInDays, _endTimes, _restrictionTypes); for (uint256 i = 0; i < _holders.length; i++) { modifyIndividualRestriction( _holders[i], @@ -678,30 +662,28 @@ contract VolumeRestrictionTM is VolumeRestrictionTMStorage, TransferManager { * @param _startTime Unix timestamp at which restriction get into effect * @param _rollingPeriodInDays Rolling period in days (Minimum value should be 1 day) * @param _endTime Unix timestamp at which restriction effects will gets end. - * @param _restrictionType It will be 0 or 1 (i.e 0 for fixed while 1 for Percentage) + * @param _restrictionType Whether it will be `Fixed` (fixed no. of tokens allowed to transact) + * or `Percentage` (tokens are calculated as per the totalSupply in the fly). */ function modifyDefaultRestriction( uint256 _allowedTokens, uint256 _startTime, uint256 _rollingPeriodInDays, uint256 _endTime, - uint256 _restrictionType + RestrictionType _restrictionType ) external withPerm(ADMIN) { - require(globalRestrictions.defaultRestriction.startTime > now, "Not Allowed"); - uint256 startTime = _startTime; - if (_startTime == 0) { - startTime = now; - } - _checkInputParams(_allowedTokens, startTime, _rollingPeriodInDays, _endTime, _restrictionType, now); + _isAllowedToModify(globalRestrictions.defaultRestriction.startTime); + uint256 startTime = _getValidStartTime(_startTime); + _checkInputParams(_allowedTokens, startTime, _rollingPeriodInDays, _endTime, _restrictionType, now, false); globalRestrictions.defaultRestriction = VolumeRestriction( _allowedTokens, startTime, _rollingPeriodInDays, _endTime, - RestrictionType(_restrictionType) + _restrictionType ); emit ModifyDefaultRestriction( _allowedTokens, @@ -719,32 +701,31 @@ contract VolumeRestrictionTM is VolumeRestrictionTMStorage, TransferManager { * @param _allowedTokens Amount of tokens allowed to be traded for all token holder. * @param _startTime Unix timestamp at which restriction get into effect * @param _endTime Unix timestamp at which restriction effects will gets end. - * @param _restrictionType It will be 0 or 1 (i.e 0 for fixed while 1 for Percentage) + * @param _restrictionType Whether it will be `Fixed` (fixed no. of tokens allowed to transact) + * or `Percentage` (tokens are calculated as per the totalSupply in the fly). */ function modifyDefaultDailyRestriction( uint256 _allowedTokens, uint256 _startTime, uint256 _endTime, - uint256 _restrictionType + RestrictionType _restrictionType ) external withPerm(ADMIN) { - uint256 startTime = _startTime; - if (_startTime == 0) { - startTime = now; - } + uint256 startTime = _getValidStartTime(_startTime); // If old startTime is already passed then new startTime should be greater than or equal to the // old startTime otherwise any past startTime can be allowed in compare to earlier startTime. _checkInputParams(_allowedTokens, startTime, 1, _endTime, _restrictionType, - (globalRestrictions.defaultDailyRestriction.startTime <= now ? globalRestrictions.defaultDailyRestriction.startTime : now) + (globalRestrictions.defaultDailyRestriction.startTime <= now ? globalRestrictions.defaultDailyRestriction.startTime : now), + true ); globalRestrictions.defaultDailyRestriction = VolumeRestriction( _allowedTokens, startTime, 1, _endTime, - RestrictionType(_restrictionType) + _restrictionType ); emit ModifyDefaultDailyRestriction( _allowedTokens, @@ -759,27 +740,36 @@ contract VolumeRestrictionTM is VolumeRestrictionTMStorage, TransferManager { * @notice Internal function used to validate the transaction for a given address * If it validates then it also update the storage corressponds to the default restriction */ - function _defaultRestrictionCheck(address _from, uint256 _amount) internal view returns ( + function _restrictionCheck( + uint256 _amount, + address _from, + bool _isDefault, + VolumeRestriction memory _restriction + ) + internal + view + returns ( Result success, uint256 fromTimestamp, uint256 sumOfLastPeriod, uint256 daysCovered, uint256 dailyTime, uint256 endTime, - bool isGlobal + bool allowedDaily ) { // using the variable to avoid stack too deep error - BucketDetails storage bucketDetails = bucketData.defaultUserToBucket[_from]; - daysCovered = globalRestrictions.defaultRestriction.rollingPeriodInDays; + VolumeRestriction memory dailyRestriction = _isDefault ? globalRestrictions.defaultDailyRestriction :individualRestrictions.individualDailyRestriction[_from]; + BucketDetails memory _bucketDetails = _isDefault ? bucketData.defaultUserToBucket[_from]: bucketData.userToBucket[_from]; + daysCovered = _restriction.rollingPeriodInDays; + // This variable works for both allowedDefault or allowedIndividual bool allowedDefault = true; - bool allowedDaily; - if (globalRestrictions.defaultRestriction.endTime >= now && globalRestrictions.defaultRestriction.startTime <= now) { - if (bucketDetails.lastTradedDayTime < globalRestrictions.defaultRestriction.startTime) { + if (_restriction.endTime >= now && _restriction.startTime <= now) { + if (_bucketDetails.lastTradedDayTime < _restriction.startTime) { // It will execute when the txn is performed first time after the addition of individual restriction - fromTimestamp = globalRestrictions.defaultRestriction.startTime; + fromTimestamp = _restriction.startTime; } else { // Picking up the last timestamp - fromTimestamp = bucketDetails.lastTradedDayTime; + fromTimestamp = _bucketDetails.lastTradedDayTime; } // Check with the bucket and parse all the new timestamps to calculate the sumOfLastPeriod @@ -788,74 +778,64 @@ contract VolumeRestrictionTM is VolumeRestrictionTMStorage, TransferManager { fromTimestamp, BokkyPooBahsDateTimeLibrary.diffDays(fromTimestamp, now), _from, + _isDefault, daysCovered, - bucketDetails + _bucketDetails ); // validation of the transaction amount - if (!_checkValidAmountToTransact(sumOfLastPeriod, _amount, globalRestrictions.defaultRestriction)) { + if ( + !_checkValidAmountToTransact( + _isDefault, _amount, _from, sumOfLastPeriod, _restriction + ) + ) { allowedDefault = false; } } - (allowedDaily, dailyTime) = _dailyTxCheck(_from, _amount, bucketDetails.dailyLastTradedDayTime, globalRestrictions.defaultDailyRestriction); + + (allowedDaily, dailyTime) = _dailyTxCheck(_isDefault, _amount, _from, _bucketDetails.dailyLastTradedDayTime, dailyRestriction); success = ((allowedDaily && allowedDefault) == true ? Result.NA : Result.INVALID); - endTime = globalRestrictions.defaultDailyRestriction.endTime; - isGlobal = true; + endTime = dailyRestriction.endTime; + allowedDaily = _isDefault; } /** - * @notice Internal function used to validate the transaction for a given address - * If it validates then it also update the storage corressponds to the individual restriction + * @notice The function is used to check specific edge case where the user restriction type change from + * default to individual or vice versa. It will return true when last transaction traded by the user + * and the current txn timestamp lies in the same day. + * NB - Instead of comparing the current day transaction amount, we are comparing the total amount traded + * on the lastTradedDayTime that makes the restriction strict. The reason is not availability of amount + * that transacted on the current day (because of bucket desgin). */ - function _individualRestrictionCheck(address _from, uint256 _amount) internal view returns ( - Result success, - uint256 fromTimestamp, - uint256 sumOfLastPeriod, - uint256 daysCovered, - uint256 dailyTime, - uint256 endTime, - bool allowedDaily - ) { - // using the variable to avoid stack too deep error - BucketDetails memory bucketDetails = bucketData.userToBucket[_from]; - VolumeRestriction memory dailyRestriction = individualRestrictions.individualDailyRestriction[_from]; - VolumeRestriction memory restriction = individualRestrictions.individualRestriction[_from]; - daysCovered = restriction.rollingPeriodInDays; - bool allowedIndividual = true; - if (restriction.endTime >= now && restriction.startTime <= now) { - if (bucketDetails.lastTradedDayTime < restriction.startTime) { - // It will execute when the txn is performed first time after the addition of individual restriction - fromTimestamp = restriction.startTime; - } else { - // Picking up the last timestamp - fromTimestamp = bucketDetails.lastTradedDayTime; - } - - // Check with the bucket and parse all the new timestamps to calculate the sumOfLastPeriod - // re-using the local variables to avoid the stack too deep error. - (sumOfLastPeriod, fromTimestamp, daysCovered) = _bucketCheck( - fromTimestamp, - BokkyPooBahsDateTimeLibrary.diffDays(fromTimestamp, now), - _from, - daysCovered, - bucketDetails - ); - // validation of the transaction amount - if (!_checkValidAmountToTransact(sumOfLastPeriod, _amount, restriction)) { - allowedIndividual = false; - } - } - (allowedDaily, dailyTime) = _dailyTxCheck(_from, _amount, bucketDetails.dailyLastTradedDayTime, dailyRestriction); - success = ((allowedDaily && allowedIndividual) ? Result.NA : Result.INVALID); - endTime = dailyRestriction.endTime; - allowedDaily = false; + function _isValidAmountAfterRestrictionChanges( + bool _isDefault, + address _from, + uint256 _amount, + uint256 _sumOfLastPeriod, + uint256 _allowedAmount + ) + internal + view + returns(bool) + { + // Always use the alternate bucket details as per the current transaction restriction + BucketDetails storage bucketDetails = _isDefault ? bucketData.userToBucket[_from] : bucketData.defaultUserToBucket[_from]; + uint256 amountTradedLastDay = _isDefault ? bucketData.bucket[_from][bucketDetails.lastTradedDayTime]: bucketData.defaultBucket[_from][bucketDetails.lastTradedDayTime]; + return VolumeRestrictionLib.isValidAmountAfterRestrictionChanges( + amountTradedLastDay, + _amount, + _sumOfLastPeriod, + _allowedAmount, + bucketDetails.lastTradedTimestamp + ); } function _dailyTxCheck( - address from, - uint256 amount, - uint256 dailyLastTradedDayTime, - VolumeRestriction memory restriction + bool _isDefault, + uint256 _amount, + address _from, + uint256 _dailyLastTradedDayTime, + VolumeRestriction memory _restriction ) internal view @@ -863,18 +843,30 @@ contract VolumeRestrictionTM is VolumeRestrictionTMStorage, TransferManager { { // Checking whether the daily restriction is added or not if yes then calculate // the total amount get traded on a particular day (~ _fromTime) - if ( now <= restriction.endTime && now >= restriction.startTime) { + if ( now <= _restriction.endTime && now >= _restriction.startTime) { uint256 txSumOfDay = 0; - if (dailyLastTradedDayTime == 0 || dailyLastTradedDayTime < restriction.startTime) + if (_dailyLastTradedDayTime == 0 || _dailyLastTradedDayTime < _restriction.startTime) // This if condition will be executed when the individual daily restriction executed first time - dailyLastTradedDayTime = restriction.startTime.add(BokkyPooBahsDateTimeLibrary.diffDays(restriction.startTime, now).mul(1 days)); - else if (now.sub(dailyLastTradedDayTime) >= 1 days) - dailyLastTradedDayTime = dailyLastTradedDayTime.add(BokkyPooBahsDateTimeLibrary.diffDays(dailyLastTradedDayTime, now).mul(1 days)); + _dailyLastTradedDayTime = _restriction.startTime.add(BokkyPooBahsDateTimeLibrary.diffDays(_restriction.startTime, now).mul(1 days)); + else if (now.sub(_dailyLastTradedDayTime) >= 1 days) + _dailyLastTradedDayTime = _dailyLastTradedDayTime.add(BokkyPooBahsDateTimeLibrary.diffDays(_dailyLastTradedDayTime, now).mul(1 days)); // Assgining total sum traded on dailyLastTradedDayTime timestamp - txSumOfDay = bucketData.bucket[from][dailyLastTradedDayTime]; - return (_checkValidAmountToTransact(txSumOfDay, amount, restriction), dailyLastTradedDayTime); + if (_isDefault) + txSumOfDay = bucketData.defaultBucket[_from][_dailyLastTradedDayTime]; + else + txSumOfDay = bucketData.bucket[_from][_dailyLastTradedDayTime]; + return ( + _checkValidAmountToTransact( + _isDefault, + _amount, + _from, + txSumOfDay, + _restriction + ), + _dailyLastTradedDayTime + ); } - return (true, dailyLastTradedDayTime); + return (true, _dailyLastTradedDayTime); } /// Internal function for the bucket check @@ -882,6 +874,7 @@ contract VolumeRestrictionTM is VolumeRestrictionTMStorage, TransferManager { uint256 _fromTime, uint256 _diffDays, address _from, + bool isDefault, uint256 _rollingPeriodInDays, BucketDetails memory _bucketDetails ) @@ -909,7 +902,10 @@ contract VolumeRestrictionTM is VolumeRestrictionTMStorage, TransferManager { // Loop starts from the first day covered in sumOfLastPeriod upto the day that is covered by rolling period. uint256 temp = _bucketDetails.daysCovered.sub(counter.sub(_rollingPeriodInDays)); temp = _bucketDetails.lastTradedDayTime.sub(temp.mul(1 days)); - sumOfLastPeriod = sumOfLastPeriod.sub(bucketData.bucket[_from][temp]); + if (isDefault) + sumOfLastPeriod = sumOfLastPeriod.sub(bucketData.defaultBucket[_from][temp]); + else + sumOfLastPeriod = sumOfLastPeriod.sub(bucketData.bucket[_from][temp]); } // Adding the last amount that is transacted on the `_fromTime` not actually doing it but left written to understand // the alogrithm @@ -924,22 +920,25 @@ contract VolumeRestrictionTM is VolumeRestrictionTMStorage, TransferManager { } function _checkValidAmountToTransact( - uint256 _sumOfLastPeriod, + bool _isDefault, uint256 _amountToTransact, + address _from, + uint256 _sumOfLastPeriod, VolumeRestriction memory _restriction ) internal view returns (bool) { - uint256 _allowedAmount = 0; + uint256 allowedAmount; if (_restriction.typeOfRestriction == RestrictionType.Percentage) { - _allowedAmount = (_restriction.allowedTokens.mul(IERC20(securityToken).totalSupply())) / uint256(10) ** 18; + allowedAmount = (_restriction.allowedTokens.mul(ISecurityToken(securityToken).totalSupply())) / uint256(10) ** 18; } else { - _allowedAmount = _restriction.allowedTokens; + allowedAmount = _restriction.allowedTokens; } // Validation on the amount to transact - return (_allowedAmount >= _sumOfLastPeriod.add(_amountToTransact)); + bool allowed = allowedAmount >= _sumOfLastPeriod.add(_amountToTransact); + return (allowed && _isValidAmountAfterRestrictionChanges(_isDefault, _from, _amountToTransact, _sumOfLastPeriod, allowedAmount)); } function _updateStorage( @@ -957,11 +956,11 @@ contract VolumeRestrictionTM is VolumeRestrictionTMStorage, TransferManager { if (isDefault){ BucketDetails storage defaultUserToBucketDetails = bucketData.defaultUserToBucket[_from]; - _updateStorageActual(_from, _amount, _lastTradedDayTime, _sumOfLastPeriod, _daysCovered, _dailyLastTradedDayTime, _endTime, defaultUserToBucketDetails); + _updateStorageActual(_from, _amount, _lastTradedDayTime, _sumOfLastPeriod, _daysCovered, _dailyLastTradedDayTime, _endTime, true, defaultUserToBucketDetails); } else { BucketDetails storage userToBucketDetails = bucketData.userToBucket[_from]; - _updateStorageActual(_from, _amount, _lastTradedDayTime, _sumOfLastPeriod, _daysCovered, _dailyLastTradedDayTime, _endTime, userToBucketDetails); + _updateStorageActual(_from, _amount, _lastTradedDayTime, _sumOfLastPeriod, _daysCovered, _dailyLastTradedDayTime, _endTime, false, userToBucketDetails); } } @@ -973,6 +972,7 @@ contract VolumeRestrictionTM is VolumeRestrictionTMStorage, TransferManager { uint256 _daysCovered, uint256 _dailyLastTradedDayTime, uint256 _endTime, + bool isDefault, BucketDetails storage details ) internal @@ -989,16 +989,24 @@ contract VolumeRestrictionTM is VolumeRestrictionTMStorage, TransferManager { if (details.daysCovered != _daysCovered) { details.daysCovered = _daysCovered; } + // Assigning the latest transaction timestamp + details.lastTradedTimestamp = now; if (_amount != 0) { if (_lastTradedDayTime !=0) { details.sumOfLastPeriod = _sumOfLastPeriod.add(_amount); // Increasing the total amount of the day by `_amount` - bucketData.bucket[_from][_lastTradedDayTime] = bucketData.bucket[_from][_lastTradedDayTime].add(_amount); + if (isDefault) + bucketData.defaultBucket[_from][_lastTradedDayTime] = bucketData.defaultBucket[_from][_lastTradedDayTime].add(_amount); + else + bucketData.bucket[_from][_lastTradedDayTime] = bucketData.bucket[_from][_lastTradedDayTime].add(_amount); } if ((_dailyLastTradedDayTime != _lastTradedDayTime) && _dailyLastTradedDayTime != 0 && now <= _endTime) { // Increasing the total amount of the day by `_amount` - bucketData.bucket[_from][_dailyLastTradedDayTime] = bucketData.bucket[_from][_dailyLastTradedDayTime].add(_amount); + if (isDefault) + bucketData.defaultBucket[_from][_dailyLastTradedDayTime] = bucketData.defaultBucket[_from][_dailyLastTradedDayTime].add(_amount); + else + bucketData.bucket[_from][_dailyLastTradedDayTime] = bucketData.bucket[_from][_dailyLastTradedDayTime].add(_amount); } } } @@ -1008,30 +1016,39 @@ contract VolumeRestrictionTM is VolumeRestrictionTMStorage, TransferManager { uint256 _startTime, uint256 _rollingPeriodDays, uint256 _endTime, - uint256 _restrictionType, - uint256 _earliestStartTime + RestrictionType _restrictionType, + uint256 _earliestStartTime, + bool isModifyDaily ) internal pure { - require(_restrictionType == 0 || _restrictionType == 1, "Invalid type"); - require(_startTime >= _earliestStartTime, "Invalid startTime"); - if (_restrictionType == uint256(RestrictionType.Fixed)) { - require(_allowedTokens > 0, "Invalid value"); - } else { - require( - _allowedTokens > 0 && _allowedTokens <= 100 * 10 ** 16, - "Invalid value" - ); + if (isModifyDaily) + require(_startTime >= _earliestStartTime, "Invalid startTime"); + else + require(_startTime > _earliestStartTime, "Invalid startTime"); + require(_allowedTokens > 0); + if (_restrictionType != RestrictionType.Fixed) { + require(_allowedTokens <= 100 * 10 ** 16, "Invalid value"); } // Maximum limit for the rollingPeriod is 365 days require(_rollingPeriodDays >= 1 && _rollingPeriodDays <= 365, "Invalid rollingperiod"); require( - BokkyPooBahsDateTimeLibrary.diffDays(_startTime, _endTime) >= _rollingPeriodDays && _endTime > _startTime, + BokkyPooBahsDateTimeLibrary.diffDays(_startTime, _endTime) >= _rollingPeriodDays, "Invalid times" ); } + function _isAllowedToModify(uint256 _startTime) internal view { + require(_startTime > now); + } + + function _getValidStartTime(uint256 _startTime) internal view returns(uint256) { + if (_startTime == 0) + _startTime = now + 1; + return _startTime; + } + /** * @notice Use to get the bucket details for a given address * @param _user Address of the token holder for whom the bucket details has queried @@ -1039,14 +1056,10 @@ contract VolumeRestrictionTM is VolumeRestrictionTMStorage, TransferManager { * @return uint256 sumOfLastPeriod * @return uint256 days covered * @return uint256 24h lastTradedDayTime + * @return uint256 Timestamp at which last transaction get executed */ - function getIndividualBucketDetailsToUser(address _user) public view returns(uint256, uint256, uint256, uint256) { - return( - bucketData.userToBucket[_user].lastTradedDayTime, - bucketData.userToBucket[_user].sumOfLastPeriod, - bucketData.userToBucket[_user].daysCovered, - bucketData.userToBucket[_user].dailyLastTradedDayTime - ); + function getIndividualBucketDetailsToUser(address _user) public view returns(uint256, uint256, uint256, uint256, uint256) { + return _getBucketDetails(bucketData.userToBucket[_user]); } /** @@ -1056,13 +1069,25 @@ contract VolumeRestrictionTM is VolumeRestrictionTMStorage, TransferManager { * @return uint256 sumOfLastPeriod * @return uint256 days covered * @return uint256 24h lastTradedDayTime + * @return uint256 Timestamp at which last transaction get executed */ - function getDefaultBucketDetailsToUser(address _user) public view returns(uint256, uint256, uint256, uint256) { + function getDefaultBucketDetailsToUser(address _user) public view returns(uint256, uint256, uint256, uint256, uint256) { + return _getBucketDetails(bucketData.defaultUserToBucket[_user]); + } + + function _getBucketDetails(BucketDetails storage _bucket) internal view returns( + uint256, + uint256, + uint256, + uint256, + uint256 + ) { return( - bucketData.defaultUserToBucket[_user].lastTradedDayTime, - bucketData.defaultUserToBucket[_user].sumOfLastPeriod, - bucketData.defaultUserToBucket[_user].daysCovered, - bucketData.defaultUserToBucket[_user].dailyLastTradedDayTime + _bucket.lastTradedDayTime, + _bucket.sumOfLastPeriod, + _bucket.daysCovered, + _bucket.dailyLastTradedDayTime, + _bucket.lastTradedTimestamp ); } @@ -1072,7 +1097,7 @@ contract VolumeRestrictionTM is VolumeRestrictionTMStorage, TransferManager { * @param _at Timestamp */ function getTotalTradedByUser(address _user, uint256 _at) external view returns(uint256) { - return bucketData.bucket[_user][_at]; + return (bucketData.bucket[_user][_at].add(bucketData.defaultBucket[_user][_at])); } /** @@ -1131,11 +1156,32 @@ contract VolumeRestrictionTM is VolumeRestrictionTMStorage, TransferManager { uint256[] memory startTime, uint256[] memory rollingPeriodInDays, uint256[] memory endTime, - uint8[] memory typeOfRestriction + RestrictionType[] memory typeOfRestriction ) { return VolumeRestrictionLib.getRestrictionData(holderToRestrictionType, individualRestrictions, getDataStore()); } + function _checkLengthOfArray( + address[] memory _holders, + uint256[] memory _allowedTokens, + uint256[] memory _startTimes, + uint256[] memory _rollingPeriodInDays, + uint256[] memory _endTimes, + RestrictionType[] memory _restrictionTypes + ) + internal + pure + { + require( + _holders.length == _allowedTokens.length && + _allowedTokens.length == _startTimes.length && + _startTimes.length == _rollingPeriodInDays.length && + _rollingPeriodInDays.length == _endTimes.length && + _endTimes.length == _restrictionTypes.length, + "Length mismatch" + ); + } + /** * @notice return the amount of tokens for a given user as per the partition */ @@ -1146,10 +1192,9 @@ contract VolumeRestrictionTM is VolumeRestrictionTMStorage, TransferManager { /** * @notice Returns the permissions flag that are associated with Percentage transfer Manager */ - function getPermissions() public view returns(bytes32[] memory ) { - bytes32[] memory allPermissions = new bytes32[](1); + function getPermissions() public view returns(bytes32[] memory allPermissions) { + allPermissions = new bytes32[](1); allPermissions[0] = ADMIN; - return allPermissions; } } diff --git a/contracts/modules/TransferManager/VRTM/VolumeRestrictionTMStorage.sol b/contracts/modules/TransferManager/VRTM/VolumeRestrictionTMStorage.sol index 00228124a..293aa5810 100644 --- a/contracts/modules/TransferManager/VRTM/VolumeRestrictionTMStorage.sol +++ b/contracts/modules/TransferManager/VRTM/VolumeRestrictionTMStorage.sol @@ -10,7 +10,7 @@ contract VolumeRestrictionTMStorage { enum TypeOfPeriod { MultipleDays, OneDay, Both } // Store the type of restriction corresponds to token holder address - mapping(address => uint8) holderToRestrictionType; + mapping(address => TypeOfPeriod) holderToRestrictionType; struct VolumeRestriction { // If typeOfRestriction is `Percentage` then allowedTokens will be in @@ -48,11 +48,14 @@ contract VolumeRestrictionTMStorage { uint256 sumOfLastPeriod; // It is the sum of transacted amount within the last rollingPeriodDays uint256 daysCovered; // No of days covered till (from the startTime of VolumeRestriction) uint256 dailyLastTradedDayTime; + uint256 lastTradedTimestamp; // It is the timestamp at which last transaction get executed } struct BucketData { // Storing _from => day's timestamp => total amount transact in a day --individual mapping(address => mapping(uint256 => uint256)) bucket; + // Storing _from => day's timestamp => total amount transact in a day --individual + mapping(address => mapping(uint256 => uint256)) defaultBucket; // Storing the information that used to validate the transaction mapping(address => BucketDetails) userToBucket; // Storing the information related to default restriction diff --git a/test/y_volume_restriction_tm.js b/test/y_volume_restriction_tm.js index 1b8819940..dbd1e7b94 100644 --- a/test/y_volume_restriction_tm.js +++ b/test/y_volume_restriction_tm.js @@ -75,6 +75,7 @@ contract('VolumeRestrictionTransferManager', accounts => { let tempArray = new Array(); let tempArray3 = new Array(); let tempArrayGlobal = new Array(); + let delegateArray = new Array(); // Initial fee for ticker registry and security token registry const initRegFee = new BN(web3.utils.toWei("1000")); @@ -91,6 +92,7 @@ contract('VolumeRestrictionTransferManager', accounts => { .toString()} Individual Total Trade on daily latestTimestamp : ${web3.utils.fromWei(await I_VolumeRestrictionTM.getTotalTradedByUser.call(account, data[3])) .toString()} + Last Transaction time in UTC: ${(new Date((data[4].toNumber()) * 1000 )).toUTCString()} `) } @@ -123,6 +125,14 @@ contract('VolumeRestrictionTransferManager', accounts => { return sum; } + async function setTime() { + let currentTime = await getLatestTime(); + let currentHour = (new Date(currentTime.toNumber() * 1000)).getUTCHours(); + console.log(`Earlier time ${new Date((await getLatestTime()).toNumber() * 1000).toUTCString()}`); + await increaseTime(duration.hours(24 - currentHour)); + console.log(`Current time ${new Date((await getLatestTime()).toNumber() * 1000).toUTCString()}`); + } + before(async() => { let newLatestTime = await getLatestTime(); fromTime = newLatestTime; @@ -781,7 +791,7 @@ contract('VolumeRestrictionTransferManager', accounts => { let tx = await I_VolumeRestrictionTM.addIndividualDailyRestriction( account_investor3, new BN(web3.utils.toWei("6")), - newLatestTime.add(new BN(duration.seconds(1))), + newLatestTime.add(new BN(duration.seconds(10))), newLatestTime.add(new BN(duration.days(4))), 0, { @@ -809,7 +819,7 @@ contract('VolumeRestrictionTransferManager', accounts => { it("Should transfer the tokens within the individual daily restriction limits", async() => { // transfer 2 tokens as per the limit - await increaseTime(5); // increase 5 seconds to layoff the time gap + await increaseTime(12); // increase 12 seconds to layoff the time gap let startTime = (await I_VolumeRestrictionTM.getIndividualDailyRestriction.call(account_investor3))[1].toString(); console.log(` Gas Estimation for the Individual daily tx - ${await I_SecurityToken.transfer.estimateGas(account_investor2, new BN(web3.utils.toWei("2")), {from: account_investor3})} @@ -890,6 +900,7 @@ contract('VolumeRestrictionTransferManager', accounts => { }); it("Should transfer tokens on the 2nd day by investor1 (Individual + Individual daily)", async() => { + await increaseTime(2); let startTime = (await I_VolumeRestrictionTM.getIndividualRestriction.call(account_investor1))[1].toString(); let rollingPeriod = (await I_VolumeRestrictionTM.getIndividualRestriction.call(account_investor1))[2].toString(); @@ -1102,7 +1113,7 @@ contract('VolumeRestrictionTransferManager', accounts => { await I_VolumeRestrictionTM.modifyIndividualDailyRestriction( account_investor3, new BN(web3.utils.toWei('3')), - 0, + newLatestTime.add(new BN(duration.seconds(10))), newLatestTime.add(new BN(duration.days(5))), 0, { @@ -1122,6 +1133,7 @@ contract('VolumeRestrictionTransferManager', accounts => { }); it("Should allow to sell to transfer more tokens by investor3", async() => { + await increaseTime(duration.seconds(15)); let startTime = (await I_VolumeRestrictionTM.getIndividualRestriction.call(account_investor3))[1].toString(); let startTimedaily = (await I_VolumeRestrictionTM.getIndividualDailyRestriction.call(account_investor3))[1].toString(); let rollingPeriod = (await I_VolumeRestrictionTM.getIndividualRestriction.call(account_investor3))[2].toString(); @@ -1345,6 +1357,7 @@ contract('VolumeRestrictionTransferManager', accounts => { }); it("Should fail to transfer above the daily limit", async() => { + await increaseTime(2); // increase time to layoff the time gap await catchRevert( I_SecurityToken.transfer(account_investor3, new BN(web3.utils.toWei("5")), {from: account_investor4}) ) @@ -1493,7 +1506,7 @@ contract('VolumeRestrictionTransferManager', accounts => { let newLatestTime = await getLatestTime(); await I_VolumeRestrictionTM.addDefaultDailyRestriction( new BN(web3.utils.toWei("2")), - 0, + newLatestTime.add(new BN(duration.seconds(10))), newLatestTime.add(new BN(duration.days(3))), 0, { @@ -1513,6 +1526,7 @@ contract('VolumeRestrictionTransferManager', accounts => { }); it("Should not able to transfer tokens more than the default daily restriction", async() => { + await increaseTime(duration.seconds(15)); await catchRevert( I_SecurityToken.transfer(account_investor2, new BN(web3.utils.toWei("3")), {from: account_investor3}) ); @@ -1537,7 +1551,7 @@ contract('VolumeRestrictionTransferManager', accounts => { assert.equal(data[0].toString(), new BN(startTime).add(new BN(duration.days(data[2].toString())))); assert.equal(web3.utils.fromWei(data[1]), await calculateSum(rollingPeriod, tempArray3)); assert.equal(data[2].toString(), 6); - assert.equal(data[3].toString(), new BN(startTimedaily).add(new BN(duration.days(1)))); + assert.equal(data[3].toString(), (new BN(startTimedaily).add(new BN(duration.days(1)))).toString()); assert.equal(amt, 2); }); }) @@ -1669,6 +1683,205 @@ contract('VolumeRestrictionTransferManager', accounts => { }); + describe("Test the major issue from the audit ( Possible accounting corruption between individualRestriction​ and ​defaultRestriction)", async() => { + + it("Should add the individual restriction for the delegate 2 address", async() => { + let currentTime = await getLatestTime(); + await I_GeneralTransferManager.modifyKYCData( + account_delegate2, + currentTime, + currentTime, + currentTime.add(new BN(duration.days(30))), + { + from: token_owner + } + ); + + await I_SecurityToken.issue(account_delegate2, new BN(web3.utils.toWei("50")), "0x0", { from: token_owner }); + // Use to set the time to start of the day 0:00 to test the edge case properly + await setTime(); + currentTime = await getLatestTime(); + await I_VolumeRestrictionTM.addIndividualRestriction( + account_delegate2, + web3.utils.toWei("12"), + currentTime.add(new BN(duration.minutes(1))), + new BN(2), + currentTime.add(new BN(duration.days(5.5))), + new BN(0), + { + from: token_owner + } + ); + assert.equal((await I_VolumeRestrictionTM.getIndividualRestriction.call(account_delegate2))[2].toNumber(), 2); + + let data = await I_VolumeRestrictionTM.getRestrictionData.call(); + await printRestrictedData(data); + + // Add default restriction as well + currentTime = await getLatestTime(); + await I_VolumeRestrictionTM.removeDefaultRestriction({from: token_owner}); + await I_VolumeRestrictionTM.addDefaultRestriction( + web3.utils.toWei("5"), + currentTime.add(new BN(duration.minutes(1))), + new BN(5), + currentTime.add(new BN(duration.days(20))), + new BN(0), + { + from: token_owner + } + ); + + data = await I_VolumeRestrictionTM.getDefaultRestriction.call(); + assert.equal(data[0].toString(), web3.utils.toWei("5")); + assert.equal(data[2].toString(), 5); + let dataRestriction = await I_VolumeRestrictionTM.getDefaultRestriction.call(); + console.log(` + *** Add Default restriction data *** + Allowed Tokens: ${web3.utils.fromWei(dataRestriction[0])} + StartTime : ${dataRestriction[1].toNumber()} + Rolling Period in days : ${dataRestriction[2].toNumber()} + EndTime : ${dataRestriction[3].toNumber()} + Type of Restriction: ${dataRestriction[4].toNumber()} + `); + }); + + it("Should transact with delegate address 2", async() => { + await increaseTime(duration.minutes(2)); + + let startTime = (await I_VolumeRestrictionTM.getIndividualRestriction.call(account_delegate2))[1].toNumber(); + let rollingPeriod = (await I_VolumeRestrictionTM.getIndividualRestriction.call(account_delegate2))[2].toNumber(); + //sell tokens upto the limit + await I_SecurityToken.transfer(account_investor2, web3.utils.toWei("6"), {from: account_delegate2}); + delegateArray.push(6); + + console.log(`Print the default bucket details`); + let data = await I_VolumeRestrictionTM.getDefaultBucketDetailsToUser.call(account_delegate2); + await print(data, account_delegate2); + assert.equal(data[0].toNumber(), 0); + assert.equal(data[1].toNumber(), 0); + + console.log(`Print the individual bucket details`); + data = await I_VolumeRestrictionTM.getIndividualBucketDetailsToUser.call(account_delegate2); + await print(data, account_delegate2); + + // get the trade amount using the timestamp + let amt = web3.utils.fromWei(await I_VolumeRestrictionTM.getTotalTradedByUser.call(account_delegate2, data[0].toString())); + // Verify the storage changes + assert.equal(data[0].toNumber(), startTime + duration.days(data[2].toNumber())); + assert.equal(web3.utils.fromWei(data[1].toString()), await calculateSum(rollingPeriod, delegateArray)); + assert.equal(data[2].toNumber(), 0); + assert.equal(amt, 6); + + // Sell more tokens + await increaseTime(duration.days(5.1)); + await I_SecurityToken.transfer(account_investor2, web3.utils.toWei("9"), {from: account_delegate2}); + + delegateArray.push(9); + + console.log(`Print the default bucket details`); + let dataDefault = await I_VolumeRestrictionTM.getDefaultBucketDetailsToUser.call(account_delegate2); + await print(dataDefault, account_delegate2); + assert.equal(dataDefault[0].toNumber(), 0); + assert.equal(dataDefault[1].toNumber(), 0); + console.log(`Print the individual bucket details`); + let dataIndividual = await I_VolumeRestrictionTM.getIndividualBucketDetailsToUser.call(account_delegate2); + await print(dataIndividual, account_delegate2); + + // get the trade amount using the timestamp + let amtTraded = web3.utils.fromWei(await I_VolumeRestrictionTM.getTotalTradedByUser.call(account_delegate2, dataIndividual[0].toString())); + // Verify the storage changes + assert.equal(dataIndividual[0].toNumber(), startTime + duration.days(dataIndividual[2].toNumber())); + assert.equal(dataIndividual[2].toNumber(), 5); + assert.equal(amtTraded, 9); + }); + + it("Should fail to transact -- edge case when user restriction changes and do the transfer on the same day", async() => { + await increaseTime(duration.days(0.6)); + //sell tokens upto the limit + await catchRevert( + I_SecurityToken.transfer(account_investor2, web3.utils.toWei("5"), {from: account_delegate2}) + ); + }); + + it("Should transact under the default restriction unaffected from the edge case", async() => { + await increaseTime(duration.days(0.5)); + let individualStartTime = (await I_VolumeRestrictionTM.getIndividualRestriction.call(account_delegate2))[1].toNumber(); + let startTime = (await I_VolumeRestrictionTM.getDefaultRestriction.call())[1].toNumber(); + let rollingPeriod = (await I_VolumeRestrictionTM.getDefaultRestriction.call())[2].toNumber(); + + //sell tokens upto the limit + await I_SecurityToken.transfer(account_investor2, web3.utils.toWei("4"), {from: account_delegate2}); + + console.log(`Print the individual bucket details`); + let dataIndividual = await I_VolumeRestrictionTM.getIndividualBucketDetailsToUser.call(account_delegate2); + await print(dataIndividual, account_delegate2); + console.log(dataIndividual[4].toString()); + + // Verify the storage changes + assert.equal(dataIndividual[0].toNumber(), individualStartTime + duration.days(dataIndividual[2].toNumber())); + assert.equal(dataIndividual[2].toNumber(), 5); + + console.log(`Print the default bucket details`); + let data = await I_VolumeRestrictionTM.getDefaultBucketDetailsToUser.call(account_delegate2); + await print(data, account_delegate2); + console.log(data[4].toString()); + + // get the trade amount using the timestamp + let amt = web3.utils.fromWei(await I_VolumeRestrictionTM.getTotalTradedByUser.call(account_delegate2, data[0].toString())); + // Verify the storage changes + assert.equal(data[0].toNumber(), startTime + duration.days(data[2].toNumber())); + assert.equal(data[2].toNumber(), 6); + assert.equal(amt, 4); + }); + + it("Should check whether user is able to transfer when amount is less than the restriction limit (when restriction change)", async() => { + let currentTime = await getLatestTime(); + await I_VolumeRestrictionTM.addIndividualRestriction( + account_delegate2, + web3.utils.toWei("7"), + currentTime.add(new BN(duration.minutes(1))), + 1, + currentTime.add(new BN(duration.days(2))), + 0, + { + from: token_owner + } + ); + assert.equal((await I_VolumeRestrictionTM.getIndividualRestriction.call(account_delegate2))[2].toNumber(), 1); + let individualStartTime = (await I_VolumeRestrictionTM.getIndividualRestriction.call(account_delegate2))[1].toNumber(); + let startTime = (await I_VolumeRestrictionTM.getDefaultRestriction.call())[1].toNumber(); + let rollingPeriod = (await I_VolumeRestrictionTM.getDefaultRestriction.call())[2].toNumber(); + + await increaseTime(duration.minutes(2)); + + // sell tokens when user restriction changes from the default restriction to individual restriction + await catchRevert (I_SecurityToken.transfer(account_investor1, web3.utils.toWei("5")), {from: account_delegate2}); + + // allow to transact when the day limit is with in the restriction. default allow to transact maximum 5 tokens within + // a given rolling period. 4 tokens are already sold here user trying to sell 1 more token on the same day + await I_SecurityToken.transfer(account_investor1, web3.utils.toWei("1"), {from: account_delegate2}); + + console.log(`Print the individual bucket details`); + let dataIndividual = await I_VolumeRestrictionTM.getIndividualBucketDetailsToUser.call(account_delegate2); + await print(dataIndividual, account_delegate2); + + // Verify the storage changes + assert.equal(dataIndividual[0].toNumber(), individualStartTime + duration.days(dataIndividual[2].toNumber())); + assert.equal(dataIndividual[2].toNumber(), 0); + // get the trade amount using the timestamp + let amt = web3.utils.fromWei(await I_VolumeRestrictionTM.getTotalTradedByUser.call(account_delegate2, dataIndividual[0].toString())); + assert.equal(amt, 1); + + console.log(`Print the default bucket details`); + let data = await I_VolumeRestrictionTM.getDefaultBucketDetailsToUser.call(account_delegate2); + await print(data, account_delegate2); + + // Verify the storage changes + assert.equal(data[0].toNumber(), startTime + duration.days(data[2].toNumber())); + assert.equal(data[2].toNumber(), 6); + }); + }); + describe("VolumeRestriction Transfer Manager Factory test cases", async() => { it("Should get the exact details of the factory", async() => {