-
Notifications
You must be signed in to change notification settings - Fork 152
Lockup Volume Restrictions #290
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
adamdossa
merged 33 commits into
PolymathNetwork:development-1.5.0
from
deconet:volume-restriction-transfer-manager
Oct 5, 2018
Merged
Changes from all commits
Commits
Show all changes
33 commits
Select commit
Hold shift + click to select a range
65bcc50
first commit. contracts compile, but need to write tests
glitch003 cd6289c
basic tests pass
glitch003 164e247
more tests
glitch003 9a69ab6
more tests
glitch003 51a9d1f
more tests
glitch003 140bbf3
better tests
glitch003 2ecc821
more coverage
glitch003 8dc4a28
missed one branch of coverage. this should fix it
glitch003 2c28750
put back old package-lock.json
glitch003 02cd36e
comment fix
glitch003 ed0c3b9
added one more test
glitch003 5e844d7
Merge branch 'development-1.5.0' into volume-restriction-transfer-man…
glitch003 39513f4
make transfer fail if lockup startTime hasn't passed yet
glitch003 6421e09
Merge pull request #2 from PolymathNetwork/development-1.5.0
glitch003 c36217c
merged dev-1.5.0 latest in and fixed tests to work with that
glitch003 7395a8c
put back old package-lock.json
glitch003 5a2c338
add newline to end of package-lock.json to clean up PR
glitch003 5781936
code in place to track balances per lockup. but i don't think this w…
glitch003 64b627f
made changes requested
glitch003 b31eaf4
changed to store withdrawn balances inside each lockup
glitch003 d935adf
Merge branch 'volume-restriction-transfer-manager' into volume-restri…
glitch003 5f725af
Merge pull request #4 from deconet/volume-restriction-transfer-manage…
glitch003 3904fa9
Merge pull request #5 from PolymathNetwork/development-1.5.0
glitch003 52a3016
Merge pull request #6 from deconet/development-1.5.0
glitch003 c9b67ef
rename test
glitch003 db3e27d
updated before hook to use new stuff so that tests will run
glitch003 30bd69a
Added a test case
glitch003 7b8d302
make releaseFrequencySeconds a bit longer in a test so that the tests…
glitch003 1343f65
Merge branch 'development-1.5.0' into volume-restriction-transfer-man…
adamdossa c392a12
Update VolumeRestrictionTransferManager.sol
adamdossa 54393b0
Update w_volume_restriction_transfer_manager.js
adamdossa e984447
fixed to work with latest dev-1.5.0 changes
glitch003 6ec89eb
minor fixes
SatyamSB File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
374 changes: 374 additions & 0 deletions
374
contracts/modules/TransferManager/VolumeRestrictionTransferManager.sol
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,374 @@ | ||
| pragma solidity ^0.4.24; | ||
|
|
||
| import "./ITransferManager.sol"; | ||
| import "openzeppelin-solidity/contracts/math/SafeMath.sol"; | ||
|
|
||
|
|
||
| contract VolumeRestrictionTransferManager is ITransferManager { | ||
|
|
||
| using SafeMath for uint256; | ||
|
|
||
| // permission definition | ||
| bytes32 public constant ADMIN = "ADMIN"; | ||
|
|
||
| // a per-user lockup | ||
| struct LockUp { | ||
| uint lockUpPeriodSeconds; // total period of lockup (seconds) | ||
| uint releaseFrequencySeconds; // how often to release a tranche of tokens (seconds) | ||
| uint startTime; // when this lockup starts (seconds) | ||
| uint totalAmount; // total amount of locked up tokens | ||
| uint alreadyWithdrawn; // amount already withdrawn for this lockup | ||
| } | ||
|
|
||
| // maps user addresses to an array of lockups for that user | ||
| mapping (address => LockUp[]) internal lockUps; | ||
|
|
||
| event AddNewLockUp( | ||
| address indexed userAddress, | ||
| uint lockUpPeriodSeconds, | ||
| uint releaseFrequencySeconds, | ||
| uint startTime, | ||
| uint totalAmount, | ||
| uint indexed addedIndex | ||
| ); | ||
|
|
||
| event RemoveLockUp( | ||
| address indexed userAddress, | ||
| uint lockUpPeriodSeconds, | ||
| uint releaseFrequencySeconds, | ||
| uint startTime, | ||
| uint totalAmount, | ||
| uint indexed removedIndex | ||
| ); | ||
|
|
||
| event ModifyLockUp( | ||
| address indexed userAddress, | ||
| uint lockUpPeriodSeconds, | ||
| uint releaseFrequencySeconds, | ||
| uint startTime, | ||
| uint totalAmount, | ||
| uint indexed modifiedIndex | ||
| ); | ||
|
|
||
| /** | ||
| * @notice Constructor | ||
| * @param _securityToken Address of the security token | ||
| * @param _polyAddress Address of the polytoken | ||
| */ | ||
| constructor (address _securityToken, address _polyAddress) | ||
| public | ||
| Module(_securityToken, _polyAddress) | ||
| { | ||
| } | ||
|
|
||
|
|
||
| /** @notice Used to verify the transfer transaction and prevent locked up tokens from being transferred | ||
| * @param _from Address of the sender | ||
| * @param _amount The amount of tokens to transfer | ||
| * @param _isTransfer Whether or not this is an actual transfer or just a test to see if the tokens would be transferrable | ||
| */ | ||
| function verifyTransfer(address _from, address /* _to*/, uint256 _amount, bytes /* _data */, bool _isTransfer) public returns(Result) { | ||
| // only attempt to verify the transfer if the token is unpaused, this isn't a mint txn, and there exists a lockup for this user | ||
| if (!paused && _from != address(0) && lockUps[_from].length != 0) { | ||
| // check if this transfer is valid | ||
| return _checkIfValidTransfer(_from, _amount, _isTransfer); | ||
| } | ||
| return Result.NA; | ||
| } | ||
|
|
||
| /** | ||
| * @notice Lets the admin create a volume restriction lockup for a given address. | ||
| * @param userAddress Address of the user whose tokens should be locked up | ||
| * @param lockUpPeriodSeconds Total period of lockup (seconds) | ||
| * @param releaseFrequencySeconds How often to release a tranche of tokens (seconds) | ||
| * @param startTime When this lockup starts (seconds) | ||
| * @param totalAmount Total amount of locked up tokens | ||
| */ | ||
| function addLockUp(address userAddress, uint lockUpPeriodSeconds, uint releaseFrequencySeconds, uint startTime, uint totalAmount) public withPerm(ADMIN) { | ||
|
|
||
| _checkLockUpParams(lockUpPeriodSeconds, releaseFrequencySeconds, totalAmount); | ||
|
|
||
| // if a startTime of 0 is passed in, then start now. | ||
| if (startTime == 0) { | ||
| startTime = now; | ||
| } | ||
|
|
||
| lockUps[userAddress].push(LockUp(lockUpPeriodSeconds, releaseFrequencySeconds, startTime, totalAmount, 0)); | ||
|
|
||
| emit AddNewLockUp( | ||
| userAddress, | ||
| lockUpPeriodSeconds, | ||
| releaseFrequencySeconds, | ||
| startTime, | ||
| totalAmount, | ||
| lockUps[userAddress].length - 1 | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * @notice Lets the admin create multiple volume restriction lockups for multiple given addresses. | ||
| * @param userAddresses Array of address of the user whose tokens should be locked up | ||
| * @param lockUpPeriodsSeconds Array of total periods of lockup (seconds) | ||
| * @param releaseFrequenciesSeconds Array of how often to release a tranche of tokens (seconds) | ||
| * @param startTimes Array of When this lockup starts (seconds) | ||
| * @param totalAmounts Array of total amount of locked up tokens | ||
| */ | ||
| function addLockUpMulti(address[] userAddresses, uint[] lockUpPeriodsSeconds, uint[] releaseFrequenciesSeconds, uint[] startTimes, uint[] totalAmounts) external withPerm(ADMIN) { | ||
|
|
||
| // make sure input params are sane | ||
| require( | ||
| userAddresses.length == lockUpPeriodsSeconds.length && | ||
| userAddresses.length == releaseFrequenciesSeconds.length && | ||
| userAddresses.length == startTimes.length && | ||
| userAddresses.length == totalAmounts.length, | ||
| "Input array length mis-match" | ||
| ); | ||
|
|
||
| for (uint i = 0; i < userAddresses.length; i++) { | ||
| addLockUp(userAddresses[i], lockUpPeriodsSeconds[i], releaseFrequenciesSeconds[i], startTimes[i], totalAmounts[i]); | ||
| } | ||
|
|
||
| } | ||
|
|
||
| /** | ||
| * @notice Lets the admin remove a user's lock up | ||
| * @param userAddress Address of the user whose tokens are locked up | ||
| * @param lockUpIndex The index of the LockUp to remove for the given userAddress | ||
| */ | ||
| function removeLockUp(address userAddress, uint lockUpIndex) public withPerm(ADMIN) { | ||
| LockUp[] storage userLockUps = lockUps[userAddress]; | ||
| require(lockUpIndex < userLockUps.length, "Array out of bounds exception"); | ||
|
|
||
| LockUp memory toRemove = userLockUps[lockUpIndex]; | ||
|
|
||
| emit RemoveLockUp( | ||
| userAddress, | ||
| toRemove.lockUpPeriodSeconds, | ||
| toRemove.releaseFrequencySeconds, | ||
| toRemove.startTime, | ||
| toRemove.totalAmount, | ||
| lockUpIndex | ||
| ); | ||
|
|
||
| if (lockUpIndex < userLockUps.length - 1) { | ||
| // move the last element in the array into the index that is desired to be removed. | ||
| userLockUps[lockUpIndex] = userLockUps[userLockUps.length - 1]; | ||
| } | ||
| // delete the last element | ||
| userLockUps.length--; | ||
glitch003 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| /** | ||
| * @notice Lets the admin modify a volume restriction lockup for a given address. | ||
| * @param userAddress Address of the user whose tokens should be locked up | ||
| * @param lockUpIndex The index of the LockUp to edit for the given userAddress | ||
| * @param lockUpPeriodSeconds Total period of lockup (seconds) | ||
| * @param releaseFrequencySeconds How often to release a tranche of tokens (seconds) | ||
| * @param startTime When this lockup starts (seconds) | ||
| * @param totalAmount Total amount of locked up tokens | ||
| */ | ||
| function modifyLockUp(address userAddress, uint lockUpIndex, uint lockUpPeriodSeconds, uint releaseFrequencySeconds, uint startTime, uint totalAmount) public withPerm(ADMIN) { | ||
| require(lockUpIndex < lockUps[userAddress].length, "Array out of bounds exception"); | ||
|
|
||
| // if a startTime of 0 is passed in, then start now. | ||
| if (startTime == 0) { | ||
| startTime = now; | ||
| } | ||
|
|
||
| _checkLockUpParams(lockUpPeriodSeconds, releaseFrequencySeconds, totalAmount); | ||
|
|
||
| // Get the lockup from the master list and edit it | ||
| lockUps[userAddress][lockUpIndex] = LockUp( | ||
| lockUpPeriodSeconds, | ||
| releaseFrequencySeconds, | ||
| startTime, | ||
| totalAmount, | ||
| lockUps[userAddress][lockUpIndex].alreadyWithdrawn | ||
| ); | ||
|
|
||
glitch003 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| emit ModifyLockUp( | ||
| userAddress, | ||
| lockUpPeriodSeconds, | ||
| releaseFrequencySeconds, | ||
| startTime, | ||
| totalAmount, | ||
| lockUpIndex | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * @notice Get the length of the lockups array for a specific user address | ||
| * @param userAddress Address of the user whose tokens should be locked up | ||
| */ | ||
| function getLockUpsLength(address userAddress) public view returns (uint) { | ||
| return lockUps[userAddress].length; | ||
| } | ||
|
|
||
| /** | ||
| * @notice Get a specific element in a user's lockups array given the user's address and the element index | ||
| * @param userAddress Address of the user whose tokens should be locked up | ||
| * @param lockUpIndex The index of the LockUp to edit for the given userAddress | ||
| */ | ||
| function getLockUp(address userAddress, uint lockUpIndex) public view returns (uint lockUpPeriodSeconds, uint releaseFrequencySeconds, uint startTime, uint totalAmount, uint alreadyWithdrawn) { | ||
| require(lockUpIndex < lockUps[userAddress].length, "Array out of bounds exception"); | ||
| LockUp storage userLockUp = lockUps[userAddress][lockUpIndex]; | ||
| return ( | ||
| userLockUp.lockUpPeriodSeconds, | ||
| userLockUp.releaseFrequencySeconds, | ||
| userLockUp.startTime, | ||
| userLockUp.totalAmount, | ||
| userLockUp.alreadyWithdrawn | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * @notice This function returns the signature of configure function | ||
| */ | ||
| function getInitFunction() public pure returns (bytes4) { | ||
| return bytes4(0); | ||
| } | ||
|
|
||
| /** | ||
| * @notice Return the permissions flag that are associated with Percentage transfer Manager | ||
| */ | ||
| function getPermissions() public view returns(bytes32[]) { | ||
| bytes32[] memory allPermissions = new bytes32[](1); | ||
| allPermissions[0] = ADMIN; | ||
| return allPermissions; | ||
| } | ||
|
|
||
| /** | ||
| * @notice Takes a userAddress as input, and returns a uint that represents the number of tokens allowed to be withdrawn right now | ||
| * @param userAddress Address of the user whose lock ups should be checked | ||
| */ | ||
| function _checkIfValidTransfer(address userAddress, uint amount, bool isTransfer) internal returns (Result) { | ||
| // get lock up array for this user | ||
| LockUp[] storage userLockUps = lockUps[userAddress]; | ||
|
|
||
| // maps the index of userLockUps to the amount allowed in this transfer | ||
| uint[] memory allowedAmountPerLockup = new uint[](userLockUps.length); | ||
|
|
||
| uint[3] memory tokenSums = [ | ||
| uint256(0), // allowed amount right now | ||
| uint256(0), // total locked up, ever | ||
| uint256(0) // already withdrawn, ever | ||
| ]; | ||
|
|
||
| // loop over the user's lock ups | ||
| for (uint i = 0; i < userLockUps.length; i++) { | ||
| LockUp storage aLockUp = userLockUps[i]; | ||
|
|
||
| uint allowedAmountForThisLockup = 0; | ||
|
|
||
| // check if lockup has entirely passed | ||
| if (now >= aLockUp.startTime.add(aLockUp.lockUpPeriodSeconds)) { | ||
| // lockup has passed, or not started yet. allow all. | ||
| allowedAmountForThisLockup = aLockUp.totalAmount.sub(aLockUp.alreadyWithdrawn); | ||
| } else if (now >= aLockUp.startTime) { | ||
| // lockup is active. calculate how many to allow to be withdrawn right now | ||
| // calculate how many periods have elapsed already | ||
| uint elapsedPeriods = (now.sub(aLockUp.startTime)).div(aLockUp.releaseFrequencySeconds); | ||
| // calculate the total number of periods, overall | ||
| uint totalPeriods = aLockUp.lockUpPeriodSeconds.div(aLockUp.releaseFrequencySeconds); | ||
| // calculate how much should be released per period | ||
| uint amountPerPeriod = aLockUp.totalAmount.div(totalPeriods); | ||
| // calculate the number of tokens that should be released, | ||
| // multiplied by the number of periods that have elapsed already | ||
| // and add it to the total tokenSums[0] | ||
| allowedAmountForThisLockup = amountPerPeriod.mul(elapsedPeriods).sub(aLockUp.alreadyWithdrawn); | ||
|
|
||
| } | ||
| // tokenSums[0] is allowed sum | ||
| tokenSums[0] = tokenSums[0].add(allowedAmountForThisLockup); | ||
| // tokenSums[1] is total locked up | ||
| tokenSums[1] = tokenSums[1].add(aLockUp.totalAmount); | ||
| // tokenSums[2] is total already withdrawn | ||
| tokenSums[2] = tokenSums[2].add(aLockUp.alreadyWithdrawn); | ||
|
|
||
| allowedAmountPerLockup[i] = allowedAmountForThisLockup; | ||
| } | ||
|
|
||
| // tokenSums[0] is allowed sum | ||
| if (amount <= tokenSums[0]) { | ||
| // transfer is valid and will succeed. | ||
| if (!isTransfer) { | ||
| // if this isn't a real transfer, don't subtract the withdrawn amounts from the lockups. it's a "read only" txn | ||
| return Result.VALID; | ||
| } | ||
|
|
||
| // we are going to write the withdrawn balances back to the lockups, so make sure that the person calling this function is the securityToken itself, since its public | ||
| require(msg.sender == securityToken, "Sender is not securityToken"); | ||
|
|
||
| // subtract amounts so they are now known to be withdrawen | ||
| for (i = 0; i < userLockUps.length; i++) { | ||
| aLockUp = userLockUps[i]; | ||
|
|
||
| // tokenSums[0] is allowed sum | ||
| if (allowedAmountPerLockup[i] >= tokenSums[0]) { | ||
| aLockUp.alreadyWithdrawn = aLockUp.alreadyWithdrawn.add(tokenSums[0]); | ||
| // we withdrew the entire tokenSums[0] from the lockup. We are done. | ||
| break; | ||
| } else { | ||
| // we have to split the tokenSums[0] across mutiple lockUps | ||
| aLockUp.alreadyWithdrawn = aLockUp.alreadyWithdrawn.add(allowedAmountPerLockup[i]); | ||
| // subtract the amount withdrawn from this lockup | ||
| tokenSums[0] = tokenSums[0].sub(allowedAmountPerLockup[i]); | ||
| } | ||
|
|
||
| } | ||
| return Result.VALID; | ||
| } | ||
|
|
||
| return _checkIfUnlockedTokenTransferIsPossible(userAddress, amount, tokenSums[1], tokenSums[2]); | ||
| } | ||
|
|
||
| function _checkIfUnlockedTokenTransferIsPossible(address userAddress, uint amount, uint totalSum, uint alreadyWithdrawnSum) internal view returns (Result) { | ||
| // the amount the user wants to withdraw is greater than their allowed amounts according to the lockups. however, if the user has like, 10 tokens, but only 4 are locked up, we should let the transfer go through for those 6 that aren't locked up | ||
| uint currentUserBalance = ISecurityToken(securityToken).balanceOf(userAddress); | ||
glitch003 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| uint stillLockedAmount = totalSum.sub(alreadyWithdrawnSum); | ||
| if (currentUserBalance >= stillLockedAmount && amount <= currentUserBalance.sub(stillLockedAmount)) { | ||
| // the user has more tokens in their balance than are actually locked up. they should be allowed to withdraw the difference | ||
| return Result.VALID; | ||
| } | ||
| return Result.INVALID; | ||
| } | ||
|
|
||
|
|
||
| /** | ||
| * @notice Parameter checking function for creating or editing a lockup. This function will cause an exception if any of the parameters are bad. | ||
| * @param lockUpPeriodSeconds Total period of lockup (seconds) | ||
| * @param releaseFrequencySeconds How often to release a tranche of tokens (seconds) | ||
| * @param totalAmount Total amount of locked up tokens | ||
| */ | ||
| function _checkLockUpParams(uint lockUpPeriodSeconds, uint releaseFrequencySeconds, uint totalAmount) internal view { | ||
| require(lockUpPeriodSeconds != 0, "lockUpPeriodSeconds cannot be zero"); | ||
| require(releaseFrequencySeconds != 0, "releaseFrequencySeconds cannot be zero"); | ||
| require(totalAmount != 0, "totalAmount cannot be zero"); | ||
|
|
||
| // check that the total amount to be released isn't too granular | ||
| require( | ||
| totalAmount % ISecurityToken(securityToken).granularity() == 0, | ||
| "The total amount to be released is more granular than allowed by the token" | ||
| ); | ||
|
|
||
| // check that releaseFrequencySeconds evenly divides lockUpPeriodSeconds | ||
| require( | ||
| lockUpPeriodSeconds % releaseFrequencySeconds == 0, | ||
| "lockUpPeriodSeconds must be evenly divisible by releaseFrequencySeconds" | ||
| ); | ||
|
|
||
| // check that totalPeriods evenly divides totalAmount | ||
| uint totalPeriods = lockUpPeriodSeconds.div(releaseFrequencySeconds); | ||
| require( | ||
| totalAmount % totalPeriods == 0, | ||
| "The total amount being locked up must be evenly divisible by the number of total periods" | ||
| ); | ||
|
|
||
| // make sure the amount to be released per period is not too granular for the token | ||
| uint amountPerPeriod = totalAmount.div(totalPeriods); | ||
| require( | ||
| amountPerPeriod % ISecurityToken(securityToken).granularity() == 0, | ||
| "The amount to be released per period is more granular than allowed by the token" | ||
| ); | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.